DATAES-464 - DefaultEntityWriter now considers read-only and transient properties.

We now register a custom Jackson module that filters read-only and transient properties for serialization.
This commit is contained in:
Oliver Gierke 2018-06-13 18:21:37 +02:00
parent ac62aaf856
commit 62a03a8fb7
3 changed files with 157 additions and 34 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2014 the original author or authors.
* Copyright 2014-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.
@ -16,52 +16,142 @@
package org.springframework.data.elasticsearch.core;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.*;
import com.fasterxml.jackson.databind.*;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.elasticsearch.core.geo.CustomGeoModule;
import org.springframework.data.geo.*;
import java.util.List;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.BeanDescription;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.BeanPropertyWriter;
import com.fasterxml.jackson.databind.ser.BeanSerializerModifier;
/**
* DocumentMapper using jackson
* EntityMapper based on a Jackson {@link ObjectMapper}.
*
* @author Artur Konczak
* @author Petar Tahchiev
* @author Oliver Gierke
*/
public class DefaultEntityMapper implements EntityMapper {
private ObjectMapper objectMapper;
public DefaultEntityMapper() {
/**
* Creates a new {@link DefaultEntityMapper} using the given {@link MappingContext}.
*
* @param context must not be {@literal null}.
*/
public DefaultEntityMapper(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
Assert.notNull(context, "MappingContext must not be null!");
objectMapper = new ObjectMapper();
objectMapper.registerModule(new SpringDataElasticsearchModule(context));
objectMapper.registerModule(new CustomGeoModule());
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(DeserializationFeature.ACCEPT_SINGLE_VALUE_AS_ARRAY, true);
objectMapper.registerModule(new CustomGeoModule());
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityMapper#mapToString(java.lang.Object)
*/
@Override
public String mapToString(Object object) throws IOException {
return objectMapper.writeValueAsString(object);
}
/*
* (non-Javadoc)
* @see org.springframework.data.elasticsearch.core.EntityMapper#mapToObject(java.lang.String, java.lang.Class)
*/
@Override
public <T> T mapToObject(String source, Class<T> clazz) throws IOException {
return objectMapper.readValue(source, clazz);
}
/**
* A simple Jackson module to register the {@link SpringDataSerializerModifier}.
*
* @author Oliver Gierke
* @since 3.1
*/
private static class SpringDataElasticsearchModule extends SimpleModule {
private static final long serialVersionUID = -9168968092458058966L;
/**
* Creates a new {@link SpringDataElasticsearchModule} using the given {@link MappingContext}.
*
* @param context must not be {@literal null}.
*/
public SpringDataElasticsearchModule(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
Assert.notNull(context, "MappingContext must not be null!");
setSerializerModifier(new SpringDataSerializerModifier(context));
}
/**
* A {@link BeanSerializerModifier} that will drop properties annotated with {@link ReadOnlyProperty} for
* serialization.
*
* @author Oliver Gierke
* @since 3.1
*/
private static class SpringDataSerializerModifier extends BeanSerializerModifier {
private final MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context;
public SpringDataSerializerModifier(
MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> context) {
Assert.notNull(context, "MappingContext must not be null!");
this.context = context;
}
/*
* (non-Javadoc)
* @see com.fasterxml.jackson.databind.ser.BeanSerializerModifier#changeProperties(com.fasterxml.jackson.databind.SerializationConfig, com.fasterxml.jackson.databind.BeanDescription, java.util.List)
*/
@Override
public List<BeanPropertyWriter> changeProperties(SerializationConfig config, BeanDescription description,
List<BeanPropertyWriter> properties) {
Class<?> type = description.getBeanClass();
ElasticsearchPersistentEntity<?> entity = context.getPersistentEntity(type);
if (entity == null) {
return super.changeProperties(config, description, properties);
}
List<BeanPropertyWriter> result = new ArrayList<>(properties.size());
for (BeanPropertyWriter beanPropertyWriter : properties) {
ElasticsearchPersistentProperty property = entity.getPersistentProperty(beanPropertyWriter.getName());
if (property != null && property.isWritable()) {
result.add(beanPropertyWriter);
}
}
return result;
}
}
}
}

View File

@ -38,6 +38,7 @@ import org.springframework.data.elasticsearch.core.aggregation.AggregatedPage;
import org.springframework.data.elasticsearch.core.aggregation.impl.AggregatedPageImpl;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentEntity;
import org.springframework.data.elasticsearch.core.mapping.ElasticsearchPersistentProperty;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.util.Assert;
@ -59,11 +60,11 @@ public class DefaultResultMapper extends AbstractResultMapper {
private MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext;
public DefaultResultMapper() {
super(new DefaultEntityMapper());
this(new SimpleElasticsearchMappingContext());
}
public DefaultResultMapper(MappingContext<? extends ElasticsearchPersistentEntity<?>, ElasticsearchPersistentProperty> mappingContext) {
super(new DefaultEntityMapper());
super(new DefaultEntityMapper(mappingContext));
this.mappingContext = mappingContext;
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2013-2014 the original author or authors.
* Copyright 2013-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.
@ -15,15 +15,17 @@
*/
package org.springframework.data.elasticsearch.core;
import static org.hamcrest.Matchers.*;
import static org.junit.Assert.*;
import static org.assertj.core.api.Assertions.*;
import java.io.IOException;
import java.util.Locale;
import org.junit.Before;
import org.junit.Test;
import org.springframework.data.annotation.ReadOnlyProperty;
import org.springframework.data.annotation.Transient;
import org.springframework.data.elasticsearch.core.geo.GeoPoint;
import org.springframework.data.elasticsearch.core.mapping.SimpleElasticsearchMappingContext;
import org.springframework.data.elasticsearch.entities.Car;
import org.springframework.data.elasticsearch.entities.GeoEntity;
import org.springframework.data.geo.Point;
@ -31,6 +33,7 @@ import org.springframework.data.geo.Point;
/**
* @author Artur Konczak
* @author Mohsin Husen
* @author Oliver Gierke
*/
public class DefaultEntityMapperTests {
@ -41,7 +44,7 @@ public class DefaultEntityMapperTests {
@Before
public void init() {
entityMapper = new DefaultEntityMapper();
entityMapper = new DefaultEntityMapper(new SimpleElasticsearchMappingContext());
}
@Test
@ -52,7 +55,7 @@ public class DefaultEntityMapperTests {
String jsonResult = entityMapper.mapToString(Car.builder().model(CAR_MODEL).name(CAR_NAME).build());
//Then
assertThat(jsonResult, is(JSON_STRING));
assertThat(jsonResult).isEqualTo(JSON_STRING);
}
@Test
@ -63,15 +66,14 @@ public class DefaultEntityMapperTests {
Car result = entityMapper.mapToObject(JSON_STRING, Car.class);
//Then
assertThat(result.getName(), is(CAR_NAME));
assertThat(result.getModel(), is(CAR_MODEL));
assertThat(result.getName()).isEqualTo(CAR_NAME);
assertThat(result.getModel()).isEqualTo(CAR_MODEL);
}
@Test
public void shouldMapGeoPointElasticsearchNames() throws IOException {
//given
final Point point = new Point(10, 20);
final int radius = 10;
final String pointAsString = point.getX() + "," + point.getY();
final double[] pointAsArray = {point.getX(), point.getY()};
final GeoEntity geoEntity = GeoEntity.builder()
@ -81,13 +83,43 @@ public class DefaultEntityMapperTests {
String jsonResult = entityMapper.mapToString(geoEntity);
//then
assertThat(jsonResult, containsString(pointTemplate("pointA", point)));
assertThat(jsonResult, containsString(pointTemplate("pointB", point)));
assertThat(jsonResult, containsString(String.format(Locale.ENGLISH, "\"%s\":\"%s\"", "pointC", pointAsString)));
assertThat(jsonResult, containsString(String.format(Locale.ENGLISH, "\"%s\":[%.1f,%.1f]", "pointD", pointAsArray[0], pointAsArray[1])));
assertThat(jsonResult).contains(pointTemplate("pointA", point));
assertThat(jsonResult).contains(pointTemplate("pointB", point));
assertThat(jsonResult).contains(String.format(Locale.ENGLISH, "\"%s\":\"%s\"", "pointC", pointAsString));
assertThat(jsonResult).contains(String.format(Locale.ENGLISH, "\"%s\":[%.1f,%.1f]", "pointD", pointAsArray[0], pointAsArray[1]));
}
@Test // DATAES-464
public void ignoresReadOnlyProperties() throws IOException {
// given
Sample sample = new Sample();
sample.readOnly = "readOnly";
sample.property = "property";
sample.transientProperty = "transient";
sample.annotatedTransientProperty = "transient";
// when
String result = entityMapper.mapToString(sample);
// then
assertThat(result).contains("\"property\"");
assertThat(result).doesNotContain("readOnly");
assertThat(result).doesNotContain("transientProperty");
assertThat(result).doesNotContain("annotatedTransientProperty");
}
private String pointTemplate(String name, Point point) {
return String.format(Locale.ENGLISH, "\"%s\":{\"lat\":%.1f,\"lon\":%.1f}", name, point.getX(), point.getY());
}
public static class Sample {
public @ReadOnlyProperty String readOnly;
public @Transient String annotatedTransientProperty;
public transient String transientProperty;
public String property;
}
}