Improve documentation and matching algorithm in data binders

This commit is contained in:
Sam Brannen
2022-04-03 16:19:52 +02:00
parent 0cf7f7bd89
commit a7cf19cec5
16 changed files with 402 additions and 203 deletions

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 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.
@@ -51,18 +51,20 @@ import org.springframework.util.PatternMatchUtils;
import org.springframework.util.StringUtils;
/**
* Binder that allows for setting property values onto a target object,
* including support for validation and binding result analysis.
* The binding process can be customized through specifying allowed fields,
* Binder that allows for setting property values on a target object, including
* support for validation and binding result analysis.
*
* <p>The binding process can be customized by specifying allowed field patterns,
* required fields, custom editors, etc.
*
* <p>Note that there are potential security implications in failing to set an array
* of allowed fields. In the case of HTTP form POST data for example, malicious clients
* can attempt to subvert an application by supplying values for fields or properties
* that do not exist on the form. In some cases this could lead to illegal data being
* set on command objects <i>or their nested objects</i>. For this reason, it is
* <b>highly recommended to specify the {@link #setAllowedFields allowedFields} property</b>
* on the DataBinder.
* <p><strong>WARNING</strong>: Data binding can lead to security issues by exposing
* parts of the object graph that are not meant to be accessed or modified by
* external clients. Therefore the design and use of data binding should be considered
* carefully with regard to security. For more details, please refer to the dedicated
* sections on data binding for
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder-model-design">Spring Web MVC</a> and
* <a href="https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html#webflux-ann-initbinder-model-design">Spring WebFlux</a>
* in the reference manual.
*
* <p>The binding results can be examined via the {@link BindingResult} interface,
* extending the {@link Errors} interface: see the {@link #getBindingResult()} method.
@@ -96,6 +98,7 @@ import org.springframework.util.StringUtils;
* @author Rob Harrop
* @author Stephane Nicoll
* @author Kazuki Shimizu
* @author Sam Brannen
* @see #setAllowedFields
* @see #setRequiredFields
* @see #registerCustomEditor
@@ -418,15 +421,21 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Register fields that should be allowed for binding. Default is all fields.
* Restrict this for example to avoid unwanted modifications by malicious
* Register field patterns that should be allowed for binding.
* <p>Default is all fields.
* <p>Restrict this for example to avoid unwanted modifications by malicious
* users when binding HTTP request parameters.
* <p>Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an
* arbitrary number of pattern parts), as well as direct equality. More
* sophisticated matching can be implemented by overriding the
* {@code isAllowed} method.
* <p>Alternatively, specify a list of <i>disallowed</i> fields.
* @param allowedFields array of field names
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>The default implementation of this method stores allowed field patterns
* in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical}
* form. Subclasses which override this method must therefore take this into
* account.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>disallowed</i> field patterns.
* @param allowedFields array of allowed field patterns
* @see #setDisallowedFields
* @see #isAllowed(String)
*/
@@ -435,8 +444,9 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Return the fields that should be allowed for binding.
* @return array of field names
* Return the field patterns that should be allowed for binding.
* @return array of allowed field patterns
* @see #setAllowedFields(String...)
*/
@Nullable
public String[] getAllowedFields() {
@@ -444,25 +454,44 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Register fields that should <i>not</i> be allowed for binding. Default
* is none. Mark fields as disallowed for example to avoid unwanted
* Register field patterns that should <i>not</i> be allowed for binding.
* <p>Default is none.
* <p>Mark fields as disallowed, for example to avoid unwanted
* modifications by malicious users when binding HTTP request parameters.
* <p>Supports "xxx*", "*xxx", "*xxx*" and "xxx*yyy" matches (with an
* arbitrary number of pattern parts), as well as direct equality.
* More sophisticated matching can be implemented by overriding the
* {@code isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> fields.
* @param disallowedFields array of field names
* <p>Supports {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality.
* <p>The default implementation of this method stores disallowed field patterns
* in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical}
* form. As of Spring Framework 5.2.21, the default implementation also transforms
* disallowed field patterns to {@linkplain String#toLowerCase() lowercase} to
* support case-insensitive pattern matching in {@link #isAllowed}. Subclasses
* which override this method must therefore take both of these transformations
* into account.
* <p>More sophisticated matching can be implemented by overriding the
* {@link #isAllowed} method.
* <p>Alternatively, specify a list of <i>allowed</i> field patterns.
* @param disallowedFields array of disallowed field patterns
* @see #setAllowedFields
* @see #isAllowed(String)
*/
public void setDisallowedFields(@Nullable String... disallowedFields) {
this.disallowedFields = PropertyAccessorUtils.canonicalPropertyNames(disallowedFields);
if (disallowedFields == null) {
this.disallowedFields = null;
}
else {
String[] fieldPatterns = new String[disallowedFields.length];
for (int i = 0; i < fieldPatterns.length; i++) {
fieldPatterns[i] = PropertyAccessorUtils.canonicalPropertyName(disallowedFields[i]).toLowerCase();
}
this.disallowedFields = fieldPatterns;
}
}
/**
* Return the fields that should <i>not</i> be allowed for binding.
* @return array of field names
* Return the field patterns that should <i>not</i> be allowed for binding.
* @return array of disallowed field patterns
* @see #setDisallowedFields(String...)
*/
@Nullable
public String[] getDisallowedFields() {
@@ -774,16 +803,20 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
}
/**
* Return if the given field is allowed for binding.
* Invoked for each passed-in property value.
* <p>The default implementation checks for "xxx*", "*xxx", "*xxx*" and "xxx*yyy"
* matches (with an arbitrary number of pattern parts), as well as direct equality,
* in the specified lists of allowed fields and disallowed fields. A field matching
* a disallowed pattern will not be accepted even if it also happens to match a
* pattern in the allowed list.
* <p>Can be overridden in subclasses.
* Determine if the given field is allowed for binding.
* <p>Invoked for each passed-in property value.
* <p>Checks for {@code "xxx*"}, {@code "*xxx"}, {@code "*xxx*"}, and
* {@code "xxx*yyy"} matches (with an arbitrary number of pattern parts), as
* well as direct equality, in the configured lists of allowed field patterns
* and disallowed field patterns.
* <p>Matching against allowed field patterns is case-sensitive; whereas,
* matching against disallowed field patterns is case-insensitive.
* <p>A field matching a disallowed pattern will not be accepted even if it
* also happens to match a pattern in the allowed list.
* <p>Can be overridden in subclasses, but care must be taken to honor the
* aforementioned contract.
* @param field the field to check
* @return if the field is allowed
* @return {@code true} if the field is allowed
* @see #setAllowedFields
* @see #setDisallowedFields
* @see org.springframework.util.PatternMatchUtils#simpleMatch(String, String)
@@ -792,7 +825,7 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter {
String[] allowed = getAllowedFields();
String[] disallowed = getDisallowedFields();
return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) &&
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field)));
(ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase())));
}
/**

View File

@@ -64,23 +64,26 @@ import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.lang.Nullable;
import org.springframework.tests.sample.beans.BeanWithObjectProperty;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.entry;
/**
* Unit tests for {@link DataBinder}.
*
* @author Rod Johnson
* @author Juergen Hoeller
* @author Rob Harrop
* @author Kazuki Shimizu
* @author Sam Brannen
*/
class DataBinderTests {
@Test
void testBindingNoErrors() throws BindException {
void bindingNoErrors() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@@ -110,12 +113,11 @@ class DataBinderTests {
assertThat(ex).isEqualTo(binder.getBindingResult());
other.reject("xxx");
boolean condition = !other.equals(binder.getBindingResult());
assertThat(condition).isTrue();
assertThat(other).isNotEqualTo(binder.getBindingResult());
}
@Test
void testBindingWithDefaultConversionNoErrors() throws BindException {
void bindingWithDefaultConversionNoErrors() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@@ -131,7 +133,7 @@ class DataBinderTests {
}
@Test
void testNestedBindingWithDefaultConversionNoErrors() throws BindException {
void nestedBindingWithDefaultConversionNoErrors() throws BindException {
TestBean rod = new TestBean(new TestBean());
DataBinder binder = new DataBinder(rod, "person");
assertThat(binder.isIgnoreUnknownFields()).isTrue();
@@ -147,7 +149,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsNotIgnoreUnknown() {
void bindingNoErrorsNotIgnoreUnknown() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setIgnoreUnknownFields(false);
@@ -160,7 +162,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsWithInvalidField() {
void bindingNoErrorsWithInvalidField() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -171,7 +173,7 @@ class DataBinderTests {
}
@Test
void testBindingNoErrorsWithIgnoreInvalid() {
void bindingNoErrorsWithIgnoreInvalid() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setIgnoreInvalidFields(true);
@@ -180,10 +182,14 @@ class DataBinderTests {
pvs.add("spouse.age", 32);
binder.bind(pvs);
binder.close();
assertThat(rod.getName()).isEqualTo("Rod");
assertThat(rod.getSpouse()).isNull();
}
@Test
void testBindingWithErrors() {
void bindingWithErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -245,7 +251,7 @@ class DataBinderTests {
}
@Test
void testBindingWithSystemFieldError() {
void bindingWithSystemFieldError() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -257,7 +263,7 @@ class DataBinderTests {
}
@Test
void testBindingWithErrorsAndCustomEditors() {
void bindingWithErrorsAndCustomEditors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.registerCustomEditor(String.class, "touchy", new PropertyEditorSupport() {
@@ -325,7 +331,7 @@ class DataBinderTests {
}
@Test
void testBindingWithCustomEditorOnObjectField() {
void bindingWithCustomEditorOnObjectField() {
BeanWithObjectProperty tb = new BeanWithObjectProperty();
DataBinder binder = new DataBinder(tb);
binder.registerCustomEditor(Integer.class, "object", new CustomNumberEditor(Integer.class, true));
@@ -336,7 +342,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatter() {
void bindingWithFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -368,7 +374,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatter() {
void bindingErrorWithFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -391,7 +397,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithParseExceptionFromFormatter() {
void bindingErrorWithParseExceptionFromFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -419,7 +425,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithRuntimeExceptionFromFormatter() {
void bindingErrorWithRuntimeExceptionFromFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -447,7 +453,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatterAgainstList() {
void bindingWithFormatterAgainstList() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -469,7 +475,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatterAgainstList() {
void bindingErrorWithFormatterAgainstList() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -492,7 +498,7 @@ class DataBinderTests {
}
@Test
void testBindingWithFormatterAgainstFields() {
void bindingWithFormatterAgainstFields() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
FormattingConversionService conversionService = new FormattingConversionService();
@@ -525,7 +531,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithFormatterAgainstFields() {
void bindingErrorWithFormatterAgainstFields() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.initDirectFieldAccess();
@@ -549,7 +555,7 @@ class DataBinderTests {
}
@Test
void testBindingWithCustomFormatter() {
void bindingWithCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.addCustomFormatter(new NumberStyleFormatter(), Float.class);
@@ -578,7 +584,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithCustomFormatter() {
void bindingErrorWithCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
binder.addCustomFormatter(new NumberStyleFormatter());
@@ -599,7 +605,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithParseExceptionFromCustomFormatter() {
void bindingErrorWithParseExceptionFromCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
@@ -624,7 +630,7 @@ class DataBinderTests {
}
@Test
void testBindingErrorWithRuntimeExceptionFromCustomFormatter() {
void bindingErrorWithRuntimeExceptionFromCustomFormatter() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb);
@@ -649,7 +655,7 @@ class DataBinderTests {
}
@Test
void testConversionWithInappropriateStringEditor() {
void conversionWithInappropriateStringEditor() {
DataBinder dataBinder = new DataBinder(null);
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
dataBinder.setConversionService(conversionService);
@@ -662,7 +668,7 @@ class DataBinderTests {
}
@Test
void testBindingWithAllowedFields() throws BindException {
void bindingWithAllowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "myparam");
@@ -672,30 +678,32 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
}
@Test
void testBindingWithDisallowedFields() throws BindException {
void bindingWithDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setDisallowedFields("age");
binder.setDisallowedFields(" ", "\t", "favouriteColour", null, "age");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "Rod");
pvs.add("age", "32x");
pvs.add("favouriteColour", "BLUE");
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields.length).isEqualTo(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(rod.getFavouriteColour()).as("did not change favourite colour").isNull();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactlyInAnyOrder("age", "favouriteColour");
}
@Test
void testBindingWithAllowedAndDisallowedFields() throws BindException {
void bindingWithAllowedAndDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "myparam");
@@ -706,34 +714,32 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age");
}
@Test
void testBindingWithOverlappingAllowedAndDisallowedFields() throws BindException {
void bindingWithOverlappingAllowedAndDisallowedFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("name", "age");
binder.setDisallowedFields("age");
binder.setDisallowedFields("AGE");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("name", "Rod");
pvs.add("age", "32x");
binder.bind(pvs);
binder.close();
assertThat(rod.getName().equals("Rod")).as("changed name correctly").isTrue();
assertThat(rod.getAge() == 0).as("did not change age").isTrue();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("age");
assertThat(rod.getName()).as("changed name correctly").isEqualTo("Rod");
assertThat(rod.getAge()).as("did not change age").isZero();
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("age");
}
@Test
void testBindingWithAllowedFieldsUsingAsterisks() throws BindException {
void bindingWithAllowedFieldsUsingAsterisks() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
binder.setAllowedFields("nam*", "*ouchy");
@@ -760,11 +766,11 @@ class DataBinderTests {
}
@Test
void testBindingWithAllowedAndDisallowedMapFields() throws BindException {
void bindingWithAllowedAndDisallowedMapFields() throws BindException {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod);
binder.setAllowedFields("someMap[key1]", "someMap[key2]");
binder.setDisallowedFields("someMap['key3']", "someMap[key4]");
binder.setDisallowedFields("someMap['KEY3']", "SomeMap[key4]");
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("someMap[key1]", "value1");
@@ -774,21 +780,18 @@ class DataBinderTests {
binder.bind(pvs);
binder.close();
assertThat(rod.getSomeMap().get("key1")).isEqualTo("value1");
assertThat(rod.getSomeMap().get("key2")).isEqualTo("value2");
assertThat(rod.getSomeMap().get("key3")).isNull();
assertThat(rod.getSomeMap().get("key4")).isNull();
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(2);
assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key3]")).isTrue();
assertThat(ObjectUtils.containsElement(disallowedFields, "someMap[key4]")).isTrue();
@SuppressWarnings("unchecked")
Map<String, String> someMap = (Map<String, String>) rod.getSomeMap();
assertThat(someMap).containsOnly(entry("key1", "value1"), entry("key2", "value2"));
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("someMap[key3]", "someMap[key4]");
}
/**
* Tests for required field, both null, non-existing and empty strings.
*/
@Test
void testBindingWithRequiredFields() {
void bindingWithRequiredFields() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@@ -819,7 +822,7 @@ class DataBinderTests {
}
@Test
void testBindingWithRequiredMapFields() {
void bindingWithRequiredMapFields() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@@ -839,7 +842,7 @@ class DataBinderTests {
}
@Test
void testBindingWithNestedObjectCreation() {
void bindingWithNestedObjectCreation() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "person");
@@ -860,7 +863,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorWithOldValueAccess() {
void customEditorWithOldValueAccess() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@@ -885,7 +888,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForSingleProperty() {
void customEditorForSingleProperty() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
DataBinder binder = new DataBinder(tb, "tb");
@@ -925,7 +928,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForPrimitiveProperty() {
void customEditorForPrimitiveProperty() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@@ -949,7 +952,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorForAllStringProperties() {
void customEditorForAllStringProperties() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@@ -981,7 +984,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForSingleProperty() {
void customFormatterForSingleProperty() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
DataBinder binder = new DataBinder(tb, "tb");
@@ -1021,7 +1024,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForPrimitiveProperty() {
void customFormatterForPrimitiveProperty() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@@ -1045,7 +1048,7 @@ class DataBinderTests {
}
@Test
void testCustomFormatterForAllStringProperties() {
void customFormatterForAllStringProperties() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
@@ -1077,7 +1080,7 @@ class DataBinderTests {
}
@Test
void testJavaBeanPropertyConventions() {
void javaBeanPropertyConventions() {
Book book = new Book();
DataBinder binder = new DataBinder(book);
@@ -1101,7 +1104,7 @@ class DataBinderTests {
}
@Test
void testOptionalProperty() {
void optionalProperty() {
OptionalHolder bean = new OptionalHolder();
DataBinder binder = new DataBinder(bean);
binder.setConversionService(new DefaultConversionService());
@@ -1122,7 +1125,7 @@ class DataBinderTests {
}
@Test
void testValidatorNoErrors() throws Exception {
void validatorNoErrors() throws Exception {
TestBean tb = new TestBean();
tb.setAge(33);
tb.setName("Rod");
@@ -1175,15 +1178,13 @@ class DataBinderTests {
assertThat(errors.getNestedPath()).isEqualTo("spouse.");
assertThat(errors.getErrorCount()).isEqualTo(1);
boolean condition1 = !errors.hasGlobalErrors();
assertThat(condition1).isTrue();
assertThat(errors.hasGlobalErrors()).isFalse();
assertThat(errors.getFieldErrorCount("age")).isEqualTo(1);
boolean condition = !errors.hasFieldErrors("name");
assertThat(condition).isTrue();
assertThat(errors.hasFieldErrors("name")).isFalse();
}
@Test
void testValidatorWithErrors() {
void validatorWithErrors() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@@ -1252,7 +1253,7 @@ class DataBinderTests {
}
@Test
void testValidatorWithErrorsAndCodesPrefix() {
void validatorWithErrorsAndCodesPrefix() {
TestBean tb = new TestBean();
tb.setSpouse(new TestBean());
@@ -1324,7 +1325,7 @@ class DataBinderTests {
}
@Test
void testValidatorWithNestedObjectNull() {
void validatorWithNestedObjectNull() {
TestBean tb = new TestBean();
Errors errors = new BeanPropertyBindingResult(tb, "tb");
Validator testValidator = new TestBeanValidator();
@@ -1343,7 +1344,7 @@ class DataBinderTests {
}
@Test
void testNestedValidatorWithoutNestedPath() {
void nestedValidatorWithoutNestedPath() {
TestBean tb = new TestBean();
tb.setName("XXX");
Errors errors = new BeanPropertyBindingResult(tb, "tb");
@@ -1357,7 +1358,8 @@ class DataBinderTests {
}
@Test
void testBindingStringArrayToIntegerSet() {
@SuppressWarnings("unchecked")
void bindingStringArrayToIntegerSet() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class) {
@@ -1371,12 +1373,8 @@ class DataBinderTests {
binder.bind(pvs);
assertThat(binder.getBindingResult().getFieldValue("set")).isEqualTo(tb.getSet());
boolean condition = tb.getSet() instanceof TreeSet;
assertThat(condition).isTrue();
assertThat(tb.getSet().size()).isEqualTo(3);
assertThat(tb.getSet().contains(10)).isTrue();
assertThat(tb.getSet().contains(20)).isTrue();
assertThat(tb.getSet().contains(30)).isTrue();
assertThat(tb.getSet()).isInstanceOf(TreeSet.class);
assertThat((Set<Integer>) tb.getSet()).containsExactly(10, 20, 30);
pvs = new MutablePropertyValues();
pvs.add("set", null);
@@ -1386,7 +1384,7 @@ class DataBinderTests {
}
@Test
void testBindingNullToEmptyCollection() {
void bindingNullToEmptyCollection() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(Set.class, new CustomCollectionEditor(TreeSet.class, true));
@@ -1394,13 +1392,12 @@ class DataBinderTests {
pvs.add("set", null);
binder.bind(pvs);
boolean condition = tb.getSet() instanceof TreeSet;
assertThat(condition).isTrue();
assertThat(tb.getSet().isEmpty()).isTrue();
assertThat(tb.getSet()).isInstanceOf(TreeSet.class);
assertThat(tb.getSet()).isEmpty();
}
@Test
void testBindingToIndexedField() {
void bindingToIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String.class, "array.name", new PropertyEditorSupport() {
@@ -1439,7 +1436,7 @@ class DataBinderTests {
}
@Test
void testBindingToNestedIndexedField() {
void bindingToNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@@ -1470,7 +1467,7 @@ class DataBinderTests {
}
@Test
void testEditorForNestedIndexedField() {
void editorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@@ -1496,7 +1493,7 @@ class DataBinderTests {
}
@Test
void testSpecificEditorForNestedIndexedField() {
void specificEditorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@@ -1522,7 +1519,7 @@ class DataBinderTests {
}
@Test
void testInnerSpecificEditorForNestedIndexedField() {
void innerSpecificEditorForNestedIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
tb.getArray()[0].setNestedIndexedBean(new IndexedTestBean());
tb.getArray()[1].setNestedIndexedBean(new IndexedTestBean());
@@ -1548,7 +1545,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToIndexedField() {
void directBindingToIndexedField() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "array", new PropertyEditorSupport() {
@@ -1601,7 +1598,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() {
void directBindingToEmptyIndexedFieldWithRegisteredSpecificEditor() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "map[key0]", new PropertyEditorSupport() {
@@ -1632,7 +1629,7 @@ class DataBinderTests {
}
@Test
void testDirectBindingToEmptyIndexedFieldWithRegisteredGenericEditor() {
void directBindingToEmptyIndexedFieldWithRegisteredGenericEditor() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, "map", new PropertyEditorSupport() {
@@ -1663,7 +1660,7 @@ class DataBinderTests {
}
@Test
void testCustomEditorWithSubclass() {
void customEditorWithSubclass() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(TestBean.class, new PropertyEditorSupport() {
@@ -1697,7 +1694,7 @@ class DataBinderTests {
}
@Test
void testBindToStringArrayWithArrayEditor() {
void bindToStringArrayWithArrayEditor() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String[].class, "stringArray", new PropertyEditorSupport() {
@@ -1709,15 +1706,12 @@ class DataBinderTests {
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("stringArray", "a1-b2");
binder.bind(pvs);
boolean condition = !binder.getBindingResult().hasErrors();
assertThat(condition).isTrue();
assertThat(tb.getStringArray().length).isEqualTo(2);
assertThat(tb.getStringArray()[0]).isEqualTo("a1");
assertThat(tb.getStringArray()[1]).isEqualTo("b2");
assertThat(binder.getBindingResult().hasErrors()).isFalse();
assertThat(tb.getStringArray()).containsExactly("a1", "b2");
}
@Test
void testBindToStringArrayWithComponentEditor() {
void bindToStringArrayWithComponentEditor() {
TestBean tb = new TestBean();
DataBinder binder = new DataBinder(tb, "tb");
binder.registerCustomEditor(String.class, "stringArray", new PropertyEditorSupport() {
@@ -1729,15 +1723,14 @@ class DataBinderTests {
MutablePropertyValues pvs = new MutablePropertyValues();
pvs.add("stringArray", new String[] {"a1", "b2"});
binder.bind(pvs);
boolean condition = !binder.getBindingResult().hasErrors();
assertThat(condition).isTrue();
assertThat(binder.getBindingResult().hasErrors()).isFalse();
assertThat(tb.getStringArray().length).isEqualTo(2);
assertThat(tb.getStringArray()[0]).isEqualTo("Xa1");
assertThat(tb.getStringArray()[1]).isEqualTo("Xb2");
}
@Test
void testBindingErrors() {
void bindingErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -1764,7 +1757,7 @@ class DataBinderTests {
}
@Test
void testAddAllErrors() {
void addAllErrors() {
TestBean rod = new TestBean();
DataBinder binder = new DataBinder(rod, "person");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -1784,7 +1777,7 @@ class DataBinderTests {
@Test
@SuppressWarnings("unchecked")
void testBindingWithResortedList() {
void bindingWithResortedList() {
IndexedTestBean tb = new IndexedTestBean();
DataBinder binder = new DataBinder(tb, "tb");
MutablePropertyValues pvs = new MutablePropertyValues();
@@ -1802,7 +1795,7 @@ class DataBinderTests {
}
@Test
void testRejectWithoutDefaultMessage() {
void rejectWithoutDefaultMessage() {
TestBean tb = new TestBean();
tb.setName("myName");
tb.setAge(99);
@@ -1820,7 +1813,7 @@ class DataBinderTests {
}
@Test
void testBindExceptionSerializable() throws Exception {
void bindExceptionSerializable() throws Exception {
SerializablePerson tb = new SerializablePerson();
tb.setName("myName");
tb.setAge(99);
@@ -1849,27 +1842,27 @@ class DataBinderTests {
}
@Test
void testTrackDisallowedFields() {
void trackDisallowedFields() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAllowedFields("name", "age");
String name = "Rob Harrop";
String beanName = "foobar";
int age = 42;
MutablePropertyValues mpvs = new MutablePropertyValues();
mpvs.add("name", name);
mpvs.add("beanName", beanName);
mpvs.add("age", age);
mpvs.add("beanName", "foobar");
binder.bind(mpvs);
assertThat(testBean.getName()).isEqualTo(name);
String[] disallowedFields = binder.getBindingResult().getSuppressedFields();
assertThat(disallowedFields).hasSize(1);
assertThat(disallowedFields[0]).isEqualTo("beanName");
assertThat(testBean.getAge()).isEqualTo(age);
assertThat(binder.getBindingResult().getSuppressedFields()).containsExactly("beanName");
}
@Test
void testAutoGrowWithinDefaultLimit() {
void autoGrowWithinDefaultLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@@ -1881,7 +1874,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowBeyondDefaultLimit() {
void autoGrowBeyondDefaultLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
@@ -1894,7 +1887,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowWithinCustomLimit() {
void autoGrowWithinCustomLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAutoGrowCollectionLimit(10);
@@ -1907,7 +1900,7 @@ class DataBinderTests {
}
@Test
void testAutoGrowBeyondCustomLimit() {
void autoGrowBeyondCustomLimit() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setAutoGrowCollectionLimit(10);
@@ -1921,7 +1914,7 @@ class DataBinderTests {
}
@Test
void testNestedGrowingList() {
void nestedGrowingList() {
Form form = new Form();
DataBinder binder = new DataBinder(form, "form");
MutablePropertyValues mpv = new MutablePropertyValues();
@@ -1937,7 +1930,7 @@ class DataBinderTests {
}
@Test
void testFieldErrorAccessVariations() {
void fieldErrorAccessVariations() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
assertThat(binder.getBindingResult().getGlobalError()).isNull();
@@ -1958,7 +1951,7 @@ class DataBinderTests {
}
@Test // SPR-14888
void testSetAutoGrowCollectionLimit() {
void setAutoGrowCollectionLimit() {
BeanWithIntegerList tb = new BeanWithIntegerList();
DataBinder binder = new DataBinder(tb);
binder.setAutoGrowCollectionLimit(257);
@@ -1972,7 +1965,7 @@ class DataBinderTests {
}
@Test // SPR-14888
void testSetAutoGrowCollectionLimitAfterInitialization() {
void setAutoGrowCollectionLimitAfterInitialization() {
DataBinder binder = new DataBinder(new BeanWithIntegerList());
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
assertThatIllegalStateException().isThrownBy(() ->
@@ -1981,7 +1974,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() {
void setCustomMessageCodesResolverBeforeInitializeBindingResultForBeanPropertyAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
@@ -1998,7 +1991,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() {
void setCustomMessageCodesResolverBeforeInitializeBindingResultForDirectFieldAccess() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
DefaultMessageCodesResolver messageCodesResolver = new DefaultMessageCodesResolver();
@@ -2013,7 +2006,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetCustomMessageCodesResolverAfterInitializeBindingResult() {
void setCustomMessageCodesResolverAfterInitializeBindingResult() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.initBeanPropertyAccess();
@@ -2028,7 +2021,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testSetMessageCodesResolverIsNullAfterInitializeBindingResult() {
void setMessageCodesResolverIsNullAfterInitializeBindingResult() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.initBeanPropertyAccess();
@@ -2042,8 +2035,7 @@ class DataBinderTests {
}
@Test // SPR-15009
void testCallSetMessageCodesResolverTwice() {
void callSetMessageCodesResolverTwice() {
TestBean testBean = new TestBean();
DataBinder binder = new DataBinder(testBean, "testBean");
binder.setMessageCodesResolver(new DefaultMessageCodesResolver());