DATAREST-1440 - Revisited removal of fields in incoming payloads.

We now only remove fields from the payload in case there's no @JsonAnySetter on the target entity.
This commit is contained in:
Oliver Drotbohm
2019-11-04 09:09:59 +01:00
parent 8689c1bb5b
commit 28f8e0d397
3 changed files with 74 additions and 19 deletions

View File

@@ -227,7 +227,8 @@ public class DomainObjectReader {
JsonNode child = entry.getValue();
String fieldName = entry.getKey();
if (!mappedProperties.hasPersistentPropertyForField(fieldName)) {
if (!mappedProperties.isWritableProperty(fieldName)) {
i.remove();
continue;
}

View File

@@ -26,17 +26,16 @@ import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Predicate;
import org.springframework.data.mapping.PersistentEntity;
import org.springframework.data.mapping.PersistentProperty;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationConfig;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.introspect.BasicClassIntrospector;
import com.fasterxml.jackson.databind.introspect.BeanPropertyDefinition;
import com.fasterxml.jackson.databind.introspect.ClassIntrospector;
@@ -50,11 +49,11 @@ import com.fasterxml.jackson.databind.introspect.ClassIntrospector;
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
class MappedProperties {
private static final ClassIntrospector INTROSPECTOR = new BasicClassIntrospector();
private final Map<PersistentProperty<?>, BeanPropertyDefinition> propertyToFieldName;
private final Map<String, PersistentProperty<?>> fieldNameToProperty;
private final Set<BeanPropertyDefinition> unmappedProperties;
private final Set<String> ignoredPropertyNames;
private final boolean anySetterFound;
/**
* Creates a new {@link MappedProperties} instance for the given {@link PersistentEntity} and {@link BeanDescription}.
@@ -62,27 +61,33 @@ class MappedProperties {
* @param entity must not be {@literal null}.
* @param description must not be {@literal null}.
*/
private MappedProperties(PersistentEntity<?, ? extends PersistentProperty<?>> entity, BeanDescription description,
Predicate<PersistentProperty<?>> filter) {
private MappedProperties(PersistentEntity<?, ? extends PersistentProperty<?>> entity, BeanDescription description) {
Assert.notNull(entity, "Entity must not be null!");
Assert.notNull(description, "BeanDescription must not be null!");
this.propertyToFieldName = new HashMap<PersistentProperty<?>, BeanPropertyDefinition>();
this.fieldNameToProperty = new HashMap<String, PersistentProperty<?>>();
this.unmappedProperties = new HashSet<BeanPropertyDefinition>();
this.propertyToFieldName = new HashMap<>();
this.fieldNameToProperty = new HashMap<>();
this.unmappedProperties = new HashSet<>();
this.anySetterFound = description.findAnySetterAccessor() != null;
// We need to call this method after findAnySetterAccessor above as that triggers the
// collection of ignored properties in the first place. See
// https://github.com/FasterXML/jackson-databind/issues/2531
this.ignoredPropertyNames = description.getIgnoredPropertyNames();
for (BeanPropertyDefinition property : description.findProperties()) {
if (description.getIgnoredPropertyNames().contains(property.getName())) {
if (ignoredPropertyNames.contains(property.getName())) {
continue;
}
Optional<? extends PersistentProperty<?>> persistentProperty = //
Optional.ofNullable(entity.getPersistentProperty(property.getInternalName()));
persistentProperty//
.filter(filter) //
persistentProperty //
.ifPresent(it -> {
propertyToFieldName.put(it, property);
fieldNameToProperty.put(property.getName(), it);
@@ -105,10 +110,11 @@ class MappedProperties {
public static MappedProperties forDeserialization(PersistentEntity<?, ?> entity, ObjectMapper mapper) {
DeserializationConfig config = mapper.getDeserializationConfig();
BeanDescription description = INTROSPECTOR.forDeserialization(config, mapper.constructType(entity.getType()),
ClassIntrospector introspector = config.getClassIntrospector();
BeanDescription description = introspector.forDeserialization(config, mapper.constructType(entity.getType()),
config);
return new MappedProperties(entity, description, it -> it.isWritable());
return new MappedProperties(entity, description);
}
/**
@@ -122,13 +128,15 @@ class MappedProperties {
public static MappedProperties forSerialization(PersistentEntity<?, ?> entity, ObjectMapper mapper) {
SerializationConfig config = mapper.getSerializationConfig();
BeanDescription description = INTROSPECTOR.forSerialization(config, mapper.constructType(entity.getType()), config);
ClassIntrospector introspector = config.getClassIntrospector();
BeanDescription description = introspector.forSerialization(config, mapper.constructType(entity.getType()), config);
return new MappedProperties(entity, description, it -> true);
return new MappedProperties(entity, description);
}
public static MappedProperties none() {
return new MappedProperties(Collections.emptyMap(), Collections.emptyMap(), Collections.emptySet());
return new MappedProperties(Collections.emptyMap(), Collections.emptyMap(), Collections.emptySet(),
Collections.emptySet(), false);
}
/**
@@ -196,4 +204,24 @@ class MappedProperties {
return propertyToFieldName.containsKey(property);
}
/**
* Returns whether the property is actually writable. I.e. whether there's a non-read-only property on the target type
* or there's a catch all method annotated with {@link JsonAnySetter}.
*
* @param name must not be {@literal null} or empty.
* @return
*/
public boolean isWritableProperty(String name) {
Assert.hasText(name, "Property name must not be null or empty!");
if (ignoredPropertyNames.contains(name)) {
return false;
}
PersistentProperty<?> property = fieldNameToProperty.get(name);
return property != null ? property.isWritable() : anySetterFound;
}
}

View File

@@ -23,6 +23,7 @@ import org.springframework.data.annotation.Transient;
import org.springframework.data.keyvalue.core.mapping.context.KeyValueMappingContext;
import org.springframework.data.mapping.PersistentEntity;
import com.fasterxml.jackson.annotation.JsonAnySetter;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonProperty.Access;
@@ -90,7 +91,7 @@ public class MappedPropertiesUnitTests {
MappedProperties properties = MappedProperties.forDeserialization(entity, mapper);
assertThat(properties.hasPersistentPropertyForField("anotherReadOnlyProperty")).isFalse();
assertThat(properties.isWritableProperty("anotherReadOnlyProperty")).isFalse();
assertThat(properties.getPersistentProperty("readOnlyProperty")).isNull();
properties = MappedProperties.forSerialization(entity, mapper);
@@ -99,6 +100,21 @@ public class MappedPropertiesUnitTests {
assertThat(properties.getPersistentProperty("readOnlyProperty")).isNotNull();
}
@Test // DATAREST-1440
public void exposesExistanceOfCatchAllMethod() {
PersistentEntity<?, ?> entity = context.getRequiredPersistentEntity(SampleWithJsonAnySetter.class);
MappedProperties properties = MappedProperties.forDeserialization(entity, mapper);
assertThat(properties.isWritableProperty("someProperty")).isTrue();
assertThat(properties.isWritableProperty("readOnlyProperty")).isFalse();
assertThat(properties.isWritableProperty("anotherReadOnlyProperty")).isFalse();
// Due to @JsonAnySetter
assertThat(properties.isWritableProperty("someRandomProperty")).isTrue();
}
static class Sample {
public @Transient String notExposedBySpringData;
@@ -108,4 +124,14 @@ public class MappedPropertiesUnitTests {
public @JsonProperty(access = Access.READ_ONLY) String readOnlyProperty;
public @ReadOnlyProperty String anotherReadOnlyProperty;
}
static class SampleWithJsonAnySetter {
public String someProperty;
public @JsonProperty(access = Access.READ_ONLY) String readOnlyProperty;
public @ReadOnlyProperty String anotherReadOnlyProperty;
@JsonAnySetter
public void set(String key, String value) {}
}
}