Skip to content

Commit aeef959

Browse files
committed
SpringValidatorAdapter's ObjectError subclasses are serializable
Closes gh-23181
1 parent 2aec175 commit aeef959

File tree

4 files changed

+113
-49
lines changed

4 files changed

+113
-49
lines changed

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
import org.springframework.beans.BeanWrapperImpl;
5151
import org.springframework.context.support.StaticMessageSource;
5252
import org.springframework.util.ObjectUtils;
53+
import org.springframework.util.SerializationTestUtils;
5354
import org.springframework.validation.BeanPropertyBindingResult;
5455
import org.springframework.validation.FieldError;
5556
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
@@ -89,7 +90,7 @@ public void testUnwrap() {
8990
}
9091

9192
@Test // SPR-13406
92-
public void testNoStringArgumentValue() {
93+
public void testNoStringArgumentValue() throws Exception {
9394
TestBean testBean = new TestBean();
9495
testBean.setPassword("pass");
9596
testBean.setConfirmPassword("pass");
@@ -104,10 +105,11 @@ public void testNoStringArgumentValue() {
104105
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password must be between 8 and 128"));
105106
assertTrue(error.contains(ConstraintViolation.class));
106107
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
108+
assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString()), is(error.toString()));
107109
}
108110

109111
@Test // SPR-13406
110-
public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() {
112+
public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() throws Exception {
111113
TestBean testBean = new TestBean();
112114
testBean.setPassword("password");
113115
testBean.setConfirmPassword("PASSWORD");
@@ -122,6 +124,7 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLog
122124
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value as Password(Confirm)"));
123125
assertTrue(error.contains(ConstraintViolation.class));
124126
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
127+
assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString()), is(error.toString()));
125128
}
126129

127130
@Test // SPR-13406
@@ -518,10 +521,10 @@ public boolean isValid(Object value, ConstraintValidatorContext context) {
518521
.addPropertyNode(f.getName())
519522
.addConstraintViolation();
520523
}
521-
} catch (IllegalAccessException ex) {
524+
}
525+
catch (IllegalAccessException ex) {
522526
throw new IllegalStateException(ex);
523527
}
524-
525528
});
526529
return fieldsErros.isEmpty();
527530
}

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

Lines changed: 96 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -165,27 +165,15 @@ protected void processConstraintViolations(Set<ConstraintViolation<Object>> viol
165165
String nestedField = bindingResult.getNestedPath() + field;
166166
if (nestedField.isEmpty()) {
167167
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
168-
ObjectError error = new ObjectError(
169-
errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()) {
170-
@Override
171-
public boolean shouldRenderDefaultMessage() {
172-
return requiresMessageFormat(violation);
173-
}
174-
};
175-
error.wrap(violation);
168+
ObjectError error = new ViolationObjectError(
169+
errors.getObjectName(), errorCodes, errorArgs, violation, this);
176170
bindingResult.addError(error);
177171
}
178172
else {
179173
Object rejectedValue = getRejectedValue(field, violation, bindingResult);
180174
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
181-
FieldError error = new FieldError(errors.getObjectName(), nestedField,
182-
rejectedValue, false, errorCodes, errorArgs, violation.getMessage()) {
183-
@Override
184-
public boolean shouldRenderDefaultMessage() {
185-
return requiresMessageFormat(violation);
186-
}
187-
};
188-
error.wrap(violation);
175+
FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField,
176+
rejectedValue, errorCodes, errorArgs, violation, this);
189177
bindingResult.addError(error);
190178
}
191179
}
@@ -307,29 +295,6 @@ protected MessageSourceResolvable getResolvableField(String objectName, String f
307295
return new DefaultMessageSourceResolvable(codes, field);
308296
}
309297

