Skip to content

Commit 37609e4

Browse files
committed
Object/FieldError exposes source object through unwrap/contains methods
Issue: SPR-16372
1 parent 6bbc5b0 commit 37609e4

File tree

5 files changed

+74
-34
lines changed

5 files changed

+74
-34
lines changed

spring-context/src/main/java/org/springframework/validation/DefaultBindingErrorProcessor.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ public void processPropertyAccessException(PropertyAccessException ex, BindingRe
7777
}
7878
FieldError error = new FieldError(bindingResult.getObjectName(), field, rejectedValue, true,
7979
codes, arguments, ex.getLocalizedMessage());
80-
error.initSource(ex);
80+
error.wrap(ex);
8181
bindingResult.addError(error);
8282
}
8383

spring-context/src/main/java/org/springframework/validation/ObjectError.java

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -74,30 +74,59 @@ public String getObjectName() {
7474
}
7575

7676
/**
77-
* Initialize the source behind this error: possibly an {@link Exception}
77+
* Preserve the source behind this error: possibly an {@link Exception}
7878
* (typically {@link org.springframework.beans.PropertyAccessException})
7979
* or a Bean Validation {@link javax.validation.ConstraintViolation}.
8080
* <p>Note that any such source object is being stored as transient:
8181
* that is, it won't be part of a serialized error representation.
8282
* @param source the source object
8383
* @since 5.0.4
8484
*/
85-
public void initSource(Object source) {
86-
Assert.state(this.source == null, "Source already initialized");
85+
public void wrap(Object source) {
86+
if (this.source != null) {
87+
throw new IllegalStateException("Already wrapping " + this.source);
88+
}
8789
this.source = source;
8890
}
8991

9092
/**
91-
* Return the source behind this error: possibly an {@link Exception}
93+
* Unwrap the source behind this error: possibly an {@link Exception}
9294
* (typically {@link org.springframework.beans.PropertyAccessException})
9395
* or a Bean Validation {@link javax.validation.ConstraintViolation}.
94-
* @return the source object, or {@code null} if none available
95-
* (none specified or not available anymore after deserialization)
96+
* <p>The cause of the outermost exception will be introspected as well,
97+
* e.g. the underlying conversion exception or exception thrown from a setter
98+
* (instead of having to unwrap the {@code PropertyAccessException} in turn).
99+
* @return the source object of the given type
100+
* @throws IllegalArgumentException if no such source object is available
101+
* (i.e. none specified or not available anymore after deserialization)
96102
* @since 5.0.4
97103
*/
98-
@Nullable
99-
public Object getSource() {
100-
return this.source;
104+
public <T> T unwrap(Class<T> sourceType) {
105+
if (sourceType.isInstance(this.source)) {
106+
return sourceType.cast(this.source);
107+
}
108+
else if (this.source instanceof Throwable) {
109+
Throwable cause = ((Throwable) this.source).getCause();
110+
if (sourceType.isInstance(cause)) {
111+
return sourceType.cast(cause);
112+
}
113+
}
114+
throw new IllegalArgumentException("No source object of the given type available: " + sourceType);
115+
}
116+
117+
/**
118+
* Check the source behind this error: possibly an {@link Exception}
119+
* (typically {@link org.springframework.beans.PropertyAccessException})
120+
* or a Bean Validation {@link javax.validation.ConstraintViolation}.
121+
* <p>The cause of the outermost exception will be introspected as well,
122+
* e.g. the underlying conversion exception or exception thrown from a setter
123+
* (instead of having to unwrap the {@code PropertyAccessException} in turn).
124+
* @return whether this error has been caused by a source object of the given type
125+
* @since 5.0.4
126+
*/
127+
public boolean contains(Class<?> sourceType) {
128+
return (sourceType.isInstance(this.source) ||
129+
(this.source instanceof Throwable && sourceType.isInstance(((Throwable) this.source).getCause())));
101130
}
102131

103132

spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,15 +144,15 @@ protected void processConstraintViolations(Set<ConstraintViolation<Object>> viol
144144
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
145145
ObjectError error = new ObjectError(
146146
errors.getObjectName(), errorCodes, errorArgs, violation.getMessage());
147-
error.initSource(violation);
147+
error.wrap(violation);
148148
bindingResult.addError(error);
149149
}
150150
else {
151151
Object rejectedValue = getRejectedValue(field, violation, bindingResult);
152152
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
153153
FieldError error = new FieldError(errors.getObjectName(), nestedField,
154154
rejectedValue, false, errorCodes, errorArgs, violation.getMessage());
155-
error.initSource(violation);
155+
error.wrap(violation);
156156
bindingResult.addError(error);
157157
}
158158
}

spring-context/src/test/java/org/springframework/validation/DataBinderTests.java

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,25 @@ public void testBindingWithErrors() {
238238

239239
assertTrue("Has age errors", br.hasFieldErrors("age"));
240240
assertTrue("Correct number of age errors", br.getFieldErrorCount("age") == 1);
241-
assertEquals("typeMismatch", binder.getBindingResult().getFieldError("age").getCode());
242241
assertEquals("32x", binder.getBindingResult().getFieldValue("age"));
243-
assertEquals("32x", binder.getBindingResult().getFieldError("age").getRejectedValue());
244-
assertTrue(binder.getBindingResult().getFieldError("age").getSource() instanceof TypeMismatchException);
242+
FieldError ageError = binder.getBindingResult().getFieldError("age");
243+
assertNotNull(ageError);
244+
assertEquals("typeMismatch", ageError.getCode());
245+
assertEquals("32x", ageError.getRejectedValue());
246+
assertTrue(ageError.contains(TypeMismatchException.class));
247+
assertTrue(ageError.contains(NumberFormatException.class));
248+
assertTrue(ageError.unwrap(NumberFormatException.class).getMessage().contains("32x"));
245249
assertEquals(0, tb.getAge());
246250

247251
assertTrue("Has touchy errors", br.hasFieldErrors("touchy"));
248252
assertTrue("Correct number of touchy errors", br.getFieldErrorCount("touchy") == 1);
249-
assertEquals("methodInvocation", binder.getBindingResult().getFieldError("touchy").getCode());
250253
assertEquals("m.y", binder.getBindingResult().getFieldValue("touchy"));
251-
assertEquals("m.y", binder.getBindingResult().getFieldError("touchy").getRejectedValue());
252-
assertTrue(binder.getBindingResult().getFieldError("touchy").getSource() instanceof MethodInvocationException);
254+
FieldError touchyError = binder.getBindingResult().getFieldError("touchy");
255+
assertNotNull(touchyError);
256+
assertEquals("methodInvocation", touchyError.getCode());
257+
assertEquals("m.y", touchyError.getRejectedValue());
258+
assertTrue(touchyError.contains(MethodInvocationException.class));
259+
assertTrue(touchyError.unwrap(MethodInvocationException.class).getCause().getMessage().contains("a ."));
253260
assertNull(tb.getTouchy());
254261

255262
rod = new TestBean();
@@ -331,16 +338,20 @@ public String getAsText() {
331338

332339
assertTrue("Has age errors", br.hasFieldErrors("age"));
333340
assertTrue("Correct number of age errors", br.getFieldErrorCount("age") == 1);
334-
assertEquals("typeMismatch", binder.getBindingResult().getFieldError("age").getCode());
335341
assertEquals("32x", binder.getBindingResult().getFieldValue("age"));
336-
assertEquals("32x", binder.getBindingResult().getFieldError("age").getRejectedValue());
342+
FieldError ageError = binder.getBindingResult().getFieldError("age");
343+
assertNotNull(ageError);
344+
assertEquals("typeMismatch", ageError.getCode());
345+
assertEquals("32x", ageError.getRejectedValue());
337346
assertEquals(0, tb.getAge());
338347

339348
assertTrue("Has touchy errors", br.hasFieldErrors("touchy"));
340349
assertTrue("Correct number of touchy errors", br.getFieldErrorCount("touchy") == 1);
341-
assertEquals("methodInvocation", binder.getBindingResult().getFieldError("touchy").getCode());
342350
assertEquals("m.y", binder.getBindingResult().getFieldValue("touchy"));
343-
assertEquals("m.y", binder.getBindingResult().getFieldError("touchy").getRejectedValue());
351+
FieldError touchyError = binder.getBindingResult().getFieldError("touchy");
352+
assertNotNull(touchyError);
353+
assertEquals("methodInvocation", touchyError.getCode());
354+
assertEquals("m.y", touchyError.getRejectedValue());
344355
assertNull(tb.getTouchy());
345356

346357
assertTrue("Does not have spouse errors", !br.hasFieldErrors("spouse"));

spring-context/src/test/java/org/springframework/validation/beanvalidation/SpringValidatorAdapterTests.java

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ public void testNoStringArgumentValue() {
9898
FieldError error = errors.getFieldError("password");
9999
assertNotNull(error);
100100
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password is must be between 8 and 128"));
101-
assertTrue(error.getSource() instanceof ConstraintViolation);
102-
assertThat(((ConstraintViolation) error.getSource()).getPropertyPath().toString(), is("password"));
101+
assertTrue(error.contains(ConstraintViolation.class));
102+
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
103103
}
104104

105105
@Test // SPR-13406
@@ -116,8 +116,8 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLog
116116
FieldError error = errors.getFieldError("password");
117117
assertNotNull(error);
118118
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value with Password(Confirm)"));
119-
assertTrue(error.getSource() instanceof ConstraintViolation);
120-
assertThat(((ConstraintViolation) error.getSource()).getPropertyPath().toString(), is("password"));
119+
assertTrue(error.contains(ConstraintViolation.class));
120+
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
121121
}
122122

123123
@Test // SPR-13406
@@ -138,10 +138,10 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithUnresolvedL
138138
assertNotNull(error2);
139139
assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value with confirmEmail"));
140140
assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required"));
141-
assertTrue(error1.getSource() instanceof ConstraintViolation);
142-
assertThat(((ConstraintViolation) error1.getSource()).getPropertyPath().toString(), is("email"));
143-
assertTrue(error2.getSource() instanceof ConstraintViolation);
144-
assertThat(((ConstraintViolation) error2.getSource()).getPropertyPath().toString(), is("confirmEmail"));
141+
assertTrue(error1.contains(ConstraintViolation.class));
142+
assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email"));
143+
assertTrue(error2.contains(ConstraintViolation.class));
144+
assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail"));
145145
}
146146

147147
@Test // SPR-15123
@@ -164,10 +164,10 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithAlwaysUseMe
164164
assertNotNull(error2);
165165
assertThat(messageSource.getMessage(error1, Locale.ENGLISH), is("email must be same value with confirmEmail"));
166166
assertThat(messageSource.getMessage(error2, Locale.ENGLISH), is("Email required"));
167-
assertTrue(error1.getSource() instanceof ConstraintViolation);
168-
assertThat(((ConstraintViolation) error1.getSource()).getPropertyPath().toString(), is("email"));
169-
assertTrue(error2.getSource() instanceof ConstraintViolation);
170-
assertThat(((ConstraintViolation) error2.getSource()).getPropertyPath().toString(), is("confirmEmail"));
167+
assertTrue(error1.contains(ConstraintViolation.class));
168+
assertThat(error1.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("email"));
169+
assertTrue(error2.contains(ConstraintViolation.class));
170+
assertThat(error2.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("confirmEmail"));
171171
}
172172

173173
@Test // SPR-16177

0 commit comments

Comments
 (0)