Skip to content

Commit

Permalink
SpringValidatorAdapter's ObjectError subclasses are serializable
Browse files Browse the repository at this point in the history
Closes gh-23181
  • Loading branch information
jhoeller committed Jul 5, 2019
1 parent 2aec175 commit aeef959
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.SerializationTestUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.beanvalidation.SpringValidatorAdapter;
Expand Down Expand Up @@ -89,7 +90,7 @@ public void testUnwrap() {
}

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

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

@Test // SPR-13406
Expand Down Expand Up @@ -518,10 +521,10 @@ public boolean isValid(Object value, ConstraintValidatorContext context) {
.addPropertyNode(f.getName())
.addConstraintViolation();
}
} catch (IllegalAccessException ex) {
}
catch (IllegalAccessException ex) {
throw new IllegalStateException(ex);
}

});
return fieldsErros.isEmpty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -165,27 +165,15 @@ protected void processConstraintViolations(Set<ConstraintViolation<Object>> viol
String nestedField = bindingResult.getNestedPath() + field;
if (nestedField.isEmpty()) {
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode);
ObjectError error = new ObjectError(
errors.getObjectName(), errorCodes, errorArgs, violation.getMessage()) {
@Override
public boolean shouldRenderDefaultMessage() {
return requiresMessageFormat(violation);
}
};
error.wrap(violation);
ObjectError error = new ViolationObjectError(
errors.getObjectName(), errorCodes, errorArgs, violation, this);
bindingResult.addError(error);
}
else {
Object rejectedValue = getRejectedValue(field, violation, bindingResult);
String[] errorCodes = bindingResult.resolveMessageCodes(errorCode, field);
FieldError error = new FieldError(errors.getObjectName(), nestedField,
rejectedValue, false, errorCodes, errorArgs, violation.getMessage()) {
@Override
public boolean shouldRenderDefaultMessage() {
return requiresMessageFormat(violation);
}
};
error.wrap(violation);
FieldError error = new ViolationFieldError(errors.getObjectName(), nestedField,
rejectedValue, errorCodes, errorArgs, violation, this);
bindingResult.addError(error);
}
}
Expand Down Expand Up @@ -307,29 +295,6 @@ protected MessageSourceResolvable getResolvableField(String objectName, String f
return new DefaultMessageSourceResolvable(codes, field);
}

/**
* Indicate whether this violation's interpolated message has remaining
* placeholders and therefore requires {@link java.text.MessageFormat}
* to be applied to it. Called for a Bean Validation defined message
* (coming out {@code ValidationMessages.properties}) when rendered
* as the default message in Spring's MessageSource.
* <p>The default implementation considers a Spring-style "{0}" placeholder
* for the field name as an indication for {@link java.text.MessageFormat}.
* Any other placeholder or escape syntax occurrences are typically a
* mismatch, coming out of regex pattern values or the like. Note that
* standard Bean Validation does not support "{0}" style placeholders at all;
* this is a feature typically used in Spring MessageSource resource bundles.
* @param violation the Bean Validation constraint violation, including
* BV-defined interpolation of named attribute references in its message
* @return {@code true} if {@code java.text.MessageFormat} is to be applied,
* or {@code false} if the violation's message should be used as-is
* @since 5.1.8
* @see #getArgumentsForConstraint
*/
protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
return violation.getMessage().contains("{0}");
}

/**
* Extract the rejected value behind the given constraint violation,
* for exposure through the Spring errors representation.
Expand All @@ -354,6 +319,33 @@ protected Object getRejectedValue(String field, ConstraintViolation<Object> viol
return invalidValue;
}

/**
* Indicate whether this violation's interpolated message has remaining
* placeholders and therefore requires {@link java.text.MessageFormat}
* to be applied to it. Called for a Bean Validation defined message
* (coming out {@code ValidationMessages.properties}) when rendered
* as the default message in Spring's MessageSource.
* <p>The default implementation considers a Spring-style "{0}" placeholder
* for the field name as an indication for {@link java.text.MessageFormat}.
* Any other placeholder or escape syntax occurrences are typically a
* mismatch, coming out of regex pattern values or the like. Note that
* standard Bean Validation does not support "{0}" style placeholders at all;
* this is a feature typically used in Spring MessageSource resource bundles.
* @param violation the Bean Validation constraint violation, including
* BV-defined interpolation of named attribute references in its message
* @return {@code true} if {@code java.text.MessageFormat} is to be applied,
* or {@code false} if the violation's message should be used as-is
* @since 5.1.8
* @see #getArgumentsForConstraint
*/
protected boolean requiresMessageFormat(ConstraintViolation<?> violation) {
return containsSpringStylePlaceholder(violation.getMessage());
}

