DATACMNS-1610 - Improved property path access.

We now expose a dedicated PersistentPropertyPathAccessor and optionally take options for both the read and write access to properties. On read, clients can define to eagerly receive null in case one of the path segments is null. By default, we reject those as it indicates that there might be an issue with the backing object if the client assumes it can look up the more deeply nested path to receive a result.

On write access clients can define to either reject, skip or log intermediate null segments where skip means, that the setting is just silently aborted. Clients can also control how to deal with collection and map intermediates. By default we now propagate the attempt to set a value to all collection or map items respectively. This could be potentially extended to provide a filter receiving both the property and property value to selectively propagate those calls in the future.

The separation of these APIs (PersistentPropertyAccessor and PersistentPropertyPathAccessor) is introduced as the latter can be implemented on top of the former and the former potentially being dynamically generated. The logic of the latter can then be clearly separated from the actual individual property lookup. ConvertingPropertyAccessor is now a PersistentPropertyPathAccessor, too, and overrides SimplePersistentPropertyPathAccessor.getTypedProperty(…) to plug in conversion logic. This allows custom collection types being used if the ConversionService backing the accessor can convert from and to Collection or Map respectively.

Deprecated all PersistentPropertyPath-related methods on PersistentPropertyAccessor for removal in 2.3.

MappingAuditableBeanWrapperFactory now uses these new settings to skip both null values as well as collection and map values when setting auditing metadata.

Related tickets: DATACMNS-1438, DATACMNS-1461, DATACMNS-1609.
This commit is contained in:
Oliver Drotbohm
2018-12-17 18:20:33 +01:00
committed by Oliver Drotbohm
parent 70f973f6f9
commit 5cdb3139a4
11 changed files with 933 additions and 40 deletions

View File

@@ -18,9 +18,15 @@ package org.springframework.data.mapping.model;
import static org.assertj.core.api.Assertions.*;
import static org.mockito.Mockito.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Value;
import org.junit.Test;
import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.data.mapping.PersistentPropertyAccessor;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
import org.springframework.format.support.DefaultFormattingConversionService;
@@ -108,6 +114,25 @@ public class ConvertingPropertyAccessorUnitTests {
});
}
@Test // DATACMNS-1377
public void shouldConvertToPropertyPathLeafType() {
Order order = new Order(new Customer("1"));
SampleMappingContext context = new SampleMappingContext();
PersistentPropertyAccessor<Order> accessor = context.getPersistentEntity(Order.class).getPropertyAccessor(order);
ConvertingPropertyAccessor<Order> convertingAccessor = new ConvertingPropertyAccessor<>(accessor,
new DefaultConversionService());
PersistentPropertyPath<SamplePersistentProperty> path = context.getPersistentPropertyPath("customer.firstname",
Order.class);
convertingAccessor.setProperty(path, 2);
assertThat(convertingAccessor.getBean().getCustomer().getFirstname()).isEqualTo("2");
}
private static ConvertingPropertyAccessor getAccessor(Object entity, ConversionService conversionService) {
PersistentPropertyAccessor wrapper = new BeanWrapper<>(entity);
@@ -125,4 +150,15 @@ public class ConvertingPropertyAccessorUnitTests {
static class Entity {
Long id;
}
@Value
static class Order {
Customer customer;
}
@Data
@AllArgsConstructor
static class Customer {
String firstname;
}
}

View File

@@ -0,0 +1,127 @@
/*
* Copyright 2018 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.data.mapping.model;
import static org.assertj.core.api.Assertions.*;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Value;
import lombok.experimental.Wither;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Stream;
import org.junit.Test;
import org.springframework.data.mapping.AccessOptions;
import org.springframework.data.mapping.AccessOptions.SetOptions.SetNulls;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentPropertyPath;
import org.springframework.data.mapping.PersistentPropertyPathAccessor;
import org.springframework.data.mapping.context.SampleMappingContext;
import org.springframework.data.mapping.context.SamplePersistentProperty;
/**
* Unit tests for {@link SimplePersistentPropertyPathAccessor}.
*
* @author Oliver Gierke
* @since 2.3
* @soundtrack Ron Spielman Trio - Raindrops (Electric Tales)
*/
public class SimplePersistentPropertyPathAccessorUnitTests {
SampleMappingContext context = new SampleMappingContext();
Customer first = new Customer("1");
Customer second = new Customer("2");
@Test // DATACMNS-1438
public void setsPropertyContainingCollectionPathForAllElements() {
Customers customers = new Customers(Arrays.asList(first, second), Collections.emptyMap());
assertFirstnamesSetFor(customers, "customers.firstname");
}
@Test // DATACMNS-1438
public void setsPropertyContainingMapPathForAllValues() {
Map<String, Customer> map = new HashMap<>();
map.put("1", first);
map.put("2", second);
Customers customers = new Customers(Collections.emptyList(), map);
assertFirstnamesSetFor(customers, "customerMap.firstname");
}
@Test // DATACMNS-1461
public void skipsNullValueIfConfigured() {
CustomerWrapper wrapper = new CustomerWrapper(null);
PersistentPropertyPathAccessor<CustomerWrapper> accessor = getAccessor(wrapper);
PersistentPropertyPath<SamplePersistentProperty> path = context.getPersistentPropertyPath("customer.firstname",
CustomerWrapper.class);
assertThatCode(() -> {
accessor.setProperty(path, "Dave", AccessOptions.defaultSetOptions().withNullHandling(SetNulls.SKIP));
}).doesNotThrowAnyException();
}
private void assertFirstnamesSetFor(Customers customers, String path) {
PersistentPropertyPath<SamplePersistentProperty> propertyPath = context.getPersistentPropertyPath(path,
Customers.class);
getAccessor(customers).setProperty(propertyPath, "firstname");
Stream.of(first, second).forEach(it -> {
assertThat(it.firstname).isEqualTo("firstname");
});
}
private <T> PersistentPropertyPathAccessor<T> getAccessor(T source) {
Class<? extends Object> type = source.getClass();
PersistentEntity<Object, SamplePersistentProperty> entity = context.getRequiredPersistentEntity(type);
PersistentPropertyPathAccessor<T> accessor = entity.getPropertyPathAccessor(source);
return accessor;
}
@Data
@AllArgsConstructor
static class Customer {
String firstname;
}
@Value
static class Customers {
@Wither List<Customer> customers;
@Wither Map<String, Customer> customerMap;
}
@AllArgsConstructor
static class CustomerWrapper {
Customer customer;
}
}