From 02da2e85ee0e58202b90d11c126e6050abe184b4 Mon Sep 17 00:00:00 2001 From: Juergen Hoeller Date: Tue, 12 May 2015 22:33:18 +0200 Subject: [PATCH] DataBinder allows for adding custom Formatters as alternative to PropertyEditors (including per-field formatters) Includes a generic FormatterPropertyEditorAdapter plus Number conversion support in TypeConverterDelegate. Issue: SPR-7773 Issue: SPR-6069 --- .../beans/TypeConverterDelegate.java | 11 +- .../FormatterPropertyEditorAdapter.java | 76 +++++ .../support/FormattingConversionService.java | 53 ++-- .../validation/DataBinder.java | 63 ++++- .../validation/DataBinderTests.java | 265 ++++++++++++++++-- 5 files changed, 427 insertions(+), 41 deletions(-) create mode 100644 spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index 513393bd7c..ff4cfa681a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -34,6 +34,7 @@ import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.util.ClassUtils; +import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; /** @@ -204,7 +205,7 @@ class TypeConverterDelegate { if (Object.class.equals(requiredType)) { return (T) convertedValue; } - if (requiredType.isArray()) { + else if (requiredType.isArray()) { // Array required -> apply appropriate conversion of elements. if (convertedValue instanceof String && Enum.class.isAssignableFrom(requiredType.getComponentType())) { convertedValue = StringUtils.commaDelimitedListToStringArray((String) convertedValue); @@ -257,6 +258,11 @@ class TypeConverterDelegate { convertedValue = attemptToConvertStringToEnum(requiredType, trimmedValue, convertedValue); standardConversion = true; } + else if (convertedValue instanceof Number && Number.class.isAssignableFrom(requiredType)) { + convertedValue = NumberUtils.convertNumberToTargetClass( + (Number) convertedValue, (Class) requiredType); + standardConversion = true; + } } else { // convertedValue == null @@ -339,7 +345,6 @@ class TypeConverterDelegate { catch (Throwable ex) { if (logger.isTraceEnabled()) { logger.trace("Field [" + convertedValue + "] isn't an enum value", ex); - } } } diff --git a/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java b/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java new file mode 100644 index 0000000000..c5db9ecdd2 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/format/support/FormatterPropertyEditorAdapter.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2015 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.support; + +import java.beans.PropertyEditor; +import java.beans.PropertyEditorSupport; +import java.text.ParseException; + +import org.springframework.context.i18n.LocaleContextHolder; +import org.springframework.format.Formatter; +import org.springframework.util.Assert; + +/** + * Adapter that bridges between {@link Formatter} and {@link PropertyEditor}. + * + * @author Juergen Hoeller + * @since 4.2 + */ +public class FormatterPropertyEditorAdapter extends PropertyEditorSupport { + + private final Formatter formatter; + + + /** + * Create a new {@code FormatterPropertyEditorAdapter} for the given {@link Formatter}. + * @param formatter the {@link Formatter} to wrap + */ + @SuppressWarnings("unchecked") + public FormatterPropertyEditorAdapter(Formatter formatter) { + Assert.notNull(formatter, "Formatter must not be null"); + this.formatter = (Formatter) formatter; + } + + + /** + * Determine the {@link Formatter}-declared field type. + * @return the field type declared in the wrapped {@link Formatter} implementation + * (never {@code null}) + * @throws IllegalArgumentException if the {@link Formatter}-declared field type + * cannot be inferred + */ + public Class getFieldType() { + return FormattingConversionService.getFieldType(this.formatter); + } + + + @Override + public void setAsText(String text) throws IllegalArgumentException { + try { + setValue(this.formatter.parse(text, LocaleContextHolder.getLocale())); + } + catch (ParseException ex) { + throw new IllegalArgumentException("Parse attempt failed for value [" + text + "]", ex); + } + } + + @Override + public String getAsText() { + return this.formatter.print(getValue(), LocaleContextHolder.getLocale()); + } + +} diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java index 3ddf7e6143..276f87066f 100644 --- a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -67,12 +67,7 @@ public class FormattingConversionService extends GenericConversionService @Override public void addFormatter(Formatter formatter) { - Class fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); - if (fieldType == null) { - throw new IllegalArgumentException("Unable to extract parameterized field type argument from Formatter [" + - formatter.getClass().getName() + "]; does the formatter parameterize the generic type?"); - } - addFormatterForFieldType(fieldType, formatter); + addFormatterForFieldType(getFieldType(formatter), formatter); } @Override @@ -88,14 +83,8 @@ public class FormattingConversionService extends GenericConversionService } @Override - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory) { - Class annotationType = (Class) - GenericTypeResolver.resolveTypeArgument(annotationFormatterFactory.getClass(), AnnotationFormatterFactory.class); - if (annotationType == null) { - throw new IllegalArgumentException("Unable to extract parameterized Annotation type argument from AnnotationFormatterFactory [" + - annotationFormatterFactory.getClass().getName() + "]; does the factory parameterize the generic type?"); - } + public void addFormatterForFieldAnnotation(AnnotationFormatterFactory annotationFormatterFactory) { + Class annotationType = getAnnotationType(annotationFormatterFactory); if (this.embeddedValueResolver != null && annotationFormatterFactory instanceof EmbeddedValueResolverAware) { ((EmbeddedValueResolverAware) annotationFormatterFactory).setEmbeddedValueResolver(this.embeddedValueResolver); } @@ -107,6 +96,28 @@ public class FormattingConversionService extends GenericConversionService } + static Class getFieldType(Formatter formatter) { + Class fieldType = GenericTypeResolver.resolveTypeArgument(formatter.getClass(), Formatter.class); + if (fieldType == null) { + throw new IllegalArgumentException("Unable to extract parameterized field type argument from Formatter [" + + formatter.getClass().getName() + "]; does the formatter parameterize the generic type?"); + } + return fieldType; + } + + @SuppressWarnings("unchecked") + static Class getAnnotationType(AnnotationFormatterFactory factory) { + Class annotationType = (Class) + GenericTypeResolver.resolveTypeArgument(factory.getClass(), AnnotationFormatterFactory.class); + if (annotationType == null) { + throw new IllegalArgumentException("Unable to extract parameterized Annotation type argument from " + + "AnnotationFormatterFactory [" + factory.getClass().getName() + + "]; does the factory parameterize the generic type?"); + } + return annotationType; + } + + private static class PrinterConverter implements GenericConverter { private final Class fieldType; @@ -148,7 +159,7 @@ public class FormattingConversionService extends GenericConversionService @Override public String toString() { - return this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer; + return (this.fieldType.getName() + " -> " + String.class.getName() + " : " + this.printer); } } @@ -197,7 +208,7 @@ public class FormattingConversionService extends GenericConversionService @Override public String toString() { - return String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser; + return (String.class.getName() + " -> " + this.fieldType.getName() + ": " + this.parser); } } @@ -249,8 +260,8 @@ public class FormattingConversionService extends GenericConversionService @Override public String toString() { - return "@" + this.annotationType.getName() + " " + this.fieldType.getName() + " -> " + - String.class.getName() + ": " + this.annotationFormatterFactory; + return ("@" + this.annotationType.getName() + " " + this.fieldType.getName() + " -> " + + String.class.getName() + ": " + this.annotationFormatterFactory); } } @@ -302,8 +313,8 @@ public class FormattingConversionService extends GenericConversionService @Override public String toString() { - return String.class.getName() + " -> @" + this.annotationType.getName() + " " + - this.fieldType.getName() + ": " + this.annotationFormatterFactory; + return (String.class.getName() + " -> @" + this.annotationType.getName() + " " + + this.fieldType.getName() + ": " + this.annotationFormatterFactory); } } diff --git a/spring-context/src/main/java/org/springframework/validation/DataBinder.java b/spring-context/src/main/java/org/springframework/validation/DataBinder.java index 6c19ce712f..b0aa5071bd 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -42,6 +42,8 @@ import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; +import org.springframework.format.Formatter; +import org.springframework.format.support.FormatterPropertyEditorAdapter; import org.springframework.lang.UsesJava8; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -553,6 +555,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return Collections.unmodifiableList(this.validators); } + //--------------------------------------------------------------------- // Implementation of PropertyEditorRegistry/TypeConverter interface //--------------------------------------------------------------------- @@ -576,6 +579,64 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { return this.conversionService; } + /** + * Add a custom formatter, applying it to all fields matching the + * {@link Formatter}-declared type. + *

Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add, generically declared for a specific type + * @since 4.2 + * @see #registerCustomEditor(Class, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); + } + + /** + * Add a custom formatter for the field type specified in {@link Formatter} class, + * applying it to the specified fields only, if any, or otherwise to all fields. + *

Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add, generically declared for a specific type + * @param fields the fields to apply the formatter to, or none if to be applied to all + * @since 4.2 + * @see #registerCustomEditor(Class, String, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter, String... fields) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + Class fieldType = adapter.getFieldType(); + if (ObjectUtils.isEmpty(fields)) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter); + } + else { + for (String field : fields) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, field, adapter); + } + } + } + + /** + * Add a custom formatter, applying it to the specified field types only, if any, + * or otherwise to all fields matching the {@link Formatter}-declared type. + *

