Support for nested path using field access

This commit adds a nested path support for DirectFieldAccessor that is
similar to what BeanWrapper provides. It is now possible to use
expressions such as "person.address.city.name" to access the name of
the city that a given person lives in using fields to traverse the
graph.

DirectFieldAccessor also now supports an auto-grow option to create
a default instance for a "null" intermediate path. This option is
false by default and leads to a NullValueInNestedPathException in such
a case.

This commit also harmonizes part of the tests suite so that core tests
are shared between BeanWrapperImpl and DirectFieldAccessor.

Note that map and list access is not implemented as part of this
commit.

Issue: SPR-9705
This commit is contained in:
Stephane Nicoll
2014-06-26 19:19:56 +02:00
parent 442bd682a7
commit 8221c9abc5
9 changed files with 670 additions and 155 deletions

View File

@@ -0,0 +1,400 @@
/*
* Copyright 2002-2014 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.beans;
import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
/**
*
* @author Stephane Nicoll
*/
public abstract class AbstractConfigurablePropertyAccessorTests {
@Rule
public final ExpectedException thrown = ExpectedException.none();
protected abstract ConfigurablePropertyAccessor createAccessor(Object target);
@Test
public void isReadableProperty() {
ConfigurablePropertyAccessor accessor = createAccessor(new Simple("John", 2));
assertThat(accessor.isReadableProperty("name"), is(true));
}
@Test
public void isReadablePropertyNotReadable() {
ConfigurablePropertyAccessor accessor = createAccessor(new NoRead());
assertFalse(accessor.isReadableProperty("age"));
}
/**
* Shouldn't throw an exception: should just return false
*/
@Test
public void isReadablePropertyNoSuchProperty() {
ConfigurablePropertyAccessor accessor = createAccessor(new NoRead());
assertFalse(accessor.isReadableProperty("xxxxx"));
}
@Test
public void isReadablePropertyNull() {
ConfigurablePropertyAccessor accessor = createAccessor(new NoRead());
thrown.expect(IllegalArgumentException.class);
accessor.isReadableProperty(null);
}
@Test
public void isWritableProperty() {
ConfigurablePropertyAccessor accessor = createAccessor(new Simple("John", 2));
assertThat(accessor.isWritableProperty("name"), is(true));
}
@Test
public void isWritablePropertyNull() {
ConfigurablePropertyAccessor accessor = createAccessor(new NoRead());
thrown.expect(IllegalArgumentException.class);
accessor.isWritableProperty(null);
}
@Test
public void isWritablePropertyNoSuchProperty() {
ConfigurablePropertyAccessor accessor = createAccessor(new NoRead());
assertFalse(accessor.isWritableProperty("xxxxx"));
}
@Test
public void getSimpleProperty() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
assertThat(accessor.getPropertyValue("name"), is("John"));
}
@Test
public void getNestedProperty() {
Person person = createPerson("John", "London", "UK");
ConfigurablePropertyAccessor accessor = createAccessor(person);
assertThat(accessor.getPropertyValue("address.city"), is("London"));
}
@Test
public void getNestedDeepProperty() {
Person person = createPerson("John", "London", "UK");
ConfigurablePropertyAccessor accessor = createAccessor(person);
assertThat(accessor.getPropertyValue("address.country.name"), is("UK"));
}
@Test
public void getPropertyIntermediateFieldIsNull() {
Person person = createPerson("John", "London", "UK");
person.address = null;
ConfigurablePropertyAccessor accessor = createAccessor(person);
try {
accessor.getPropertyValue("address.country.name");
fail("Should have failed to get value with null intermediate path");
}
catch (NullValueInNestedPathException e) {
assertEquals("address", e.getPropertyName());
assertEquals(Person.class, e.getBeanClass());
}
}
@Test
public void getPropertyIntermediateFieldIsNullWithAutoGrow() {
Person person = createPerson("John", "London", "UK");
person.address = null;
ConfigurablePropertyAccessor accessor = createAccessor(person);
accessor.setAutoGrowNestedPaths(true);
assertEquals("DefaultCountry", accessor.getPropertyValue("address.country.name"));
}
@Test
public void getUnknownField() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
try {
accessor.getPropertyValue("foo");
fail("Should have failed to get an unknown field.");
}
catch (NotReadablePropertyException e) {
assertEquals(Simple.class, e.getBeanClass());
assertEquals("foo", e.getPropertyName());
}
}
@Test
public void getUnknownNestedField() {
Person person = createPerson("John", "London", "UK");
ConfigurablePropertyAccessor accessor = createAccessor(person);
thrown.expect(NotReadablePropertyException.class);
accessor.getPropertyValue("address.bar");
}
@Test
public void setSimpleProperty() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
accessor.setPropertyValue("name", "SomeValue");
assertThat(simple.name, is("SomeValue"));
assertThat(simple.getName(), is("SomeValue"));
}
@Test
public void setNestedProperty() {
Person person = createPerson("John", "Paris", "FR");
ConfigurablePropertyAccessor accessor = createAccessor(person);
accessor.setPropertyValue("address.city", "London");
assertThat(person.address.city, is("London"));
}
@Test
public void setNestedDeepProperty() {
Person person = createPerson("John", "Paris", "FR");
ConfigurablePropertyAccessor accessor = createAccessor(person);
accessor.setPropertyValue("address.country.name", "UK");
assertThat(person.address.country.name, is("UK"));
}
@Test
public void setPropertyIntermediateFieldIsNull() {
Person person = createPerson("John", "Paris", "FR");
person.address.country = null;
ConfigurablePropertyAccessor accessor = createAccessor(person);
try {
accessor.setPropertyValue("address.country.name", "UK");
fail("Should have failed to set value with intermediate null value");
}
catch (NullValueInNestedPathException e) {
assertEquals("address.country", e.getPropertyName());
assertEquals(Person.class, e.getBeanClass());
}
assertThat(person.address.country, is(nullValue())); // Not touched
}
@Test
public void setPropertyIntermediateFieldIsNullWithAutoGrow() {
Person person = createPerson("John", "Paris", "FR");
person.address.country = null;
ConfigurablePropertyAccessor accessor = createAccessor(person);
accessor.setAutoGrowNestedPaths(true);
accessor.setPropertyValue("address.country.name", "UK");
assertThat(person.address.country.name, is("UK"));
}
@Test
public void setUnknownField() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
try {
accessor.setPropertyValue("foo", "value");
fail("Should have failed to set an unknown field.");
}
catch (NotWritablePropertyException e) {
assertEquals(Simple.class, e.getBeanClass());
assertEquals("foo", e.getPropertyName());
}
}
@Test
public void setUnknownNestedField() {
Person person = createPerson("John", "Paris", "FR");
ConfigurablePropertyAccessor accessor = createAccessor(person);
thrown.expect(NotWritablePropertyException.class);
accessor.setPropertyValue("address.bar", "value");
}
@Test
public void propertyType() {
Person person = createPerson("John", "Paris", "FR");
ConfigurablePropertyAccessor accessor = createAccessor(person);
assertEquals(String.class, accessor.getPropertyType("address.city"));
}
@Test
public void propertyTypeUnknownField() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
assertThat(accessor.getPropertyType("foo"), is(nullValue()));
}
@Test
public void propertyTypeDescriptor() {
Person person = createPerson("John", "Paris", "FR");
ConfigurablePropertyAccessor accessor = createAccessor(person);
assertThat(accessor.getPropertyTypeDescriptor("address.city"), is(notNullValue()));
}
@Test
public void propertyTypeDescriptorUnknownField() {
Simple simple = new Simple("John", 2);
ConfigurablePropertyAccessor accessor = createAccessor(simple);
assertThat(accessor.getPropertyTypeDescriptor("foo"), is(nullValue()));
}
private Person createPerson(String name, String city, String country) {
return new Person(name, new Address(city, country));
}
private static class Simple {
private String name;
private Integer integer;
private Simple(String name, Integer integer) {
this.name = name;
this.integer = integer;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getInteger() {
return integer;
}
public void setInteger(Integer integer) {
this.integer = integer;
}
}
private static class Person {
private String name;
private Address address;
private Person(String name, Address address) {
this.name = name;
this.address = address;
}
public Person() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Address getAddress() {
return address;
}
public void setAddress(Address address) {
this.address = address;
}
}
private static class Address {
private String city;
private Country country;
private Address(String city, String country) {
this.city = city;
this.country = new Country(country);
}
public Address() {
this("DefaultCity", "DefaultCountry");
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public Country getCountry() {
return country;
}
public void setCountry(Country country) {
this.country = country;
}
}
private static class Country {
private String name;
public Country(String name) {
this.name = name;
}
public Country() {
this(null);
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
@SuppressWarnings("unused")
static class NoRead {
public void setAge(int age) {
}
}
}

View File

@@ -68,7 +68,12 @@ import static org.junit.Assert.*;
* @author Chris Beams
* @author Dave Syer
*/
public final class BeanWrapperTests {
public final class BeanWrapperTests extends AbstractConfigurablePropertyAccessorTests {
@Override
protected ConfigurablePropertyAccessor createAccessor(Object target) {
return new BeanWrapperImpl(target);
}
@Test
public void testNullNestedTypeDescriptor() {
@@ -116,48 +121,6 @@ public final class BeanWrapperTests {
assertEquals("9", foo.listOfMaps.get(0).get("luckyNumber"));
}
@Test
public void testIsReadablePropertyNotReadable() {
NoRead nr = new NoRead();
BeanWrapper bw = new BeanWrapperImpl(nr);
assertFalse(bw.isReadableProperty("age"));
}
/**
* Shouldn't throw an exception: should just return false
*/
@Test
public void testIsReadablePropertyNoSuchProperty() {
NoRead nr = new NoRead();
BeanWrapper bw = new BeanWrapperImpl(nr);
assertFalse(bw.isReadableProperty("xxxxx"));
}
@Test
public void testIsReadablePropertyNull() {
NoRead nr = new NoRead();
BeanWrapper bw = new BeanWrapperImpl(nr);
try {
bw.isReadableProperty(null);
fail("Can't inquire into readability of null property");
}
catch (IllegalArgumentException ex) {
// expected
}
}
@Test
public void testIsWritablePropertyNull() {
NoRead nr = new NoRead();
BeanWrapper bw = new BeanWrapperImpl(nr);
try {
bw.isWritableProperty(null);
fail("Can't inquire into writability of null property");
}
catch (IllegalArgumentException ex) {
// expected
}
}
@Test
public void testReadableAndWritableForIndexedProperties() {
@@ -1636,12 +1599,6 @@ public final class BeanWrapperTests {
}
@SuppressWarnings("unused")
private static class NoRead {
public void setAge(int age) {
}
}
@SuppressWarnings("unused")

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2002-2011 the original author or authors.
* Copyright 2002-2014 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.
@@ -29,7 +29,12 @@ import org.junit.Test;
* @author Jose Luis Martin
* @author Chris Beams
*/
public class DirectFieldAccessorTests {
public class DirectFieldAccessorTests extends AbstractConfigurablePropertyAccessorTests {
@Override
protected ConfigurablePropertyAccessor createAccessor(Object target) {
return new DirectFieldAccessor(target);
}
@Test
public void withShadowedField() throws Exception {
@@ -42,4 +47,5 @@ public class DirectFieldAccessorTests {
DirectFieldAccessor dfa = new DirectFieldAccessor(p);
assertEquals(JTextField.class, dfa.getPropertyType("name"));
}
}