diff --git a/src/main/java/org/springframework/data/elasticsearch/core/DefaultEntityMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/DefaultEntityMapper.java index f96c37c9f..54d732459 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DefaultEntityMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DefaultEntityMapper.java @@ -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, 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 mapToObject(String source, Class 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, 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, ElasticsearchPersistentProperty> context; + + public SpringDataSerializerModifier( + MappingContext, 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 changeProperties(SerializationConfig config, BeanDescription description, + List properties) { + + Class type = description.getBeanClass(); + ElasticsearchPersistentEntity entity = context.getPersistentEntity(type); + + if (entity == null) { + return super.changeProperties(config, description, properties); + } + + List 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; + } + } + } } diff --git a/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java b/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java index 0c334b5d9..5932027a6 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/DefaultResultMapper.java @@ -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, ElasticsearchPersistentProperty> mappingContext; public DefaultResultMapper() { - super(new DefaultEntityMapper()); + this(new SimpleElasticsearchMappingContext()); } public DefaultResultMapper(MappingContext, ElasticsearchPersistentProperty> mappingContext) { - super(new DefaultEntityMapper()); + super(new DefaultEntityMapper(mappingContext)); this.mappingContext = mappingContext; } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/DefaultEntityMapperTests.java b/src/test/java/org/springframework/data/elasticsearch/core/DefaultEntityMapperTests.java index 0f1ad3abf..b07cb1eb4 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/DefaultEntityMapperTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/DefaultEntityMapperTests.java @@ -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; + } }