Registers a corresponding {@link PropertyEditor} adapter underneath the covers. + * @param formatter the formatter to add (does not need to generically declare a + * field type if field types are explicitly specified as parameters) + * @param fieldTypes the field types to apply the formatter to, or none if to be + * derived from the given {@link Formatter} implementation class + * @since 4.2 + * @see #registerCustomEditor(Class, PropertyEditor) + */ + public void addCustomFormatter(Formatter formatter, Class... fieldTypes) { + FormatterPropertyEditorAdapter adapter = new FormatterPropertyEditorAdapter(formatter); + if (ObjectUtils.isEmpty(fieldTypes)) { + getPropertyEditorRegistry().registerCustomEditor(adapter.getFieldType(), adapter); + } + else { + for (Class fieldType : fieldTypes) { + getPropertyEditorRegistry().registerCustomEditor(fieldType, adapter); + } + } + } + @Override public void registerCustomEditor(Class requiredType, PropertyEditor propertyEditor) { getPropertyEditorRegistry().registerCustomEditor(requiredType, propertyEditor); diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 4e3a2b46b3..291f8582af 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -35,7 +35,7 @@ import java.util.Map; import java.util.Set; import java.util.TreeSet; -import junit.framework.TestCase; +import org.junit.Test; import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.MutablePropertyValues; @@ -59,13 +59,16 @@ import org.springframework.tests.sample.beans.TestBean; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import static org.junit.Assert.*; + /** * @author Rod Johnson * @author Juergen Hoeller * @author Rob Harrop */ -public class DataBinderTests extends TestCase { +public class DataBinderTests { + @Test public void testBindingNoErrors() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -99,7 +102,8 @@ public class DataBinderTests extends TestCase { assertTrue(!other.equals(binder.getBindingResult())); } - public void testedBindingWithDefaultConversionNoErrors() throws Exception { + @Test + public void testBindingWithDefaultConversionNoErrors() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); assertTrue(binder.isIgnoreUnknownFields()); @@ -114,7 +118,8 @@ public class DataBinderTests extends TestCase { assertTrue(rod.isJedi()); } - public void testedNestedBindingWithDefaultConversionNoErrors() throws Exception { + @Test + public void testNestedBindingWithDefaultConversionNoErrors() throws Exception { TestBean rod = new TestBean(new TestBean()); DataBinder binder = new DataBinder(rod, "person"); assertTrue(binder.isIgnoreUnknownFields()); @@ -129,6 +134,7 @@ public class DataBinderTests extends TestCase { assertTrue(((TestBean) rod.getSpouse()).isJedi()); } + @Test public void testBindingNoErrorsNotIgnoreUnknown() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -147,6 +153,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingNoErrorsWithInvalidField() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -163,6 +170,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingNoErrorsWithIgnoreInvalid() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -174,6 +182,7 @@ public class DataBinderTests extends TestCase { binder.bind(pvs); } + @Test public void testBindingWithErrors() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -235,6 +244,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingWithSystemFieldError() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -251,6 +261,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingWithErrorsAndCustomEditors() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -317,6 +328,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingWithCustomEditorOnObjectField() { BeanWithObjectProperty tb = new BeanWithObjectProperty(); DataBinder binder = new DataBinder(tb); @@ -327,6 +339,7 @@ public class DataBinderTests extends TestCase { assertEquals(new Integer(1), tb.getObject()); } + @Test public void testBindingWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -358,6 +371,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingErrorWithFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -380,12 +394,13 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingErrorWithStringFormatter() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); FormattingConversionService conversionService = new FormattingConversionService(); DefaultConversionService.addDefaultConverters(conversionService); - conversionService.addFormatterForFieldType(String.class, new Formatter() { + conversionService.addFormatter(new Formatter() { @Override public String parse(String text, Locale locale) throws ParseException { throw new ParseException(text, 0); @@ -404,6 +419,7 @@ public class DataBinderTests extends TestCase { assertEquals("test", binder.getBindingResult().getFieldValue("name")); } + @Test public void testBindingWithFormatterAgainstList() { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); @@ -425,6 +441,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingErrorWithFormatterAgainstList() { BeanWithIntegerList tb = new BeanWithIntegerList(); DataBinder binder = new DataBinder(tb); @@ -447,6 +464,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingWithFormatterAgainstFields() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -479,6 +497,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testBindingErrorWithFormatterAgainstFields() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb); @@ -502,6 +521,78 @@ public class DataBinderTests extends TestCase { } } + @Test + public void testBindingWithCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.addCustomFormatter(new NumberStyleFormatter(), Float.class); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1,2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertEquals(new Float(1.2), tb.getMyFloat()); + assertEquals("1,2", binder.getBindingResult().getFieldValue("myFloat")); + + PropertyEditor editor = binder.getBindingResult().findEditor("myFloat", Float.class); + assertNotNull(editor); + editor.setValue(new Float(1.4)); + assertEquals("1,4", editor.getAsText()); + + editor = binder.getBindingResult().findEditor("myFloat", null); + assertNotNull(editor); + editor.setAsText("1,6"); + assertTrue(((Number) editor.getValue()).floatValue() == 1.6f); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithCustomFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.addCustomFormatter(new NumberStyleFormatter()); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("myFloat", "1x2"); + + LocaleContextHolder.setLocale(Locale.GERMAN); + try { + binder.bind(pvs); + assertEquals(new Float(0.0), tb.getMyFloat()); + assertEquals("1x2", binder.getBindingResult().getFieldValue("myFloat")); + assertTrue(binder.getBindingResult().hasFieldErrors("myFloat")); + } + finally { + LocaleContextHolder.resetLocaleContext(); + } + } + + @Test + public void testBindingErrorWithCustomStringFormatter() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb); + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + throw new ParseException(text, 0); + } + @Override + public String print(String object, Locale locale) { + return object; + } + }); + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "test"); + + binder.bind(pvs); + assertTrue(binder.getBindingResult().hasFieldErrors("name")); + assertEquals("test", binder.getBindingResult().getFieldValue("name")); + } + + @Test public void testBindingWithAllowedFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -516,6 +607,7 @@ public class DataBinderTests extends TestCase { assertTrue("did not change age", rod.getAge() == 0); } + @Test public void testBindingWithDisallowedFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -533,6 +625,7 @@ public class DataBinderTests extends TestCase { assertEquals("age", disallowedFields[0]); } + @Test public void testBindingWithAllowedAndDisallowedFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -551,6 +644,7 @@ public class DataBinderTests extends TestCase { assertEquals("age", disallowedFields[0]); } + @Test public void testBindingWithOverlappingAllowedAndDisallowedFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -569,6 +663,7 @@ public class DataBinderTests extends TestCase { assertEquals("age", disallowedFields[0]); } + @Test public void testBindingWithAllowedFieldsUsingAsterisks() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -595,6 +690,7 @@ public class DataBinderTests extends TestCase { assertTrue("Same object", tb.equals(rod)); } + @Test public void testBindingWithAllowedAndDisallowedMapFields() throws Exception { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); @@ -622,6 +718,7 @@ public class DataBinderTests extends TestCase { /** * Tests for required field, both null, non-existing and empty strings. */ + @Test public void testBindingWithRequiredFields() throws Exception { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -652,6 +749,7 @@ public class DataBinderTests extends TestCase { assertEquals("", br.getFieldValue("spouse.name")); } + @Test public void testBindingWithRequiredMapFields() throws Exception { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -671,6 +769,7 @@ public class DataBinderTests extends TestCase { assertEquals("required", br.getFieldError("someMap[key4]").getCode()); } + @Test public void testBindingWithNestedObjectCreation() throws Exception { TestBean tb = new TestBean(); @@ -691,6 +790,32 @@ public class DataBinderTests extends TestCase { assertEquals("test", tb.getSpouse().getName()); } + @Test + public void testCustomEditorWithOldValueAccess() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.registerCustomEditor(String.class, null, new PropertyEditorSupport() { + @Override + public void setAsText(String text) throws IllegalArgumentException { + if (getValue() == null || !text.equalsIgnoreCase(getValue().toString())) { + setValue(text); + } + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + binder.bind(pvs); + assertEquals("value", tb.getName()); + + pvs = new MutablePropertyValues(); + pvs.add("name", "vaLue"); + binder.bind(pvs); + assertEquals("value", tb.getName()); + } + + @Test public void testCustomEditorForSingleProperty() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -730,6 +855,7 @@ public class DataBinderTests extends TestCase { assertEquals("spouse.name", binder.getBindingResult().getFieldError("spouse.*").getField()); } + @Test public void testCustomEditorForPrimitiveProperty() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -739,6 +865,7 @@ public class DataBinderTests extends TestCase { public void setAsText(String text) throws IllegalArgumentException { setValue(new Integer(99)); } + @Override public String getAsText() { return "argh"; @@ -753,6 +880,7 @@ public class DataBinderTests extends TestCase { assertEquals(99, tb.getAge()); } + @Test public void testCustomEditorForAllStringProperties() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -762,6 +890,7 @@ public class DataBinderTests extends TestCase { public void setAsText(String text) throws IllegalArgumentException { setValue("prefix" + text); } + @Override public String getAsText() { return ((String) getValue()).substring(6); @@ -784,30 +913,104 @@ public class DataBinderTests extends TestCase { assertEquals("prefixvalue", tb.getTouchy()); } - public void testCustomEditorWithOldValueAccess() { + @Test + public void testCustomFormatterForSingleProperty() { TestBean tb = new TestBean(); + tb.setSpouse(new TestBean()); DataBinder binder = new DataBinder(tb, "tb"); - binder.registerCustomEditor(String.class, null, new PropertyEditorSupport() { + binder.addCustomFormatter(new Formatter() { @Override - public void setAsText(String text) throws IllegalArgumentException { - if (getValue() == null || !text.equalsIgnoreCase(getValue().toString())) { - setValue(text); - } + public String parse(String text, Locale locale) throws ParseException { + return "prefix" + text; } - }); + @Override + public String print(String object, Locale locale) { + return object.substring(6); + } + }, "name"); MutablePropertyValues pvs = new MutablePropertyValues(); pvs.add("name", "value"); + pvs.add("touchy", "value"); + pvs.add("spouse.name", "sue"); binder.bind(pvs); - assertEquals("value", tb.getName()); - pvs = new MutablePropertyValues(); - pvs.add("name", "vaLue"); + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("spouse.name", "someCode", "someMessage"); + + assertEquals("", binder.getBindingResult().getNestedPath()); + assertEquals("value", binder.getBindingResult().getFieldValue("name")); + assertEquals("prefixvalue", binder.getBindingResult().getFieldError("name").getRejectedValue()); + assertEquals("prefixvalue", tb.getName()); + assertEquals("value", binder.getBindingResult().getFieldValue("touchy")); + assertEquals("value", binder.getBindingResult().getFieldError("touchy").getRejectedValue()); + assertEquals("value", tb.getTouchy()); + + assertTrue(binder.getBindingResult().hasFieldErrors("spouse.*")); + assertEquals(1, binder.getBindingResult().getFieldErrorCount("spouse.*")); + assertEquals("spouse.name", binder.getBindingResult().getFieldError("spouse.*").getField()); + } + + @Test + public void testCustomFormatterForPrimitiveProperty() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.addCustomFormatter(new Formatter() { + @Override + public Integer parse(String text, Locale locale) throws ParseException { + return 99; + } + + @Override + public String print(Integer object, Locale locale) { + return "argh"; + } + }, "age"); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("age", ""); binder.bind(pvs); - assertEquals("value", tb.getName()); + + assertEquals("argh", binder.getBindingResult().getFieldValue("age")); + assertEquals(99, tb.getAge()); + } + + @Test + public void testCustomFormatterForAllStringProperties() { + TestBean tb = new TestBean(); + DataBinder binder = new DataBinder(tb, "tb"); + + binder.addCustomFormatter(new Formatter() { + @Override + public String parse(String text, Locale locale) throws ParseException { + return "prefix" + text; + } + @Override + public String print(String object, Locale locale) { + return object.substring(6); + } + }); + + MutablePropertyValues pvs = new MutablePropertyValues(); + pvs.add("name", "value"); + pvs.add("touchy", "value"); + binder.bind(pvs); + + binder.getBindingResult().rejectValue("name", "someCode", "someMessage"); + binder.getBindingResult().rejectValue("touchy", "someCode", "someMessage"); + + assertEquals("value", binder.getBindingResult().getFieldValue("name")); + assertEquals("prefixvalue", binder.getBindingResult().getFieldError("name").getRejectedValue()); + assertEquals("prefixvalue", tb.getName()); + assertEquals("value", binder.getBindingResult().getFieldValue("touchy")); + assertEquals("prefixvalue", binder.getBindingResult().getFieldError("touchy").getRejectedValue()); + assertEquals("prefixvalue", tb.getTouchy()); } + @Test public void testJavaBeanPropertyConventions() { Book book = new Book(); DataBinder binder = new DataBinder(book); @@ -831,6 +1034,7 @@ public class DataBinderTests extends TestCase { assertEquals(0, book.getNInStock()); } + @Test public void testValidatorNoErrors() { TestBean tb = new TestBean(); tb.setAge(33); @@ -894,6 +1098,7 @@ public class DataBinderTests extends TestCase { assertTrue(!errors.hasFieldErrors("name")); } + @Test public void testValidatorWithErrors() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -962,6 +1167,7 @@ public class DataBinderTests extends TestCase { assertEquals(new Integer(0), (errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()); } + @Test public void testValidatorWithErrorsAndCodesPrefix() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -1033,6 +1239,7 @@ public class DataBinderTests extends TestCase { assertEquals(new Integer(0), (errors.getFieldErrors("spouse.age").get(0)).getRejectedValue()); } + @Test public void testValidatorWithNestedObjectNull() { TestBean tb = new TestBean(); Errors errors = new BeanPropertyBindingResult(tb, "tb"); @@ -1051,6 +1258,7 @@ public class DataBinderTests extends TestCase { assertEquals(null, (errors.getFieldErrors("spouse").get(0)).getRejectedValue()); } + @Test public void testNestedValidatorWithoutNestedPath() { TestBean tb = new TestBean(); tb.setName("XXX"); @@ -1064,6 +1272,7 @@ public class DataBinderTests extends TestCase { assertEquals("tb", (errors.getGlobalErrors().get(0)).getObjectName()); } + @Test public void testBindingStringArrayToIntegerSet() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1091,6 +1300,7 @@ public class DataBinderTests extends TestCase { assertNull(tb.getSet()); } + @Test public void testBindingNullToEmptyCollection() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1103,6 +1313,7 @@ public class DataBinderTests extends TestCase { assertTrue(tb.getSet().isEmpty()); } + @Test public void testBindingToIndexedField() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1141,6 +1352,7 @@ public class DataBinderTests extends TestCase { assertEquals("NOT_ROD", errors.getFieldError("map[key1].name").getCodes()[6]); } + @Test public void testBindingToNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); @@ -1178,6 +1390,7 @@ public class DataBinderTests extends TestCase { assertEquals("NOT_ROD", errors.getFieldError("array[0].nestedIndexedBean.list[0].name").getCodes()[8]); } + @Test public void testEditorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); @@ -1203,6 +1416,7 @@ public class DataBinderTests extends TestCase { assertEquals("test2", binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")); } + @Test public void testSpecificEditorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); @@ -1228,6 +1442,7 @@ public class DataBinderTests extends TestCase { assertEquals("test2", binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")); } + @Test public void testInnerSpecificEditorForNestedIndexedField() { IndexedTestBean tb = new IndexedTestBean(); tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean()); @@ -1253,6 +1468,7 @@ public class DataBinderTests extends TestCase { assertEquals("test2", binder.getBindingResult().getFieldValue("array[1].nestedIndexedBean.list[1].name")); } + @Test public void testDirectBindingToIndexedField() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1305,6 +1521,7 @@ public class DataBinderTests extends TestCase { assertEquals("NOT_NULL", errors.getFieldError("map[key0]").getCodes()[4]); } + @Test public void testDirectBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1335,6 +1552,7 @@ public class DataBinderTests extends TestCase { assertEquals("NOT_NULL", errors.getFieldError("map[key0]").getCodes()[5]); } + @Test public void testDirectBindingToEmptyIndexedFieldWithRegisteredGenericEditor() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1365,6 +1583,7 @@ public class DataBinderTests extends TestCase { assertEquals("NOT_NULL", errors.getFieldError("map[key0]").getCodes()[5]); } + @Test public void testCustomEditorWithSubclass() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1398,6 +1617,7 @@ public class DataBinderTests extends TestCase { assertEquals("arraya", errors.getFieldValue("array[0]")); } + @Test public void testBindToStringArrayWithArrayEditor() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1416,6 +1636,7 @@ public class DataBinderTests extends TestCase { assertEquals("b2", tb.getStringArray()[1]); } + @Test public void testBindToStringArrayWithComponentEditor() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1434,6 +1655,7 @@ public class DataBinderTests extends TestCase { assertEquals("Xb2", tb.getStringArray()[1]); } + @Test public void testBindingErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -1460,6 +1682,7 @@ public class DataBinderTests extends TestCase { assertEquals("Field Person Age did not have correct type", msg); } + @Test public void testAddAllErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); @@ -1478,6 +1701,7 @@ public class DataBinderTests extends TestCase { assertEquals("badName", nameError.getCode()); } + @Test public void testBindingWithResortedList() { IndexedTestBean tb = new IndexedTestBean(); DataBinder binder = new DataBinder(tb, "tb"); @@ -1495,6 +1719,7 @@ public class DataBinderTests extends TestCase { assertEquals(tb1.getName(), binder.getBindingResult().getFieldValue("list[1].name")); } + @Test public void testRejectWithoutDefaultMessage() throws Exception { TestBean tb = new TestBean(); tb.setName("myName"); @@ -1512,6 +1737,7 @@ public class DataBinderTests extends TestCase { assertEquals("invalid field", ms.getMessage(ex.getFieldError("age"), Locale.US)); } + @Test public void testBindExceptionSerializable() throws Exception { SerializablePerson tb = new SerializablePerson(); tb.setName("myName"); @@ -1540,6 +1766,7 @@ public class DataBinderTests extends TestCase { assertEquals("myName", ex2.getFieldValue("name")); } + @Test public void testTrackDisallowedFields() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1559,6 +1786,7 @@ public class DataBinderTests extends TestCase { assertEquals("beanName", disallowedFields[0]); } + @Test public void testAutoGrowWithinDefaultLimit() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1570,6 +1798,7 @@ public class DataBinderTests extends TestCase { assertEquals(5, testBean.getFriends().size()); } + @Test public void testAutoGrowBeyondDefaultLimit() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1586,6 +1815,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testAutoGrowWithinCustomLimit() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1598,6 +1828,7 @@ public class DataBinderTests extends TestCase { assertEquals(5, testBean.getFriends().size()); } + @Test public void testAutoGrowBeyondCustomLimit() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1615,6 +1846,7 @@ public class DataBinderTests extends TestCase { } } + @Test public void testNestedGrowingList() { Form form = new Form(); DataBinder binder = new DataBinder(form, "form"); @@ -1630,6 +1862,7 @@ public class DataBinderTests extends TestCase { assertEquals(2, list.size()); } + @Test public void testFieldErrorAccessVariations() throws Exception { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean");