diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java index de823582d4d..75ebd3f7e1f 100644 --- a/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -59,9 +59,9 @@ public class DefaultBindingErrorProcessor implements BindingErrorProcessor { String fixedField = bindingResult.getNestedPath() + missingField; String[] codes = bindingResult.resolveMessageCodes(MISSING_FIELD_ERROR_CODE, missingField); Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), fixedField); - bindingResult.addError(new FieldError( - bindingResult.getObjectName(), fixedField, "", true, - codes, arguments, "Field '" + fixedField + "' is required")); + FieldError error = new FieldError(bindingResult.getObjectName(), fixedField, "", true, + codes, arguments, "Field '" + fixedField + "' is required"); + bindingResult.addError(error); } @Override @@ -72,12 +72,13 @@ public class DefaultBindingErrorProcessor implements BindingErrorProcessor { String[] codes = bindingResult.resolveMessageCodes(ex.getErrorCode(), field); Object[] arguments = getArgumentsForBindError(bindingResult.getObjectName(), field); Object rejectedValue = ex.getValue(); - if (rejectedValue != null && rejectedValue.getClass().isArray()) { + if (ObjectUtils.isArray(rejectedValue)) { rejectedValue = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(rejectedValue)); } - bindingResult.addError(new FieldError( - bindingResult.getObjectName(), field, rejectedValue, true, - codes, arguments, ex.getLocalizedMessage())); + FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true, + codes, arguments, ex.getLocalizedMessage()); + error.initSource(ex); + bindingResult.addError(error); } /** diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java index c72529a0f48..f99a28aa7bd 100644 --- a/spring-context/src/main/java/org/springframework/validation/FieldError.java +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -99,13 +99,6 @@ public class FieldError extends ObjectError { } - @Override - public String toString() { - return "Field error in object '" + getObjectName() + "' on field '" + this.field + - "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " + - resolvableToString(); - } - @Override public boolean equals(@Nullable Object other) { if (this == other) { @@ -129,4 +122,11 @@ public class FieldError extends ObjectError { return hashCode; } + @Override + public String toString() { + return "Field error in object '" + getObjectName() + "' on field '" + this.field + + "': rejected value [" + ObjectUtils.nullSafeToString(this.rejectedValue) + "]; " + + resolvableToString(); + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/ObjectError.java b/spring-context/src/main/java/org/springframework/validation/ObjectError.java index 9ad8eeee7e0..23cf35c3721 100644 --- a/spring-context/src/main/java/org/springframework/validation/ObjectError.java +++ b/spring-context/src/main/java/org/springframework/validation/ObjectError.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -37,6 +37,9 @@ public class ObjectError extends DefaultMessageSourceResolvable { private final String objectName; + @Nullable + private Object source; + /** * Create a new instance of the ObjectError class. @@ -70,12 +73,31 @@ public class ObjectError extends DefaultMessageSourceResolvable { return this.objectName; } + /** + * Initialize the source behind this error: possibly an {@link Exception} + * (typically {@link org.springframework.beans.PropertyAccessException}) + * or a Bean Validation {@link javax.validation.ConstraintViolation}. + * @param source the source object + * @since 5.0.4 + */ + public void initSource(Object source) { + Assert.state(this.source == null, "Source already initialized"); + this.source = source; + } - @Override - public String toString() { - return "Error in object '" + this.objectName + "': " + resolvableToString(); + /** + * Return the source behind this error: possibly an {@link Exception} + * (typically {@link org.springframework.beans.PropertyAccessException}) + * or a Bean Validation {@link javax.validation.ConstraintViolation}. + * @return the source object, or {@code null} if none + * @since 5.0.4 + */ + @Nullable + public Object getSource() { + return this.source; } + @Override public boolean equals(@Nullable Object other) { if (this == other) { @@ -93,4 +115,9 @@ public class ObjectError extends DefaultMessageSourceResolvable { return super.hashCode() * 29 + getObjectName().hashCode(); } + @Override + public String toString() { + return "Error in object '" + this.objectName + "': " + resolvableToString(); + } + } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index 6f6115ef8aa..48567792771 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -142,15 +142,18 @@ public class SpringValidatorAdapter implements SmartValidator, javax.validation. String nestedField = bindingResult.getNestedPath() + field; if ("".equals(nestedField)) { String[] errorCodes = bindingResult.resolveMessageCodes(errorCode); - bindingResult.addError(new ObjectError( - errors.getObjectName(), errorCodes, errorArgs, violation.getMessage())); + ObjectError error = new ObjectError( + errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()); + error.initSource(violation); + bindingResult.addError(error); } else { Object rejectedValue = getRejectedValue(field, violation, bindingResult); String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field); - bindingResult.addError(new FieldError( - errors.getObjectName(), nestedField, rejectedValue, false, - errorCodes, errorArgs, violation.getMessage())); + FieldError error = new FieldError(errors.getObjectName(), nestedField, + rejectedValue, false, errorCodes, errorArgs, violation.getMessage()); + error.initSource(violation); + bindingResult.addError(error); } } else { 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 57484158413..10ab8e30950 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2016 the original author or authors. + * Copyright 2002-2018 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,9 +42,11 @@ import org.junit.rules.ExpectedException; import org.springframework.beans.BeanWrapper; import org.springframework.beans.InvalidPropertyException; +import org.springframework.beans.MethodInvocationException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.NotWritablePropertyException; import org.springframework.beans.NullValueInNestedPathException; +import org.springframework.beans.TypeMismatchException; import org.springframework.beans.propertyeditors.CustomCollectionEditor; import org.springframework.beans.propertyeditors.CustomNumberEditor; import org.springframework.beans.propertyeditors.StringTrimmerEditor; @@ -78,11 +80,11 @@ import static org.junit.Assert.*; public class DataBinderTests { @Rule - public ExpectedException expectedException = ExpectedException.none(); + public final ExpectedException expectedException = ExpectedException.none(); @Test - public void testBindingNoErrors() throws Exception { + public void testBindingNoErrors() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); assertTrue(binder.isIgnoreUnknownFields()); @@ -116,7 +118,7 @@ public class DataBinderTests { } @Test - public void testBindingWithDefaultConversionNoErrors() throws Exception { + public void testBindingWithDefaultConversionNoErrors() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); assertTrue(binder.isIgnoreUnknownFields()); @@ -132,7 +134,7 @@ public class DataBinderTests { } @Test - public void testNestedBindingWithDefaultConversionNoErrors() throws Exception { + public void testNestedBindingWithDefaultConversionNoErrors() throws BindException { TestBean rod = new TestBean(new TestBean()); DataBinder binder = new DataBinder(rod, "person"); assertTrue(binder.isIgnoreUnknownFields()); @@ -148,7 +150,7 @@ public class DataBinderTests { } @Test - public void testBindingNoErrorsNotIgnoreUnknown() throws Exception { + public void testBindingNoErrorsNotIgnoreUnknown() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setIgnoreUnknownFields(false); @@ -167,7 +169,7 @@ public class DataBinderTests { } @Test - public void testBindingNoErrorsWithInvalidField() throws Exception { + public void testBindingNoErrorsWithInvalidField() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -184,7 +186,7 @@ public class DataBinderTests { } @Test - public void testBindingNoErrorsWithIgnoreInvalid() throws Exception { + public void testBindingNoErrorsWithIgnoreInvalid() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setIgnoreInvalidFields(true); @@ -196,7 +198,7 @@ public class DataBinderTests { } @Test - public void testBindingWithErrors() throws Exception { + public void testBindingWithErrors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -239,6 +241,7 @@ public class DataBinderTests { assertEquals("typeMismatch", binder.getBindingResult().getFieldError("age").getCode()); assertEquals("32x", binder.getBindingResult().getFieldValue("age")); assertEquals("32x", binder.getBindingResult().getFieldError("age").getRejectedValue()); + assertTrue(binder.getBindingResult().getFieldError("age").getSource() instanceof TypeMismatchException); assertEquals(0, tb.getAge()); assertTrue("Has touchy errors", br.hasFieldErrors("touchy")); @@ -246,6 +249,7 @@ public class DataBinderTests { assertEquals("methodInvocation", binder.getBindingResult().getFieldError("touchy").getCode()); assertEquals("m.y", binder.getBindingResult().getFieldValue("touchy")); assertEquals("m.y", binder.getBindingResult().getFieldError("touchy").getRejectedValue()); + assertTrue(binder.getBindingResult().getFieldError("touchy").getSource() instanceof MethodInvocationException); assertNull(tb.getTouchy()); rod = new TestBean(); @@ -260,7 +264,7 @@ public class DataBinderTests { } @Test - public void testBindingWithSystemFieldError() throws Exception { + public void testBindingWithSystemFieldError() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); MutablePropertyValues pvs = new MutablePropertyValues(); @@ -277,7 +281,7 @@ public class DataBinderTests { } @Test - public void testBindingWithErrorsAndCustomEditors() throws Exception { + public void testBindingWithErrorsAndCustomEditors() { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() { @@ -683,7 +687,7 @@ public class DataBinderTests { } @Test - public void testBindingWithAllowedFields() throws Exception { + public void testBindingWithAllowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "myparam"); @@ -698,7 +702,7 @@ public class DataBinderTests { } @Test - public void testBindingWithDisallowedFields() throws Exception { + public void testBindingWithDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setDisallowedFields("age"); @@ -716,7 +720,7 @@ public class DataBinderTests { } @Test - public void testBindingWithAllowedAndDisallowedFields() throws Exception { + public void testBindingWithAllowedAndDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "myparam"); @@ -735,7 +739,7 @@ public class DataBinderTests { } @Test - public void testBindingWithOverlappingAllowedAndDisallowedFields() throws Exception { + public void testBindingWithOverlappingAllowedAndDisallowedFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("name", "age"); @@ -754,7 +758,7 @@ public class DataBinderTests { } @Test - public void testBindingWithAllowedFieldsUsingAsterisks() throws Exception { + public void testBindingWithAllowedFieldsUsingAsterisks() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod, "person"); binder.setAllowedFields("nam*", "*ouchy"); @@ -781,7 +785,7 @@ public class DataBinderTests { } @Test - public void testBindingWithAllowedAndDisallowedMapFields() throws Exception { + public void testBindingWithAllowedAndDisallowedMapFields() throws BindException { TestBean rod = new TestBean(); DataBinder binder = new DataBinder(rod); binder.setAllowedFields("someMap[key1]", "someMap[key2]"); @@ -809,7 +813,7 @@ public class DataBinderTests { * Tests for required field, both null, non-existing and empty strings. */ @Test - public void testBindingWithRequiredFields() throws Exception { + public void testBindingWithRequiredFields() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -840,7 +844,7 @@ public class DataBinderTests { } @Test - public void testBindingWithRequiredMapFields() throws Exception { + public void testBindingWithRequiredMapFields() { TestBean tb = new TestBean(); tb.setSpouse(new TestBean()); @@ -860,7 +864,7 @@ public class DataBinderTests { } @Test - public void testBindingWithNestedObjectCreation() throws Exception { + public void testBindingWithNestedObjectCreation() { TestBean tb = new TestBean(); DataBinder binder = new DataBinder(tb, "person"); @@ -1828,7 +1832,7 @@ public class DataBinderTests { } @Test - public void testRejectWithoutDefaultMessage() throws Exception { + public void testRejectWithoutDefaultMessage() { TestBean tb = new TestBean(); tb.setName("myName"); tb.setAge(99); @@ -1875,7 +1879,7 @@ public class DataBinderTests { } @Test - public void testTrackDisallowedFields() throws Exception { + public void testTrackDisallowedFields() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAllowedFields("name", "age"); @@ -1895,7 +1899,7 @@ public class DataBinderTests { } @Test - public void testAutoGrowWithinDefaultLimit() throws Exception { + public void testAutoGrowWithinDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1907,7 +1911,7 @@ public class DataBinderTests { } @Test - public void testAutoGrowBeyondDefaultLimit() throws Exception { + public void testAutoGrowBeyondDefaultLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); @@ -1924,7 +1928,7 @@ public class DataBinderTests { } @Test - public void testAutoGrowWithinCustomLimit() throws Exception { + public void testAutoGrowWithinCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); @@ -1937,7 +1941,7 @@ public class DataBinderTests { } @Test - public void testAutoGrowBeyondCustomLimit() throws Exception { + public void testAutoGrowBeyondCustomLimit() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); binder.setAutoGrowCollectionLimit(10); @@ -1971,7 +1975,7 @@ public class DataBinderTests { } @Test - public void testFieldErrorAccessVariations() throws Exception { + public void testFieldErrorAccessVariations() { TestBean testBean = new TestBean(); DataBinder binder = new DataBinder(testBean, "testBean"); assertNull(binder.getBindingResult().getGlobalError()); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java index befc921f748..2d366ce81e2 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2018 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. @@ -32,6 +32,7 @@ import java.util.Set; import javax.validation.Constraint; import javax.validation.ConstraintValidator; import javax.validation.ConstraintValidatorContext; +import javax.validation.ConstraintViolation; import javax.validation.Payload; import javax.validation.Valid; import javax.validation.Validation; @@ -48,6 +49,7 @@ import org.springframework.beans.BeanWrapperImpl; import org.springframework.context.support.StaticMessageSource; import org.springframework.util.ObjectUtils; import org.springframework.validation.BeanPropertyBindingResult; +import org.springframework.validation.FieldError; import static java.lang.annotation.ElementType.*; import static java.lang.annotation.RetentionPolicy.*; @@ -93,8 +95,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("pass")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Size of Password is must be between 8 and 128")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password is must be between 8 and 128")); + assertTrue(error.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error.getSource()).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -108,8 +113,11 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("password"), is(1)); assertThat(errors.getFieldValue("password"), is("password")); - assertThat(messageSource.getMessage(errors.getFieldError("password"), Locale.ENGLISH), - is("Password must be same value with Password(Confirm)")); + FieldError error = errors.getFieldError("password"); + assertNotNull(error); + assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value with Password(Confirm)")); + assertTrue(error.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error.getSource()).getPropertyPath().toString(), is("password")); } @Test // SPR-13406 @@ -124,10 +132,16 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value with confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error1.getSource()).getPropertyPath().toString(), is("email")); + assertTrue(error2.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error2.getSource()).getPropertyPath().toString(), is("confirmEmail")); } @Test // SPR-15123 @@ -144,10 +158,16 @@ public class SpringValidatorAdapterTests { assertThat(errors.getFieldErrorCount("email"), is(1)); assertThat(errors.getFieldValue("email"), is("test@example.com")); assertThat(errors.getFieldErrorCount("confirmEmail"), is(1)); - assertThat(messageSource.getMessage(errors.getFieldError("email"), Locale.ENGLISH), - is("email must be same value with confirmEmail")); - assertThat(messageSource.getMessage(errors.getFieldError("confirmEmail"), Locale.ENGLISH), - is("Email required")); + FieldError error1 = errors.getFieldError("email"); + FieldError error2 = errors.getFieldError("confirmEmail"); + assertNotNull(error1); + assertNotNull(error2); + assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value with confirmEmail")); + assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required")); + assertTrue(error1.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error1.getSource()).getPropertyPath().toString(), is("email")); + assertTrue(error2.getSource() instanceof ConstraintViolation); + assertThat(((ConstraintViolation) error2.getSource()).getPropertyPath().toString(), is("confirmEmail")); } @Test // SPR-16177