initial JSR-303 Bean Validation support; revised ConversionService and FormatterRegistry

This commit is contained in:
Juergen Hoeller
2009-09-07 23:58:42 +00:00
parent f9f9b431a6
commit a86a698e5b
72 changed files with 1808 additions and 848 deletions

View File

@@ -1,9 +1,21 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.ui.format;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@@ -13,14 +25,20 @@ import java.math.BigInteger;
import java.text.ParseException;
import java.util.Locale;
import static org.junit.Assert.*;
import org.junit.Before;
import org.junit.Test;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.style.ToStringCreator;
import org.springframework.ui.format.number.CurrencyFormatter;
import org.springframework.ui.format.number.IntegerFormatter;
import org.springframework.ui.format.support.GenericFormatterRegistry;
/**
* @author Keith Donald
* @author Juergen Hoeller
*/
public class GenericFormatterRegistryTests {
private GenericFormatterRegistry registry;
@@ -32,8 +50,8 @@ public class GenericFormatterRegistryTests {
@Test
public void testAdd() throws ParseException {
registry.add(new IntegerFormatter());
Formatter formatter = registry.getFormatter(typeDescriptor(Integer.class));
registry.addFormatterByType(new IntegerFormatter());
Formatter formatter = registry.getFormatter(Integer.class);
String formatted = formatter.format(new Integer(3), Locale.US);
assertEquals("3", formatted);
Integer i = (Integer) formatter.parse("3", Locale.US);
@@ -42,23 +60,38 @@ public class GenericFormatterRegistryTests {
@Test
public void testAddByObjectType() {
registry.add(BigInteger.class, new IntegerFormatter());
Formatter formatter = registry.getFormatter(typeDescriptor(BigInteger.class));
registry.addFormatterByType(BigInteger.class, new IntegerFormatter());
Formatter formatter = registry.getFormatter(BigInteger.class);
String formatted = formatter.format(new BigInteger("3"), Locale.US);
assertEquals("3", formatted);
}
@Test
public void testAddAnnotationFormatterFactory() throws Exception {
registry.add(new CurrencyAnnotationFormatterFactory());
public void testAddByAnnotation() throws Exception {
registry.addFormatterByAnnotation(Currency.class, new CurrencyFormatter());
Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("currencyField")));
String formatted = formatter.format(new BigDecimal("5.00"), Locale.US);
assertEquals("$5.00", formatted);
}
@Test
public void testAddAnnotationFormatterFactory() throws Exception {
registry.addFormatterByAnnotation(new CurrencyAnnotationFormatterFactory());
Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("currencyField")));
String formatted = formatter.format(new BigDecimal("5.00"), Locale.US);
assertEquals("$5.00", formatted);
}
@Test
public void testGetDefaultFormatterFromMetaAnnotation() throws Exception {
Formatter formatter = registry.getFormatter(new TypeDescriptor(getClass().getField("smartCurrencyField")));
String formatted = formatter.format(new BigDecimal("5.00"), Locale.US);
assertEquals("$5.00", formatted);
}
@Test
public void testGetDefaultFormatterForType() {
Formatter formatter = registry.getFormatter(typeDescriptor(Address.class));
Formatter formatter = registry.getFormatter(Address.class);
Address address = new Address();
address.street = "12345 Bel Aire Estates";
address.city = "Palm Bay";
@@ -70,33 +103,44 @@ public class GenericFormatterRegistryTests {
@Test
public void testGetNoFormatterForType() {
assertNull(registry.getFormatter(typeDescriptor(Integer.class)));
assertNull(registry.getFormatter(Integer.class));
}
@Test(expected=IllegalArgumentException.class)
public void testGetFormatterCannotConvert() {
registry.add(Integer.class, new AddressFormatter());
registry.addFormatterByType(Integer.class, new AddressFormatter());
}
@Currency
public BigDecimal currencyField;
private static TypeDescriptor typeDescriptor(Class<?> clazz) {
return TypeDescriptor.valueOf(clazz);
@SmartCurrency
public BigDecimal smartCurrencyField;
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Currency {
}
public static class CurrencyAnnotationFormatterFactory implements
AnnotationFormatterFactory<Currency, BigDecimal> {
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Formatted(CurrencyFormatter.class)
public @interface SmartCurrency {
}
public static class CurrencyAnnotationFormatterFactory implements AnnotationFormatterFactory<Currency, Number> {
private CurrencyFormatter currencyFormatter = new CurrencyFormatter();
private final CurrencyFormatter currencyFormatter = new CurrencyFormatter();
public Formatter<BigDecimal> getFormatter(Currency annotation) {
return currencyFormatter;
public Formatter<Number> getFormatter(Currency annotation) {
return this.currencyFormatter;
}
}
@Formatted(AddressFormatter.class)
public static class Address {
private String street;
private String city;
private String state;
@@ -148,7 +192,7 @@ public class GenericFormatterRegistryTests {
.append("zip", zip).toString();
}
}
public static class AddressFormatter implements Formatter<Address> {
public String format(Address address, Locale locale) {
@@ -164,14 +208,6 @@ public class GenericFormatterRegistryTests {
address.setZip(fields[3]);
return address;
}
}
@Target( { ElementType.METHOD, ElementType.FIELD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Currency {
}
}
}

View File

@@ -26,6 +26,11 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Retention;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
import java.math.BigDecimal;
import junit.framework.TestCase;
@@ -44,6 +49,8 @@ import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.ui.format.number.DecimalFormatter;
import org.springframework.ui.format.Formatted;
import org.springframework.ui.format.support.GenericFormatterRegistry;
import org.springframework.util.StringUtils;
/**
@@ -297,7 +304,7 @@ public class DataBinderTests extends TestCase {
public void testBindingWithFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.getFormatterRegistry().add(Float.class, new DecimalFormatter());
binder.getFormatterRegistry().addFormatterByType(Float.class, new DecimalFormatter());
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("myFloat", "1,2");
@@ -322,6 +329,45 @@ public class DataBinderTests extends TestCase {
}
}
public void testBindingWithDefaultFormatterFromField() {
doTestBindingWithDefaultFormatter(new FormattedFieldTestBean());
}
public void testBindingWithDefaultFormatterFromGetter() {
doTestBindingWithDefaultFormatter(new FormattedGetterTestBean());
}
public void testBindingWithDefaultFormatterFromSetter() {
doTestBindingWithDefaultFormatter(new FormattedSetterTestBean());
}
private void doTestBindingWithDefaultFormatter(Object tb) {
DataBinder binder = new DataBinder(tb);
binder.setFormatterRegistry(new GenericFormatterRegistry());
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.addPropertyValue("number", "1,2");
LocaleContextHolder.setLocale(Locale.GERMAN);
try {
binder.bind(pvs);
assertEquals(new Float("1.2"), binder.getBindingResult().getRawFieldValue("number"));
assertEquals("1,2", binder.getBindingResult().getFieldValue("number"));
PropertyEditor editor = binder.getBindingResult().findEditor("number", Float.class);
assertNotNull(editor);
editor.setValue(new Float("1.4"));
assertEquals("1,4", editor.getAsText());
editor = binder.getBindingResult().findEditor("number", null);
assertNotNull(editor);
editor.setAsText("1,6");
assertEquals(new Float("1.6"), editor.getValue());
}
finally {
LocaleContextHolder.resetLocaleContext();
}
}
public void testBindingWithAllowedFields() throws Exception {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
@@ -1376,4 +1422,56 @@ public class DataBinderTests extends TestCase {
}
}
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Formatted(DecimalFormatter.class)
public @interface Decimal {
}
private static class FormattedFieldTestBean {
@Decimal
private Float number;
public Float getNumber() {
return number;
}
public void setNumber(Float number) {
this.number = number;
}
}
private static class FormattedGetterTestBean {
private Float number;
@Decimal
public Float getNumber() {
return number;
}
public void setNumber(Float number) {
this.number = number;
}
}
private static class FormattedSetterTestBean {
private Float number;
public Float getNumber() {
return number;
}
@Decimal
public void setNumber(Float number) {
this.number = number;
}
}
}

View File

@@ -0,0 +1,199 @@
/*
* Copyright 2002-2009 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.validation.beanvalidation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
import java.util.Iterator;
import java.util.Set;
import javax.validation.Constraint;
import javax.validation.ConstraintPayload;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import javax.validation.ConstraintViolation;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import org.hibernate.validation.HibernateValidationProvider;
import static org.junit.Assert.*;
import org.junit.Test;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
/**
* @author Juergen Hoeller
* @since 3.0
*/
public class ValidatorFactoryTests {
@Test
public void testSimpleValidation() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
ValidPerson person = new ValidPerson();
Set<ConstraintViolation<ValidPerson>> result = validator.validate(person);
assertEquals(2, result.size());
for (ConstraintViolation<ValidPerson> cv : result) {
String path = cv.getPropertyPath().toString();
if ("name".equals(path) || "address.street".equals(path)) {
assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NotNull);
}
else {
fail("Invalid constraint violation with path '" + path + "'");
}
}
}
@Test
public void testSimpleValidationWithCustomProvider() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setProviderClass(HibernateValidationProvider.class);
validator.afterPropertiesSet();
ValidPerson person = new ValidPerson();
Set<ConstraintViolation<ValidPerson>> result = validator.validate(person);
assertEquals(2, result.size());
for (ConstraintViolation<ValidPerson> cv : result) {
String path = cv.getPropertyPath().toString();
if ("name".equals(path) || "address.street".equals(path)) {
assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NotNull);
}
else {
fail("Invalid constraint violation with path '" + path + "'");
}
}
}
@Test
public void testSimpleValidationWithClassLevel() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
ValidPerson person = new ValidPerson();
person.setName("Juergen");
person.getAddress().setStreet("Juergen's Street");
Set<ConstraintViolation<ValidPerson>> result = validator.validate(person);
assertEquals(1, result.size());
Iterator<ConstraintViolation<ValidPerson>> iterator = result.iterator();
ConstraintViolation cv = iterator.next();
assertEquals("", cv.getPropertyPath().toString());
assertTrue(cv.getConstraintDescriptor().getAnnotation() instanceof NameAddressValid);
}
@Test
public void testSpringValidation() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
ValidPerson person = new ValidPerson();
BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person");
validator.validate(person, result);
assertEquals(2, result.getErrorCount());
FieldError fieldError = result.getFieldError("name");
assertEquals("name", fieldError.getField());
System.out.println(Arrays.asList(fieldError.getCodes()));
System.out.println(fieldError.getDefaultMessage());
fieldError = result.getFieldError("address.street");
assertEquals("address.street", fieldError.getField());
System.out.println(Arrays.asList(fieldError.getCodes()));
System.out.println(fieldError.getDefaultMessage());
}
@Test
public void testSpringValidationWithClassLevel() throws Exception {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.afterPropertiesSet();
ValidPerson person = new ValidPerson();
person.setName("Juergen");
person.getAddress().setStreet("Juergen's Street");
BeanPropertyBindingResult result = new BeanPropertyBindingResult(person, "person");
validator.validate(person, result);
assertEquals(1, result.getErrorCount());
ObjectError fieldError = result.getGlobalError();
System.out.println(Arrays.asList(fieldError.getCodes()));
System.out.println(fieldError.getDefaultMessage());
}
@NameAddressValid
private static class ValidPerson {
@NotNull
private String name;
@Valid
private ValidAddress address = new ValidAddress();
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public ValidAddress getAddress() {
return address;
}
public void setAddress(ValidAddress address) {
this.address = address;
}
}
private static class ValidAddress {
@NotNull
private String street;
public String getStreet() {
return street;
}
public void setStreet(String street) {
this.street = street;
}
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = NameAddressValidator.class)
public @interface NameAddressValid {
String message() default "Street must not contain name";
Class<?>[] groups() default {};
Class<? extends ConstraintPayload>[] payload() default {};
}
public static class NameAddressValidator implements ConstraintValidator<NameAddressValid, ValidPerson> {
public void initialize(NameAddressValid constraintAnnotation) {
}
public boolean isValid(ValidPerson value, ConstraintValidatorContext constraintValidatorContext) {
return (value.name == null || !value.address.street.contains(value.name));
}
}
}