private static boolean containsSpringStylePlaceholder(@Nullable String message) {
return (message != null && message.contains("{0}"));
}


//---------------------------------------------------------------------
// Implementation of JSR-303 Validator interface
Expand Down Expand Up @@ -436,6 +428,71 @@ public Object[] getArguments() {
public String getDefaultMessage() {
return this.resolvableString;
}

@Override
public String toString() {
return this.resolvableString;
}
}


/**
* Subclass of {@code ObjectError} with Spring-style default message rendering.
*/
@SuppressWarnings("serial")
private static class ViolationObjectError extends ObjectError implements Serializable {

@Nullable
private transient SpringValidatorAdapter adapter;

@Nullable
private transient ConstraintViolation<?> violation;

public ViolationObjectError(String objectName, String[] codes, Object[] arguments,
ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {

super(objectName, codes, arguments, violation.getMessage());
this.adapter = adapter;
this.violation = violation;
wrap(violation);
}

@Override
public boolean shouldRenderDefaultMessage() {
return (this.adapter != null && this.violation != null ?
this.adapter.requiresMessageFormat(this.violation) :
containsSpringStylePlaceholder(getDefaultMessage()));
}
}


/**
* Subclass of {@code FieldError} with Spring-style default message rendering.
*/
@SuppressWarnings("serial")
private static class ViolationFieldError extends FieldError implements Serializable {

@Nullable
private transient SpringValidatorAdapter adapter;

@Nullable
private transient ConstraintViolation<?> violation;

public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes,
Object[] arguments, ConstraintViolation<?> violation, SpringValidatorAdapter adapter) {

super(objectName, field, rejectedValue, false, codes, arguments, violation.getMessage());
this.adapter = adapter;
this.violation = violation;
wrap(violation);
}

@Override
public boolean shouldRenderDefaultMessage() {
return (this.adapter != null && this.violation != null ?
this.adapter.requiresMessageFormat(this.violation) :
containsSpringStylePlaceholder(getDefaultMessage()));
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
import org.springframework.beans.BeanWrapperImpl;
import org.springframework.context.support.StaticMessageSource;
import org.springframework.util.ObjectUtils;
import org.springframework.util.SerializationTestUtils;
import org.springframework.validation.BeanPropertyBindingResult;
import org.springframework.validation.FieldError;

Expand Down Expand Up @@ -86,7 +87,7 @@ public void testUnwrap() {
}

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

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

@Test // SPR-13406
Expand Down Expand Up @@ -473,10 +476,10 @@ public boolean isValid(Object value, ConstraintValidatorContext context) {
.addPropertyNode(f.getName())
.addConstraintViolation();
}
} catch (IllegalAccessException ex) {
}
catch (IllegalAccessException ex) {
throw new IllegalStateException(ex);
}

});
return fieldsErros.isEmpty();
}
Expand Down
5 changes: 3 additions & 2 deletions src/checkstyle/checkstyle.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?xml version="1.0"?>
<!DOCTYPE module PUBLIC "-//Checkstyle//DTD Checkstyle Configuration 1.3//EN" "https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="com.puppycrawl.tools.checkstyle.Checker">

<!-- Suppressions -->
<module name="SuppressionFilter">
<property name="file" value="${config_loc}/checkstyle-suppressions.xml"/>
Expand Down Expand Up @@ -46,7 +45,9 @@
<module name="com.puppycrawl.tools.checkstyle.checks.design.FinalClassCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.design.InterfaceIsTypeCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.design.HideUtilityClassConstructorCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.design.MutableExceptionCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.design.MutableExceptionCheck">
<property name="format" value="^.*Exception$" />
</module>
<module name="com.puppycrawl.tools.checkstyle.checks.design.InnerTypeLastCheck" />
<module name="com.puppycrawl.tools.checkstyle.checks.design.OneTopLevelClassCheck" />

Expand Down

0 comments on commit aeef959

Please sign in to comment.