310-
/**
311-
* Indicate whether this violation's interpolated message has remaining
312-
* placeholders and therefore requires {@link java.text.MessageFormat}
313-
* to be applied to it. Called for a Bean Validation defined message
314-
* (coming out {@code ValidationMessages.properties}) when rendered
315-
* as the default message in Spring's MessageSource.
316-
* <p>The default implementation considers a Spring-style "{0}" placeholder
317-
* for the field name as an indication for {@link java.text.MessageFormat}.
318-
* Any other placeholder or escape syntax occurrences are typically a
319-
* mismatch, coming out of regex pattern values or the like. Note that
320-
* standard Bean Validation does not support "{0}" style placeholders at all;
321-
* this is a feature typically used in Spring MessageSource resource bundles.
322-
* @param violation the Bean Validation constraint violation, including
323-
* BV-defined interpolation of named attribute references in its message
324-
* @return {@code true} if {@code java.text.MessageFormat} is to be applied,
325-
* or {@code false} if the violation's message should be used as-is
326-
* @since 5.1.8
327-
* @see #getArgumentsForConstraint
328-
*/
329-
protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
330-
return violation.getMessage().contains("{0}");
331-
}
332-
333298
/**
334299
* Extract the rejected value behind the given constraint violation,
335300
* for exposure through the Spring errors representation.
@@ -354,6 +319,33 @@ protected Object getRejectedValue(String field, ConstraintViolation<Object> viol
354319
return invalidValue;
355320
}
356321

322+
/**
323+
* Indicate whether this violation's interpolated message has remaining
324+
* placeholders and therefore requires {@link java.text.MessageFormat}
325+
* to be applied to it. Called for a Bean Validation defined message
326+
* (coming out {@code ValidationMessages.properties}) when rendered
327+
* as the default message in Spring's MessageSource.
328+
* <p>The default implementation considers a Spring-style "{0}" placeholder
329+
* for the field name as an indication for {@link java.text.MessageFormat}.
330+
* Any other placeholder or escape syntax occurrences are typically a
331+
* mismatch, coming out of regex pattern values or the like. Note that
332+
* standard Bean Validation does not support "{0}" style placeholders at all;
333+
* this is a feature typically used in Spring MessageSource resource bundles.
334+
* @param violation the Bean Validation constraint violation, including
335+
* BV-defined interpolation of named attribute references in its message
336+
* @return {@code true} if {@code java.text.MessageFormat} is to be applied,
337+
* or {@code false} if the violation's message should be used as-is
338+
* @since 5.1.8
339+
* @see #getArgumentsForConstraint
340+
*/
341+
protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
342+
return containsSpringStylePlaceholder(violation.getMessage());
343+
}
344+
345+
private static boolean containsSpringStylePlaceholder(@Nullable String message) {
346+
return (message != null && message.contains("{0}"));
347+
}
348+
357349

358350
//---------------------------------------------------------------------
359351
// Implementation of JSR-303 Validator interface
@@ -436,6 +428,71 @@ public Object[] getArguments() {
436428
public String getDefaultMessage() {
437429
return this.resolvableString;
438430
}
431+
432+
@Override
433+
public String toString() {
434+
return this.resolvableString;
435+
}
436+
}
437+
438+
439+
/**
440+
* Subclass of {@code ObjectError} with Spring-style default message rendering.
441+
*/
442+
@SuppressWarnings("serial")
443+
private static class ViolationObjectError extends ObjectError implements Serializable {
444+
445+
@Nullable
446+
private transient SpringValidatorAdapter adapter;
447+
448+
@Nullable
449+
private transient ConstraintViolation<?> violation;
450+
451+
public ViolationObjectError(String objectName, String[] codes, Object[] arguments,
452+
ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {
453+
454+
super(objectName, codes, arguments, violation.getMessage());
455+
this.adapter = adapter;
456+
this.violation = violation;
457+
wrap(violation);
458+
}
459+
460+
@Override
461+
public boolean shouldRenderDefaultMessage() {
462+
return (this.adapter != null && this.violation != null ?
463+
this.adapter.requiresMessageFormat(this.violation) :
464+
containsSpringStylePlaceholder(getDefaultMessage()));
465+
}
466+
}
467+
468+
469+
/**
470+
* Subclass of {@code FieldError} with Spring-style default message rendering.
471+
*/
472+
@SuppressWarnings("serial")
473+
private static class ViolationFieldError extends FieldError implements Serializable {
474+
475+
@Nullable
476+
private transient SpringValidatorAdapter adapter;
477+
478+
@Nullable
479+
private transient ConstraintViolation<?> violation;
480+
481+
public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes,
482+
Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {
483+
484+
super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage());
485+
this.adapter = adapter;
486+
this.violation = violation;
487+
wrap(violation);
488+
}
489+
490+
@Override
491+
public boolean shouldRenderDefaultMessage() {
492+
return (this.adapter != null && this.violation != null ?
493+
this.adapter.requiresMessageFormat(this.violation) :
494+
containsSpringStylePlaceholder(getDefaultMessage()));
495+
}
439496
}
440497

