From 704cc79cee653e8aa0a7bde4594bd0f17de71646 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 19 Aug 2009 14:43:20 +0000 Subject: [PATCH] polish --- .../ui/format/FormatterRegistry.java | 16 ++-- .../ui/format/GenericFormatterRegistry.java | 80 ++++++++++++++----- .../ui/format/number/CurrencyFormatter.java | 11 +++ .../ui/format/number/DecimalFormatter.java | 42 +++++++--- .../ui/format/number/IntegerFormatter.java | 20 ++++- .../ui/format/number/PercentFormatter.java | 24 ++++-- .../format/GenericFormatterRegistryTests.java | 25 ++++-- 7 files changed, 160 insertions(+), 58 deletions(-) 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 index c39c1b8372..0c895e9112 100644 --- 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 @@ -27,7 +27,7 @@ import org.springframework.core.convert.TypeDescriptor; public interface FormatterRegistry { /** - * Adds a Formatter to this registry indexed by . + * Adds a Formatter to this registry indexed by <T>. * Calling getFormatter(<T>.class) returns formatter. * @param formatter the formatter * @param the type of object the formatter formats @@ -35,16 +35,16 @@ public interface FormatterRegistry { 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. + * Adds a Formatter to this registry indexed by type. + * Use this add method when type differs from <T>. + * Calling getFormatter(type) returns a decorator that wraps the targetFormatter. + * On format, the decorator first coerses the instance of tType 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 type the object type * @param targetFormatter the target formatter * @param the type of object the target formatter formats */ - void add(Class objectType, Formatter targetFormatter); + void add(Class type, Formatter targetFormatter); /** * Adds a AnnotationFormatterFactory that will format values of properties annotated with a specific annotation. @@ -53,7 +53,7 @@ public interface FormatterRegistry { void add(AnnotationFormatterFactory factory); /** - * Get the Formatter for the type. + * Get the Formatter for the type descriptor. * @return the Formatter, or null if none is registered */ @SuppressWarnings("unchecked") 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 index 6b060e5be8..8663233187 100644 --- 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 @@ -21,8 +21,10 @@ import java.lang.reflect.Type; import java.lang.reflect.TypeVariable; import java.text.ParseException; import java.util.HashMap; +import java.util.LinkedList; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import org.springframework.core.GenericTypeResolver; @@ -33,9 +35,10 @@ import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.util.Assert; /** - * A generic implementation of {@link FormatterRegistry} suitable for use in most binding environments. + * A generic implementation of {@link FormatterRegistry} suitable for use in most environments. * @author Keith Donald * @since 3.0 + * @see #setConversionService(ConversionService) * @see #add(Formatter) * @see #add(Class, Formatter) * @see #add(AnnotationFormatterFactory) @@ -50,13 +53,37 @@ public class GenericFormatterRegistry implements FormatterRegistry { private ConversionService conversionService = new DefaultConversionService(); /** - * Sets the type conversion service used to coerse objects to the types required for Formatting purposes. + * Sets the type conversion service that will be used to coerse objects to the types required for formatting. + * Defaults to a {@link DefaultConversionService}. * @param conversionService the conversion service * @see #add(Class, Formatter) */ public void setConversionService(ConversionService conversionService) { this.conversionService = conversionService; } + + /** + * Registers the formatters in the map provided by type. + * JavaBean-friendly alternative to calling {@link #add(Class, Formatter)}. + * @param formatters the formatters map + * @see #add(Class, Formatter) + */ + public void setFormatters(Map, Formatter> formatters) { + for (Map.Entry, Formatter> entry : formatters.entrySet()) { + add(entry.getKey(), entry.getValue()); + } + } + + /** + * Registers the annotation formatter factories in the set provided. + * JavaBean-friendly alternative to calling {@link #add(AnnotationFormatterFactory)}. + * @see #add(AnnotationFormatterFactory) + */ + public void setAnnotationFormatterFactories(Set factories) { + for (AnnotationFormatterFactory factory : factories) { + add(factory); + } + } // implementing FormatterRegistry @@ -64,12 +91,15 @@ public class GenericFormatterRegistry implements FormatterRegistry { typeFormatters.put(getFormattedObjectType(formatter.getClass()), formatter); } - public void add(Class objectType, Formatter formatter) { - if (objectType.isAnnotation()) { - annotationFormatters.put(objectType, new SimpleAnnotationFormatterFactory(formatter)); - } else { - typeFormatters.put(objectType, formatter); + public void add(Class type, Formatter formatter) { + Class formattedObjectType = getFormattedObjectType(formatter.getClass()); + if (!conversionService.canConvert(formattedObjectType, type)) { + throw new IllegalArgumentException("Unable to register formatter " + formatter + " for type [" + type.getName() + "]; not able to convert from [" + formattedObjectType.getName() + "] to parse"); } + if (!conversionService.canConvert(type, formattedObjectType)) { + throw new IllegalArgumentException("Unable to register formatter " + formatter + " for type [" + type.getName() + "]; not able to convert to [" + formattedObjectType.getName() + "] to format"); + } + typeFormatters.put(type, formatter); } public void add(AnnotationFormatterFactory factory) { @@ -158,7 +188,7 @@ public class GenericFormatterRegistry implements FormatterRegistry { private Formatter getTypeFormatter(Class type) { Assert.notNull(type, "The Class of the object to format is required"); - Formatter formatter = typeFormatters.get(type); + Formatter formatter = findFormatter(type); if (formatter != null) { Class formattedObjectType = getFormattedObjectType(formatter.getClass()); if (type.isAssignableFrom(formattedObjectType)) { @@ -170,6 +200,26 @@ public class GenericFormatterRegistry implements FormatterRegistry { return getDefaultFormatter(type); } } + + private Formatter findFormatter(Class type) { + LinkedList classQueue = new LinkedList(); + classQueue.addFirst(type); + while (!classQueue.isEmpty()) { + Class currentClass = classQueue.removeLast(); + Formatter formatter = typeFormatters.get(currentClass); + if (formatter != null) { + return formatter; + } + if (currentClass.getSuperclass() != null) { + classQueue.addFirst(currentClass.getSuperclass()); + } + Class[] interfaces = currentClass.getInterfaces(); + for (Class ifc : interfaces) { + classQueue.addFirst(ifc); + } + } + return null; + } private Formatter getDefaultFormatter(Class type) { Formatted formatted = AnnotationUtils.findAnnotation(type, Formatted.class); @@ -191,20 +241,6 @@ public class GenericFormatterRegistry implements FormatterRegistry { } } - private static class SimpleAnnotationFormatterFactory implements AnnotationFormatterFactory { - - private Formatter formatter; - - public SimpleAnnotationFormatterFactory(Formatter formatter) { - this.formatter = formatter; - } - - public Formatter getFormatter(Annotation annotation) { - return formatter; - } - - } - private class ConvertingFormatter implements Formatter { private Class type; 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 index c0604223cb..23807b7818 100644 --- 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 @@ -32,6 +32,7 @@ import org.springframework.ui.format.Formatter; * Applies {@link RoundingMode#DOWN} to parsed values. * @author Keith Donald * @since 3.0 + * @see #setLenient(boolean) */ public final class CurrencyFormatter implements Formatter { @@ -39,6 +40,16 @@ public final class CurrencyFormatter implements Formatter { private boolean lenient; + /** + * Specify whether or not parsing is to be lenient. + * With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + * Default is false. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + public String format(BigDecimal decimal, Locale locale) { if (decimal == null) { return ""; 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 index 736e4d0e2d..493b6017cf 100644 --- 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 @@ -16,6 +16,7 @@ package org.springframework.ui.format.number; import java.math.BigDecimal; +import java.text.DecimalFormat; import java.text.NumberFormat; import java.text.ParseException; import java.text.ParsePosition; @@ -24,14 +25,17 @@ import java.util.Locale; import org.springframework.ui.format.Formatter; /** - * A BigDecimal formatter for decimal values. + * A Number 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)}. + * Allows configuration over the decimal number pattern. + * The {@link #parse(String, Locale)} routine always returns a BigDecimal. * @author Keith Donald * @since 3.0 + * @see #setPattern(String) + * @see #setLenient(boolean) */ -public final class DecimalFormatter implements Formatter { +public final class DecimalFormatter implements Formatter { private DefaultNumberFormatFactory formatFactory = new DefaultNumberFormatFactory(); @@ -40,13 +44,28 @@ public final class DecimalFormatter implements Formatter { public DecimalFormatter() { initDefaults(); } - - public DecimalFormatter(String pattern) { - initDefaults(); + + /** + * 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) { formatFactory.setPattern(pattern); } - - public String format(BigDecimal decimal, Locale locale) { + + /** + * Specify whether or not parsing is to be lenient. + * With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + * Default is false. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + public String format(Number decimal, Locale locale) { if (decimal == null) { return ""; } @@ -54,8 +73,7 @@ public final class DecimalFormatter implements Formatter { return format.format(decimal); } - public BigDecimal parse(String formatted, Locale locale) - throws ParseException { + public Number parse(String formatted, Locale locale) throws ParseException { if (formatted.length() == 0) { return null; } @@ -73,7 +91,9 @@ public final class DecimalFormatter implements Formatter { } return decimal; } - + + // internal helpers + private void initDefaults() { formatFactory.setParseBigDecimal(true); } 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 index 7825997aaa..1fc85b8e08 100644 --- 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 @@ -23,18 +23,30 @@ import java.util.Locale; import org.springframework.ui.format.Formatter; /** - * A Long formatter for whole integer values. + * 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 * @since 3.0 + * @see #setLenient(boolean) */ -public final class IntegerFormatter implements Formatter { +public final class IntegerFormatter implements Formatter { private IntegerNumberFormatFactory formatFactory = new IntegerNumberFormatFactory(); private boolean lenient; - public String format(Long integer, Locale locale) { + /** + * Specify whether or not parsing is to be lenient. + * With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + * Default is false. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + public String format(Number integer, Locale locale) { if (integer == null) { return ""; } @@ -42,7 +54,7 @@ public final class IntegerFormatter implements Formatter { return format.format(integer); } - public Long parse(String formatted, Locale locale) + public Number parse(String formatted, Locale locale) throws ParseException { if (formatted.length() == 0) { return null; 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 index fee2195b55..7189285309 100644 --- 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 @@ -24,27 +24,39 @@ import java.util.Locale; import org.springframework.ui.format.Formatter; /** - * A BigDecimal formatter for percent values. + * A Number formatter for percent values. * Delegates to {@link NumberFormat#getPercentInstance(Locale)}. * Configures BigDecimal parsing so there is no loss in precision. + * The {@link #parse(String, Locale)} routine always returns a BigDecimal. * @author Keith Donald * @since 3.0 + * @see #setLenient(boolean) */ -public final class PercentFormatter implements Formatter { +public final class PercentFormatter implements Formatter { private PercentNumberFormatFactory percentFormatFactory = new PercentNumberFormatFactory(); private boolean lenient; - public String format(BigDecimal decimal, Locale locale) { - if (decimal == null) { + /** + * Specify whether or not parsing is to be lenient. + * With lenient parsing, the parser may allow inputs that do not precisely match the format. + * With strict parsing, inputs must match the format exactly. + * Default is false. + */ + public void setLenient(boolean lenient) { + this.lenient = lenient; + } + + public String format(Number number, Locale locale) { + if (number == null) { return ""; } NumberFormat format = percentFormatFactory.getNumberFormat(locale); - return format.format(decimal); + return format.format(number); } - public BigDecimal parse(String formatted, Locale locale) + public Number parse(String formatted, Locale locale) throws ParseException { if (formatted.length() == 0) { return null; 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 index 266923c85c..210bb37be9 100644 --- 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 @@ -4,6 +4,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import java.math.BigDecimal; +import java.math.BigInteger; import java.text.ParseException; import java.util.Locale; @@ -25,18 +26,20 @@ public class GenericFormatterRegistryTests { } @Test - public void testAdd() { + public void testAdd() throws ParseException { registry.add(new IntegerFormatter()); - Formatter formatter = registry.getFormatter(typeDescriptor(Long.class)); - String formatted = formatter.format(new Long(3), Locale.US); + Formatter formatter = registry.getFormatter(typeDescriptor(Integer.class)); + String formatted = formatter.format(new Integer(3), Locale.US); assertEquals("3", formatted); + Integer i = (Integer) formatter.parse("3", Locale.US); + assertEquals(new Integer(3), i); } @Test public void testAddByObjectType() { - registry.add(Integer.class, new IntegerFormatter()); - Formatter formatter = registry.getFormatter(typeDescriptor(Integer.class)); - String formatted = formatter.format(new Integer(3), Locale.US); + registry.add(BigInteger.class, new IntegerFormatter()); + Formatter formatter = registry.getFormatter(typeDescriptor(BigInteger.class)); + String formatted = formatter.format(new BigInteger("3"), Locale.US); assertEquals("3", formatted); } @@ -64,6 +67,11 @@ public class GenericFormatterRegistryTests { public void testGetNoFormatterForType() { assertNull(registry.getFormatter(typeDescriptor(Integer.class))); } + + @Test(expected=IllegalArgumentException.class) + public void testGetFormatterCannotConvert() { + registry.add(Integer.class, new AddressFormatter()); + } @CurrencyFormat public BigDecimal currencyField; @@ -74,8 +82,11 @@ public class GenericFormatterRegistryTests { public static class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory { + + private CurrencyFormatter currencyFormatter = new CurrencyFormatter(); + public Formatter getFormatter(CurrencyFormat annotation) { - return new CurrencyFormatter(); + return currencyFormatter; } }