diff --git a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java index a6145b98b..226779e3a 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/convert/MappingElasticsearchConverter.java @@ -23,7 +23,6 @@ import java.util.stream.Collectors; import org.slf4j.Logger; import org.slf4j.LoggerFactory; - import org.springframework.beans.BeansException; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; @@ -550,13 +549,13 @@ public class MappingElasticsearchConverter } Class entityType = ClassUtils.getUserClass(source.getClass()); - TypeInformation type = ClassTypeInformation.from(entityType); + TypeInformation typeInformation = ClassTypeInformation.from(entityType); if (requiresTypeHint(entityType)) { - typeMapper.writeType(type, sink); + typeMapper.writeType(typeInformation, sink); } - writeInternal(source, sink, type); + writeInternal(source, sink, typeInformation); } /** @@ -564,11 +563,11 @@ public class MappingElasticsearchConverter * * @param source * @param sink - * @param typeHint + * @param typeInformation */ @SuppressWarnings("unchecked") protected void writeInternal(@Nullable Object source, Map sink, - @Nullable TypeInformation typeHint) { + @Nullable TypeInformation typeInformation) { if (null == source) { return; @@ -594,7 +593,7 @@ public class MappingElasticsearchConverter } ElasticsearchPersistentEntity entity = mappingContext.getRequiredPersistentEntity(entityType); - addCustomTypeKeyIfNecessary(typeHint, source, sink); + addCustomTypeKeyIfNecessary(source, sink, typeInformation); writeInternal(source, sink, entity); } @@ -603,7 +602,7 @@ public class MappingElasticsearchConverter * * @param source * @param sink - * @param typeHint + * @param entity */ protected void writeInternal(@Nullable Object source, Map sink, @Nullable ElasticsearchPersistentEntity entity) { @@ -725,7 +724,7 @@ public class MappingElasticsearchConverter Map document = existingValue instanceof Map ? (Map) existingValue : Document.create(); - addCustomTypeKeyIfNecessary(ClassTypeInformation.from(property.getRawType()), value, document); + addCustomTypeKeyIfNecessary(value, document, ClassTypeInformation.from(property.getRawType())); writeInternal(value, document, entity); sink.set(property, document); } @@ -923,18 +922,18 @@ public class MappingElasticsearchConverter // region helper methods /** - * Adds custom type information to the given {@link Map} if necessary. That is if the value is not the same as the one - * given. This is usually the case if you store a subtype of the actual declared type of the property. + * Adds custom typeInformation information to the given {@link Map} if necessary. That is if the value is not the same + * as the one given. This is usually the case if you store a subtype of the actual declared typeInformation of the + * property. * - * @param type - * @param value must not be {@literal null}. + * @param source must not be {@literal null}. * @param sink must not be {@literal null}. + * @param type */ - protected void addCustomTypeKeyIfNecessary(@Nullable TypeInformation type, Object value, - Map sink) { + protected void addCustomTypeKeyIfNecessary(Object source, Map sink, @Nullable TypeInformation type) { Class reference = type != null ? type.getActualType().getType() : Object.class; - Class valueType = ClassUtils.getUserClass(value.getClass()); + Class valueType = ClassUtils.getUserClass(source.getClass()); boolean notTheSameClass = !valueType.equals(reference); if (notTheSameClass) { @@ -948,7 +947,7 @@ public class MappingElasticsearchConverter * @param type must not be {@literal null}. * @return {@literal true} if not a simple type, {@link Collection} or type with custom write target. */ - private boolean requiresTypeHint(Class type) { + public boolean requiresTypeHint(Class type) { return !isSimpleType(type) && !ClassUtils.isAssignable(Collection.class, type) && !conversions.hasCustomWriteTarget(type, Document.class); diff --git a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java index f2e2b02e1..fa984f1dd 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/index/MappingBuilder.java @@ -81,6 +81,8 @@ public class MappingBuilder { private static final String COMPLETION_MAX_INPUT_LENGTH = "max_input_length"; private static final String COMPLETION_CONTEXTS = "contexts"; + private static final String TYPEHINT_PROPERTY = "_class"; + private static final String TYPE_DYNAMIC = "dynamic"; private static final String TYPE_VALUE_KEYWORD = "keyword"; private static final String TYPE_VALUE_GEO_POINT = "geo_point"; @@ -131,6 +133,14 @@ public class MappingBuilder { } } + private void writeTypeHintMapping(XContentBuilder builder) throws IOException { + builder.startObject(TYPEHINT_PROPERTY) // + .field(FIELD_PARAM_TYPE, TYPE_VALUE_KEYWORD) // + .field(FIELD_PARAM_INDEX, false) // + .field(FIELD_PARAM_DOC_VALUES, false) // + .endObject(); + } + private void mapEntity(XContentBuilder builder, @Nullable ElasticsearchPersistentEntity entity, boolean isRootObject, String nestedObjectFieldName, boolean nestedOrObjectField, FieldType fieldType, @Nullable Field parentFieldAnnotation, @Nullable DynamicMapping dynamicMapping) throws IOException { @@ -162,6 +172,8 @@ public class MappingBuilder { builder.startObject(FIELD_PROPERTIES); + writeTypeHintMapping(builder); + if (entity != null) { entity.doWithProperties((PropertyHandler) property -> { try { diff --git a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperationsTest.java b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperationsTest.java index 9b034537c..10adb780a 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperationsTest.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/ReactiveIndexOperationsTest.java @@ -31,7 +31,6 @@ import org.json.JSONException; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONCompareMode; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; @@ -257,7 +256,7 @@ public class ReactiveIndexOperationsTest { .as(StepVerifier::create) // .assertNext(document -> { try { - assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(expected, document.toJson(), false); } catch (JSONException e) { fail("", e); } @@ -282,7 +281,7 @@ public class ReactiveIndexOperationsTest { .as(StepVerifier::create) // .assertNext(document -> { try { - assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(expected, document.toJson(), false); } catch (JSONException e) { fail("", e); } @@ -310,7 +309,7 @@ public class ReactiveIndexOperationsTest { .as(StepVerifier::create) // .assertNext(document -> { try { - assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(expected, document.toJson(), false); } catch (JSONException e) { fail("", e); } @@ -340,7 +339,7 @@ public class ReactiveIndexOperationsTest { .as(StepVerifier::create) // .assertNext(document -> { try { - assertEquals(expected, document.toJson(), JSONCompareMode.NON_EXTENSIBLE); + assertEquals(expected, document.toJson(), false); } catch (JSONException e) { fail("", e); } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java index 76b11d8aa..aa46971ca 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java @@ -37,9 +37,7 @@ import java.time.LocalDate; import java.util.Collection; import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Set; import org.elasticsearch.search.suggest.completion.context.ContextMapping; import org.json.JSONException; @@ -420,7 +418,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { String mapping = getMappingBuilder().buildPropertyMapping(FieldMappingParameters.class); // then - assertEquals(expected, mapping, true); + assertEquals(expected, mapping, false); } @Test @@ -439,7 +437,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { String mapping = getMappingBuilder().buildPropertyMapping(ConfigureDynamicMappingEntity.class); - assertEquals(expected, mapping, true); + assertEquals(expected, mapping, false); } @Test // DATAES-784 @@ -454,7 +452,7 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { String mapping = getMappingBuilder().buildPropertyMapping(ValueDoc.class); - assertEquals(expected, mapping, true); + assertEquals(expected, mapping, false); } @Test // DATAES-788 @@ -568,6 +566,54 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { .isInstanceOf(MappingException.class); } + @Test // #1711 + @DisplayName("should write typeHint entries") + void shouldWriteTypeHintEntries() throws JSONException { + + String expected = "{\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"id\": {\n" + // + " \"type\": \"keyword\"\n" + // + " },\n" + // + " \"nestedEntity\": {\n" + // + " \"type\": \"nested\",\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"nestedField\": {\n" + // + " \"type\": \"text\"\n" + // + " }\n" + // + " }\n" + // + " },\n" + // + " \"objectEntity\": {\n" + // + " \"type\": \"object\",\n" + // + " \"properties\": {\n" + // + " \"_class\": {\n" + // + " \"type\": \"keyword\",\n" + // + " \"index\": false,\n" + // + " \"doc_values\": false\n" + // + " },\n" + // + " \"objectField\": {\n" + // + " \"type\": \"text\"\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + " }\n" + // + "}\n"; // + + String mapping = getMappingBuilder().buildPropertyMapping(TypeHintEntity.class); + + assertEquals(expected, mapping, false); + } + @Setter @Getter @NoArgsConstructor @@ -862,21 +908,6 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { orientation = GeoShapeField.Orientation.clockwise) private String shape2; } - @Document(indexName = "test-index-user-mapping-builder") - static class User { - @Nullable @Id private String id; - - @Field(type = FieldType.Nested, ignoreFields = { "users" }) private Set groups = new HashSet<>(); - } - - @Document(indexName = "test-index-group-mapping-builder") - static class Group { - - @Nullable @Id String id; - - @Field(type = FieldType.Nested, ignoreFields = { "groups" }) private Set users = new HashSet<>(); - } - @Document(indexName = "test-index-field-mapping-parameters") static class FieldMappingParameters { @Nullable @Field private String indexTrue; @@ -1008,4 +1039,25 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests { @Field(type = Text) private String text; @Mapping(enabled = false) @Field(type = Object) private Object object; } + + @Data + @AllArgsConstructor + @NoArgsConstructor + static class TypeHintEntity { + @Id @Field(type = Keyword) private String id; + + @Field(type = Nested) private NestedEntity nestedEntity; + + @Field(type = Object) private ObjectEntity objectEntity; + + @Data + static class NestedEntity { + @Field(type = Text) private String nestedField; + } + + @Data + static class ObjectEntity { + @Field(type = Text) private String objectField; + } + } } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java index aa427ed41..d4a134ea9 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleDynamicTemplatesMappingTests.java @@ -15,11 +15,12 @@ */ package org.springframework.data.elasticsearch.core.index; -import static org.assertj.core.api.Assertions.*; +import static org.skyscreamer.jsonassert.JSONAssert.*; import java.util.HashMap; import java.util.Map; +import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; @@ -38,7 +39,7 @@ import org.springframework.lang.Nullable; public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests { @Test // DATAES-568 - public void testCorrectDynamicTemplatesMappings() { + public void testCorrectDynamicTemplatesMappings() throws JSONException { String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntity.class); @@ -46,11 +47,11 @@ public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests + "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"}," + "\"path_match\":\"names.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}"; - assertThat(mapping).isEqualTo(EXPECTED_MAPPING_ONE); + assertEquals(EXPECTED_MAPPING_ONE, mapping, false); } @Test // DATAES-568 - public void testCorrectDynamicTemplatesMappingsTwo() { + public void testCorrectDynamicTemplatesMappingsTwo() throws JSONException { String mapping = getMappingBuilder().buildPropertyMapping(SampleDynamicTemplatesEntityTwo.class); String EXPECTED_MAPPING_TWO = "{\"dynamic_templates\":" + "[{\"with_custom_analyzer\":{" @@ -59,7 +60,7 @@ public class SimpleDynamicTemplatesMappingTests extends MappingContextBaseTests + "\"mapping\":{\"type\":\"string\",\"analyzer\":\"standard_lowercase_asciifolding\"}," + "\"path_match\":\"participantA1.*\"}}]," + "\"properties\":{\"names\":{\"type\":\"object\"}}}"; - assertThat(mapping).isEqualTo(EXPECTED_MAPPING_TWO); + assertEquals(EXPECTED_MAPPING_TWO, mapping, false); } /** diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java index e2248c20d..4349b8572 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/SimpleElasticsearchDateMappingTests.java @@ -15,13 +15,14 @@ */ package org.springframework.data.elasticsearch.core.index; -import static org.assertj.core.api.Assertions.*; +import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; import lombok.Data; import java.time.LocalDateTime; +import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.DateFormat; @@ -43,11 +44,11 @@ public class SimpleElasticsearchDateMappingTests extends MappingContextBaseTests + "\"basicFormatDate\":{\"" + "type\":\"date\",\"format\":\"basic_date\"}}}"; @Test // DATAES-568, DATAES-828 - public void testCorrectDateMappings() { + public void testCorrectDateMappings() throws JSONException { String mapping = getMappingBuilder().buildPropertyMapping(SampleDateMappingEntity.class); - assertThat(mapping).isEqualTo(EXPECTED_MAPPING); + assertEquals(EXPECTED_MAPPING, mapping, false); } /**