diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java
new file mode 100644
index 00000000000..05bf91ba6ec
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/AnnotationFormatterFactory.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2004-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.ui.format;
+
+import java.lang.annotation.Annotation;
+
+/**
+ * A factory that creates {@link Formatter formatters} to format property values on properties annotated with a particular format {@link Annotation}.
+ * For example, a CurrencyAnnotationFormatterFactory might create a Formatter that formats a BigDecimal value set on a property annotated with @CurrencyFormat.
+ * @author Keith Donald
+ * @since 3.0
+ * @param The type of Annotation this factory uses to create Formatter instances
+ * @param The type of Object Formatters created by this factory format
+ */
+public interface AnnotationFormatterFactory {
+
+ /**
+ * Get the Formatter that will format the value of the property annotated with the provided annotation.
+ * The annotation instance can contain properties that may be used to configure the Formatter that is returned.
+ * @param annotation the annotation instance
+ * @return the Formatter to use to format values of properties annotated with the annotation.
+ */
+ Formatter getFormatter(A annotation);
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java
new file mode 100644
index 00000000000..bac18638852
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatted.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2004-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.ui.format;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A type that can be formatted as a String for display in a UI.
+ * @author Keith Donald
+ * @since 3.0
+ */
+@Target({ElementType.TYPE})
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface Formatted {
+
+ /**
+ * The Formatter that handles the formatting.
+ */
+ Class> value();
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java
new file mode 100644
index 00000000000..e9342f6169c
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/Formatter.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2004-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.ui.format;
+
+import java.text.ParseException;
+import java.util.Locale;
+
+/**
+ * Formats objects of type T for display.
+ * @author Keith Donald
+ * @since 3.0
+ * @param the type of object this formatter can format
+ */
+public interface Formatter {
+
+ /**
+ * Format the object of type T for display.
+ * @param object the object to format
+ * @param locale the user's locale
+ * @return the formatted display string
+ */
+ String format(T object, Locale locale);
+
+ /**
+ * Parse an object from its formatted representation.
+ * @param formatted a formatted representation
+ * @param locale the user's locale
+ * @return the parsed object
+ * @throws ParseException when a parse exception occurs
+ */
+ T parse(String formatted, Locale locale) throws ParseException;
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java b/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java
new file mode 100644
index 00000000000..62d997461de
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/FormatterRegistry.java
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2004-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.ui.format;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.core.convert.TypeDescriptor;
+
+/**
+ * A shared registry of Formatters.
+ * @author Keith Donald
+ * @since 3.0
+ */
+public interface FormatterRegistry {
+
+ /**
+ * Adds a Formatter to this registry indexed by .
+ * Calling getFormatter(<T>.class) returns formatter.
+ * @param formatter the formatter
+ * @param the type of object the formatter formats
+ */
+ void add(Formatter formatter);
+
+ /**
+ * Adds a Formatter to this registry indexed by objectType.
+ * Use this add method when objectType differs from <T>.
+ * Calling getFormatter(objectType) returns a decorator that wraps the targetFormatter.
+ * On format, the decorator first coerses the instance of objectType to <T>, then delegates to targetFormatter to format the value.
+ * On parse, the decorator first delegates to the formatter to parse a <T>, then coerses the parsed value to objectType.
+ * @param objectType the object type
+ * @param targetFormatter the target formatter
+ * @param the type of object the target formatter formats
+ */
+ void add(Class> objectType, Formatter targetFormatter);
+
+ /**
+ * Adds a AnnotationFormatterFactory that will format values of properties annotated with a specific annotation.
+ * @param factory the annotation formatter factory
+ */
+ void add(AnnotationFormatterFactory factory);
+
+ /**
+ * Get the Formatter for the type.
+ * @return the Formatter, or null if none is registered
+ */
+ Formatter getFormatter(TypeDescriptor type);
+
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/GenericFormatterRegistry.java b/org.springframework.context/src/main/java/org/springframework/ui/format/GenericFormatterRegistry.java
new file mode 100644
index 00000000000..b4f85b0077b
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/GenericFormatterRegistry.java
@@ -0,0 +1,145 @@
+/*
+ * Copyright 2004-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.ui.format;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+import org.springframework.core.GenericTypeResolver;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.util.Assert;
+
+/**
+ * A generic implementation of {@link FormatterRegistry} suitable for use in most binding environments.
+ * @author Keith Donald
+ * @since 3.0
+ * @see #add(Class, Formatter)
+ * @see #add(AnnotationFormatterFactory)
+ */
+@SuppressWarnings("unchecked")
+public class GenericFormatterRegistry implements FormatterRegistry {
+
+ private Map typeFormatters = new ConcurrentHashMap();
+
+ private Map annotationFormatters = new HashMap();
+
+ public void add(Formatter formatter) {
+ // TODO
+ }
+
+ public void add(Class> objectType, Formatter formatter) {
+ if (objectType.isAnnotation()) {
+ annotationFormatters.put(objectType, new SimpleAnnotationFormatterFactory(formatter));
+ } else {
+ typeFormatters.put(objectType, formatter);
+ }
+ }
+
+ public void add(AnnotationFormatterFactory factory) {
+ annotationFormatters.put(getAnnotationType(factory), factory);
+ }
+
+ public Formatter> getFormatter(TypeDescriptor type) {
+ Assert.notNull(type, "The TypeDescriptor is required");
+ Annotation[] annotations = type.getAnnotations();
+ for (Annotation a : annotations) {
+ AnnotationFormatterFactory factory = annotationFormatters.get(a.annotationType());
+ if (factory != null) {
+ return factory.getFormatter(a);
+ }
+ }
+ return getFormatter(type.getType());
+ }
+
+ // internal helpers
+
+ private Formatter> getFormatter(Class> type) {
+ Assert.notNull(type, "The Class of the object to format is required");
+ Formatter formatter = typeFormatters.get(type);
+ if (formatter != null) {
+ return formatter;
+ } else {
+ Formatted formatted = AnnotationUtils.findAnnotation(type, Formatted.class);
+ if (formatted != null) {
+ Class formatterClass = formatted.value();
+ try {
+ formatter = (Formatter) formatterClass.newInstance();
+ } catch (InstantiationException e) {
+ throw new IllegalStateException(
+ "Formatter referenced by @Formatted annotation does not have default constructor", e);
+ } catch (IllegalAccessException e) {
+ throw new IllegalStateException(
+ "Formatter referenced by @Formatted annotation does not have public constructor", e);
+ }
+ typeFormatters.put(type, formatter);
+ return formatter;
+ } else {
+ return null;
+ }
+ }
+ }
+
+ private Class getAnnotationType(AnnotationFormatterFactory factory) {
+ Class classToIntrospect = factory.getClass();
+ while (classToIntrospect != null) {
+ Type[] genericInterfaces = classToIntrospect.getGenericInterfaces();
+ for (Type genericInterface : genericInterfaces) {
+ if (genericInterface instanceof ParameterizedType) {
+ ParameterizedType pInterface = (ParameterizedType) genericInterface;
+ if (AnnotationFormatterFactory.class.isAssignableFrom((Class) pInterface.getRawType())) {
+ return getParameterClass(pInterface.getActualTypeArguments()[0], factory.getClass());
+ }
+ }
+ }
+ classToIntrospect = classToIntrospect.getSuperclass();
+ }
+ throw new IllegalArgumentException(
+ "Unable to extract Annotation type A argument from AnnotationFormatterFactory ["
+ + factory.getClass().getName() + "]; does the factory parameterize the generic type?");
+ }
+
+ private Class getParameterClass(Type parameterType, Class converterClass) {
+ if (parameterType instanceof TypeVariable) {
+ parameterType = GenericTypeResolver.resolveTypeVariable((TypeVariable) parameterType, converterClass);
+ }
+ if (parameterType instanceof Class) {
+ return (Class) parameterType;
+ }
+ throw new IllegalArgumentException("Unable to obtain the java.lang.Class for parameterType [" + parameterType
+ + "] on Formatter [" + converterClass.getName() + "]");
+ }
+
+ private static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory {
+
+ private Formatter formatter;
+
+ public SimpleAnnotationFormatterFactory(Formatter formatter) {
+ this.formatter = formatter;
+ }
+
+ public Formatter getFormatter(Annotation annotation) {
+ return formatter;
+ }
+
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java
new file mode 100644
index 00000000000..d5c5c152bd8
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/date/DateFormatter.java
@@ -0,0 +1,90 @@
+/*
+ * Copyright 2004-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.ui.format.date;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.ui.format.Formatter;
+
+/**
+ * A formatter for {@link Date} types.
+ * Allows the configuration of an explicit date pattern and locale.
+ * @author Keith Donald
+ * @since 3.0
+ * @see SimpleDateFormat
+ */
+public class DateFormatter implements Formatter {
+
+ private static Log logger = LogFactory.getLog(DateFormatter.class);
+
+ /**
+ * The default date pattern.
+ */
+ private static final String DEFAULT_PATTERN = "yyyy-MM-dd";
+
+ private String pattern;
+
+ /**
+ * Sets the pattern to use to format date values.
+ * If not specified, the default pattern 'yyyy-MM-dd' is used.
+ * @param pattern the date formatting pattern
+ */
+ public void setPattern(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);
+ }
+
+ // subclassing hookings
+
+ protected DateFormat getDateFormat(Locale locale) {
+ DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT, locale);
+ format.setLenient(false);
+ if (format instanceof SimpleDateFormat) {
+ String pattern = determinePattern(this.pattern);
+ ((SimpleDateFormat) format).applyPattern(pattern);
+ } else {
+ logger.warn("Unable to apply format pattern '" + pattern
+ + "'; Returned DateFormat is not a SimpleDateFormat");
+ }
+ return format;
+ }
+
+ // internal helpers
+
+ private String determinePattern(String pattern) {
+ return pattern != null ? pattern : DEFAULT_PATTERN;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java
new file mode 100644
index 00000000000..60f5e723638
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/date/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Formatters for java.util.Date fields.
+ */
+package org.springframework.ui.format.date;
+
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java
new file mode 100644
index 00000000000..bd37362d6d7
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormat.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * A annotation to apply to a BigDecimal property to have its value formatted as currency amount using a {@link CurrencyFormatter}.
+ * @author Keith Donald
+ * @since 3.0
+ */
+@Target( { ElementType.METHOD, ElementType.FIELD })
+@Retention(RetentionPolicy.RUNTIME)
+@Documented
+public @interface CurrencyFormat {
+
+}
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java
new file mode 100644
index 00000000000..d12e1ea4323
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyFormatter.java
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+import org.springframework.ui.format.Formatter;
+
+/**
+ * A BigDecimal formatter for currency values.
+ * Delegates to {@link NumberFormat#getCurrencyInstance(Locale)}.
+ * Configures BigDecimal parsing so there is no loss of precision.
+ * Sets the scale of parsed BigDecimal values to {@link NumberFormat#getMaximumFractionDigits()}.
+ * Applies {@link RoundingMode#DOWN} to parsed values.
+ * @author Keith Donald
+ * @since 3.0
+ */
+public class CurrencyFormatter implements Formatter {
+
+ private CurrencyNumberFormatFactory currencyFormatFactory = new CurrencyNumberFormatFactory();
+
+ private boolean lenient;
+
+ public String format(BigDecimal decimal, Locale locale) {
+ if (decimal == null) {
+ return "";
+ }
+ NumberFormat format = currencyFormatFactory.getNumberFormat(locale);
+ return format.format(decimal);
+ }
+
+ public BigDecimal parse(String formatted, Locale locale)
+ throws ParseException {
+ if (formatted.length() == 0) {
+ return null;
+ }
+ NumberFormat format = currencyFormatFactory.getNumberFormat(locale);
+ ParsePosition position = new ParsePosition(0);
+ BigDecimal decimal = (BigDecimal) format.parse(formatted, position);
+ if (position.getErrorIndex() != -1) {
+ throw new ParseException(formatted, position.getIndex());
+ }
+ if (!lenient) {
+ if (formatted.length() != position.getIndex()) {
+ // indicates a part of the string that was not parsed
+ throw new ParseException(formatted, position.getIndex());
+ }
+ }
+ decimal = decimal.setScale(format.getMaximumFractionDigits(), format.getRoundingMode());
+ return decimal;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java
new file mode 100644
index 00000000000..648feb4ae0c
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/CurrencyNumberFormatFactory.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.math.RoundingMode;
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Produces NumberFormat instances that format currency values.
+ * @author Keith Donald
+ * @since 3.0
+ * @see NumberFormat
+ */
+final class CurrencyNumberFormatFactory extends NumberFormatFactory {
+
+ private RoundingMode roundingMode = RoundingMode.DOWN;
+
+ public NumberFormat getNumberFormat(Locale locale) {
+ DecimalFormat format = (DecimalFormat) NumberFormat.getCurrencyInstance(locale);
+ format.setParseBigDecimal(true);
+ format.setRoundingMode(roundingMode);
+ return format;
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java
new file mode 100644
index 00000000000..1ba182f5625
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DecimalFormatter.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.math.BigDecimal;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+import org.springframework.ui.format.Formatter;
+
+/**
+ * A BigDecimal formatter for decimal values.
+ * Delegates to {@link NumberFormat#getInstance(Locale)}.
+ * Configures BigDecimal parsing so there is no loss in precision.
+ * Allows configuration over the decimal number pattern; see {@link #DecimalFormatter(String)}.
+ * @author Keith Donald
+ * @since 3.0
+ */
+public class DecimalFormatter implements Formatter {
+
+ private DefaultNumberFormatFactory formatFactory = new DefaultNumberFormatFactory();
+
+ private boolean lenient;
+
+ public DecimalFormatter() {
+ initDefaults();
+ }
+
+ public DecimalFormatter(String pattern) {
+ initDefaults();
+ formatFactory.setPattern(pattern);
+ }
+
+ public String format(BigDecimal decimal, Locale locale) {
+ if (decimal == null) {
+ return "";
+ }
+ NumberFormat format = formatFactory.getNumberFormat(locale);
+ return format.format(decimal);
+ }
+
+ public BigDecimal parse(String formatted, Locale locale)
+ throws ParseException {
+ if (formatted.length() == 0) {
+ return null;
+ }
+ NumberFormat format = formatFactory.getNumberFormat(locale);
+ ParsePosition position = new ParsePosition(0);
+ BigDecimal decimal = (BigDecimal) format.parse(formatted, position);
+ if (position.getErrorIndex() != -1) {
+ throw new ParseException(formatted, position.getIndex());
+ }
+ if (!lenient) {
+ if (formatted.length() != position.getIndex()) {
+ // indicates a part of the string that was not parsed
+ throw new ParseException(formatted, position.getIndex());
+ }
+ }
+ return decimal;
+ }
+
+ private void initDefaults() {
+ formatFactory.setParseBigDecimal(true);
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java
new file mode 100644
index 00000000000..1cdd51262c8
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/DefaultNumberFormatFactory.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+
+/**
+ * Works with a general purpose {@link DecimalFormat} instance returned by calling
+ * {@link NumberFormat#getInstance(Locale)} by default.
+ * @author Keith Donald
+ * @see NumberFormat
+ * @see DecimalFormat
+ * @since 3.0
+ */
+class DefaultNumberFormatFactory extends NumberFormatFactory {
+
+ private static Log logger = LogFactory.getLog(DefaultNumberFormatFactory.class);
+
+ private String pattern;
+
+ private Boolean parseBigDecimal;
+
+ /**
+ * Sets the pattern to use to format number values.
+ * If not specified, the default DecimalFormat pattern is used.
+ * @param pattern the format pattern
+ * @see DecimalFormat#applyPattern(String)
+ */
+ public void setPattern(String pattern) {
+ this.pattern = pattern;
+ }
+
+ /**
+ * Sets whether the format should always parse a big decimal.
+ * @param parseBigDecimal the big decimal parse status
+ * @see DecimalFormat#setParseBigDecimal(boolean)
+ */
+ public void setParseBigDecimal(boolean parseBigDecimal) {
+ this.parseBigDecimal = parseBigDecimal;
+ }
+
+ public NumberFormat getNumberFormat(Locale locale) {
+ NumberFormat format = NumberFormat.getInstance(locale);
+ if (pattern != null) {
+ if (format instanceof DecimalFormat) {
+ ((DecimalFormat) format).applyPattern(pattern);
+ } else {
+ logger.warn("Unable to apply format pattern '" + pattern
+ + "'; Returned NumberFormat is not a DecimalFormat");
+ }
+ }
+ if (parseBigDecimal != null) {
+ if (format instanceof DecimalFormat) {
+ ((DecimalFormat) format).setParseBigDecimal(parseBigDecimal);
+ } else {
+ logger.warn("Unable to call setParseBigDecimal; not a DecimalFormat");
+ }
+ }
+ return format;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java
new file mode 100644
index 00000000000..3535dfc3837
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerFormatter.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+import org.springframework.ui.format.Formatter;
+
+/**
+ * A Long formatter for whole integer values.
+ * Delegates to {@link NumberFormat#getIntegerInstance(Locale)}.
+ * @author Keith Donald
+ * @since 3.0
+ */
+public class IntegerFormatter implements Formatter {
+
+ private IntegerNumberFormatFactory formatFactory = new IntegerNumberFormatFactory();
+
+ private boolean lenient;
+
+ public String format(Long integer, Locale locale) {
+ if (integer == null) {
+ return "";
+ }
+ NumberFormat format = formatFactory.getNumberFormat(locale);
+ return format.format(integer);
+ }
+
+ public Long parse(String formatted, Locale locale)
+ throws ParseException {
+ if (formatted.length() == 0) {
+ return null;
+ }
+ NumberFormat format = formatFactory.getNumberFormat(locale);
+ ParsePosition position = new ParsePosition(0);
+ Long integer = (Long) format.parse(formatted, position);
+ if (position.getErrorIndex() != -1) {
+ throw new ParseException(formatted, position.getIndex());
+ }
+ if (!lenient) {
+ if (formatted.length() != position.getIndex()) {
+ // indicates a part of the string that was not parsed
+ throw new ParseException(formatted, position.getIndex());
+ }
+ }
+ return integer;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java
new file mode 100644
index 00000000000..502c6dac998
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/IntegerNumberFormatFactory.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Produces NumberFormat instances that format integer values.
+ * @author Keith Donald
+ * @see NumberFormat
+ * @since 3.0
+ */
+final class IntegerNumberFormatFactory extends NumberFormatFactory {
+ public NumberFormat getNumberFormat(Locale locale) {
+ return NumberFormat.getIntegerInstance(locale);
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java
new file mode 100644
index 00000000000..66f871afa06
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/NumberFormatFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * A factory for {@link NumberFormat} objects.
+ * Conceals the complexity associated with configuring, constructing, and/or caching number format instances.
+ * @author Keith Donald
+ * @since 3.0
+ */
+abstract class NumberFormatFactory {
+
+ /**
+ * Factory method that returns a fully-configured {@link NumberFormat} instance to use to format an object for
+ * display.
+ * @return the number format
+ */
+ public abstract NumberFormat getNumberFormat(Locale locale);
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java
new file mode 100644
index 00000000000..5044b28464e
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentFormatter.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.math.BigDecimal;
+import java.text.NumberFormat;
+import java.text.ParseException;
+import java.text.ParsePosition;
+import java.util.Locale;
+
+import org.springframework.ui.format.Formatter;
+
+/**
+ * A BigDecimal formatter for percent values.
+ * Delegates to {@link NumberFormat#getPercentInstance(Locale)}.
+ * Configures BigDecimal parsing so there is no loss in precision.
+ * @author Keith Donald
+ * @since 3.0
+ */
+public class PercentFormatter implements Formatter {
+
+ private PercentNumberFormatFactory percentFormatFactory = new PercentNumberFormatFactory();
+
+ private boolean lenient;
+
+ public String format(BigDecimal decimal, Locale locale) {
+ if (decimal == null) {
+ return "";
+ }
+ NumberFormat format = percentFormatFactory.getNumberFormat(locale);
+ return format.format(decimal);
+ }
+
+ public BigDecimal parse(String formatted, Locale locale)
+ throws ParseException {
+ if (formatted.length() == 0) {
+ return null;
+ }
+ NumberFormat format = percentFormatFactory.getNumberFormat(locale);
+ ParsePosition position = new ParsePosition(0);
+ BigDecimal decimal = (BigDecimal) format.parse(formatted, position);
+ if (position.getErrorIndex() != -1) {
+ throw new ParseException(formatted, position.getIndex());
+ }
+ if (!lenient) {
+ if (formatted.length() != position.getIndex()) {
+ // indicates a part of the string that was not parsed
+ throw new ParseException(formatted, position.getIndex());
+ }
+ }
+ return decimal;
+ }
+
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java
new file mode 100644
index 00000000000..b6724a3e475
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/PercentNumberFormatFactory.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2004-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.ui.format.number;
+
+import java.text.DecimalFormat;
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * Produces NumberFormat instances that format percent values.
+ * @see NumberFormat
+ * @author Keith Donald
+ * @since 3.0
+ */
+final class PercentNumberFormatFactory extends NumberFormatFactory {
+ public NumberFormat getNumberFormat(Locale locale) {
+ DecimalFormat format = (DecimalFormat) NumberFormat.getPercentInstance(locale);
+ format.setParseBigDecimal(true);
+ return format;
+ }
+}
\ No newline at end of file
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java
new file mode 100644
index 00000000000..421a7800b6c
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/number/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * Formatters for java.lang.Number properties.
+ */
+package org.springframework.ui.format.number;
+
diff --git a/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java b/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java
new file mode 100644
index 00000000000..1e04819f108
--- /dev/null
+++ b/org.springframework.context/src/main/java/org/springframework/ui/format/package-info.java
@@ -0,0 +1,5 @@
+/**
+ * A SPI for defining Formatters to format field model values for display in a UI.
+ */
+package org.springframework.ui.format;
+
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java
new file mode 100644
index 00000000000..692f05ab5ee
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/GenericFormatterRegistryTests.java
@@ -0,0 +1,48 @@
+package org.springframework.ui.format;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Locale;
+
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.springframework.core.convert.TypeDescriptor;
+import org.springframework.ui.format.number.IntegerFormatter;
+
+public class GenericFormatterRegistryTests {
+
+ private GenericFormatterRegistry registry;
+
+ @Before
+ public void setUp() {
+ registry = new GenericFormatterRegistry();
+ }
+
+ @Test
+ @Ignore
+ public void testAdd() {
+ registry.add(new IntegerFormatter());
+ Formatter formatter = registry.getFormatter(typeDescriptor(Long.class));
+ String formatted = formatter.format(new Long(3), Locale.US);
+ assertEquals("3", formatted);
+ }
+
+ @Test
+ @Ignore
+ public void testAddByOtherObjectType() {
+ registry.add(Integer.class, new IntegerFormatter());
+ Formatter formatter = registry.getFormatter(typeDescriptor(Integer.class));
+ String formatted = formatter.format(new Integer(3), Locale.US);
+ assertEquals("3", formatted);
+ }
+
+ @Test
+ @Ignore
+ public void testAddAnnotationFormatterFactory() {
+ }
+
+ private static TypeDescriptor typeDescriptor(Class> clazz) {
+ return TypeDescriptor.valueOf(clazz);
+ }
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java
new file mode 100644
index 00000000000..8c12c709219
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/date/DateFormatterTests.java
@@ -0,0 +1,36 @@
+package org.springframework.ui.format.date;
+
+import static org.junit.Assert.assertEquals;
+
+import java.text.ParseException;
+import java.util.Calendar;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.springframework.ui.format.date.DateFormatter;
+
+public class DateFormatterTests {
+
+ private DateFormatter formatter = new DateFormatter();
+
+ @Test
+ public void formatValue() {
+ Calendar cal = Calendar.getInstance(Locale.US);
+ cal.clear();
+ cal.set(Calendar.YEAR, 2009);
+ cal.set(Calendar.MONTH, Calendar.JUNE);
+ cal.set(Calendar.DAY_OF_MONTH, 1);
+ assertEquals("2009-06-01", formatter.format(cal.getTime(), Locale.US));
+ }
+
+ @Test
+ public void parseValue() throws ParseException {
+ Calendar cal = Calendar.getInstance(Locale.US);
+ cal.clear();
+ cal.set(Calendar.YEAR, 2009);
+ cal.set(Calendar.MONTH, Calendar.JUNE);
+ cal.set(Calendar.DAY_OF_MONTH, 1);
+ assertEquals(cal.getTime(), formatter.parse("2009-06-01", Locale.US));
+ }
+
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java
new file mode 100644
index 00000000000..9eb3c26ba5f
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/CurrencyFormatterTests.java
@@ -0,0 +1,51 @@
+package org.springframework.ui.format.number;
+
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.springframework.ui.format.number.CurrencyFormatter;
+
+public class CurrencyFormatterTests {
+
+ private CurrencyFormatter formatter = new CurrencyFormatter();
+
+ @Test
+ public void formatValue() {
+ assertEquals("$23.00", formatter.format(new BigDecimal("23"), Locale.US));
+ }
+
+ @Test
+ public void parseValue() throws ParseException {
+ assertEquals(new BigDecimal("23.56"), formatter.parse("$23.56", Locale.US));
+ }
+
+ @Test
+ public void parseEmptyValue() throws ParseException {
+ assertEquals(null, formatter.parse("", Locale.US));
+ }
+
+ @Test(expected = ParseException.class)
+ public void parseBogusValue() throws ParseException {
+ formatter.parse("bogus", Locale.US);
+ }
+
+ @Test
+ public void parseValueDefaultRoundDown() throws ParseException {
+ assertEquals(new BigDecimal("23.56"), formatter.parse("$23.567", Locale.US));
+ }
+
+ @Test
+ public void parseWholeValue() throws ParseException {
+ assertEquals(new BigDecimal("23.00"), formatter.parse("$23", Locale.US));
+ }
+
+ @Test(expected=ParseException.class)
+ public void parseValueNotLenientFailure() throws ParseException {
+ formatter.parse("$23.56bogus", Locale.US);
+ }
+
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java
new file mode 100644
index 00000000000..2842d6ba2ad
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/DecimalFormatterTests.java
@@ -0,0 +1,41 @@
+package org.springframework.ui.format.number;
+
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.springframework.ui.format.number.DecimalFormatter;
+
+public class DecimalFormatterTests {
+
+ private DecimalFormatter formatter = new DecimalFormatter();
+
+ @Test
+ public void formatValue() {
+ assertEquals("23.56", formatter.format(new BigDecimal("23.56"), Locale.US));
+ }
+
+ @Test
+ public void parseValue() throws ParseException {
+ assertEquals(new BigDecimal("23.56"), formatter.parse("23.56", Locale.US));
+ }
+
+ @Test
+ public void parseEmptyValue() throws ParseException {
+ assertEquals(null, formatter.parse("", Locale.US));
+ }
+
+ @Test(expected = ParseException.class)
+ public void parseBogusValue() throws ParseException {
+ formatter.parse("bogus", Locale.US);
+ }
+
+ @Test(expected = ParseException.class)
+ public void parsePercentValueNotLenientFailure() throws ParseException {
+ formatter.parse("23.56bogus", Locale.US);
+ }
+
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java
new file mode 100644
index 00000000000..60934f88cbe
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/IntegerFormatterTests.java
@@ -0,0 +1,40 @@
+package org.springframework.ui.format.number;
+
+import static org.junit.Assert.assertEquals;
+
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.springframework.ui.format.number.IntegerFormatter;
+
+public class IntegerFormatterTests {
+
+ private IntegerFormatter formatter = new IntegerFormatter();
+
+ @Test
+ public void formatValue() {
+ assertEquals("23", formatter.format(23L, Locale.US));
+ }
+
+ @Test
+ public void parseValue() throws ParseException {
+ assertEquals((Long) 2356L, formatter.parse("2356", Locale.US));
+ }
+
+ @Test
+ public void parseEmptyValue() throws ParseException {
+ assertEquals(null, formatter.parse("", Locale.US));
+ }
+
+ @Test(expected = ParseException.class)
+ public void parseBogusValue() throws ParseException {
+ formatter.parse("bogus", Locale.US);
+ }
+
+ @Test(expected = ParseException.class)
+ public void parsePercentValueNotLenientFailure() throws ParseException {
+ formatter.parse("23.56", Locale.US);
+ }
+
+}
diff --git a/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java b/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java
new file mode 100644
index 00000000000..025767affe8
--- /dev/null
+++ b/org.springframework.context/src/test/java/org/springframework/ui/format/number/PercentFormatterTests.java
@@ -0,0 +1,42 @@
+package org.springframework.ui.format.number;
+
+import static org.junit.Assert.assertEquals;
+
+import java.math.BigDecimal;
+import java.text.ParseException;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.springframework.ui.format.number.PercentFormatter;
+
+public class PercentFormatterTests {
+
+ private PercentFormatter formatter = new PercentFormatter();
+
+ @Test
+ public void formatValue() {
+ assertEquals("23%", formatter.format(new BigDecimal(".23"), Locale.US));
+ }
+
+ @Test
+ public void parseValue() throws ParseException {
+ assertEquals(new BigDecimal(".2356"), formatter.parse("23.56%",
+ Locale.US));
+ }
+
+ @Test
+ public void parseEmptyValue() throws ParseException {
+ assertEquals(null, formatter.parse("", Locale.US));
+ }
+
+ @Test(expected = ParseException.class)
+ public void parseBogusValue() throws ParseException {
+ formatter.parse("bogus", Locale.US);
+ }
+
+ @Test(expected = ParseException.class)
+ public void parsePercentValueNotLenientFailure() throws ParseException {
+ formatter.parse("23.56%bogus", Locale.US);
+ }
+
+}