441498
}

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

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.springframework.beans.BeanWrapperImpl;
4949
import org.springframework.context.support.StaticMessageSource;
5050
import org.springframework.util.ObjectUtils;
51+
import org.springframework.util.SerializationTestUtils;
5152
import org.springframework.validation.BeanPropertyBindingResult;
5253
import org.springframework.validation.FieldError;
5354

@@ -86,7 +87,7 @@ public void testUnwrap() {
8687
}
8788

8889
@Test // SPR-13406
89-
public void testNoStringArgumentValue() {
90+
public void testNoStringArgumentValue() throws Exception {
9091
TestBean testBean = new TestBean();
9192
testBean.setPassword("pass");
9293
testBean.setConfirmPassword("pass");
@@ -101,10 +102,11 @@ public void testNoStringArgumentValue() {
101102
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Size of Password must be between 8 and 128"));
102103
assertTrue(error.contains(ConstraintViolation.class));
103104
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
105+
assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString()), is(error.toString()));
104106
}
105107

106108
@Test // SPR-13406
107-
public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() {
109+
public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLogicalFieldName() throws Exception {
108110
TestBean testBean = new TestBean();
109111
testBean.setPassword("password");
110112
testBean.setConfirmPassword("PASSWORD");
@@ -119,6 +121,7 @@ public void testApplyMessageSourceResolvableToStringArgumentValueWithResolvedLog
119121
assertThat(messageSource.getMessage(error, Locale.ENGLISH), is("Password must be same value as Password(Confirm)"));
120122
assertTrue(error.contains(ConstraintViolation.class));
121123
assertThat(error.unwrap(ConstraintViolation.class).getPropertyPath().toString(), is("password"));
124+
assertThat(SerializationTestUtils.serializeAndDeserialize(error.toString()), is(error.toString()));
122125
}
123126

124127
@Test // SPR-13406
@@ -473,10 +476,10 @@ public boolean isValid(Object value, ConstraintValidatorContext context) {
473476
.addPropertyNode(f.getName())
474477
.addConstraintViolation();
475478
}
476-
} catch (IllegalAccessException ex) {
479+
}
480+
catch (IllegalAccessException ex) {
477481
throw new IllegalStateException(ex);
478482
}
479-
480483
});
481484
return fieldsErros.isEmpty();
482485
}

src/checkstyle/checkstyle.xml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
<?xml version="1.0"?>
22
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
33
<module name="com.puppycrawl.tools.checkstyle.Checker">
4-
54
<!-- Suppressions -->
65
<module name="SuppressionFilter">
76
<property name="file" value="${config_loc}/checkstyle-suppressions.xml"/>
@@ -46,7 +45,9 @@
4645
<module name="com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck" />
4746
<module name="com.puppycrawl.tools.checkstyle.checks.design.InterfaceIsTypeCheck" />
4847
<module name="com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck" />
49-
<module name="com.puppycrawl.tools.checkstyle.checks.design.MutableExceptionCheck" />
48+
<module name="com.puppycrawl.tools.checkstyle.checks.design.MutableExceptionCheck">
49+
<property name="format" value="^.*Exception$" />
50+
</module>
5051
<module name="com.puppycrawl.tools.checkstyle.checks.design.InnerTypeLastCheck" />
5152
<module name="com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck" />
5253

0 commit comments

Comments
 (0)