From 869ffcdd11440b5ad9830e0cf33049da0fa42ac3 Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Sat, 10 Aug 2024 13:51:08 +0200 Subject: [PATCH 1/2] Allow for `null` and `empty` parameters in the MultiField annotation. Signed-off-by: Youssef Aouichaoui --- ...SimpleElasticsearchPersistentProperty.java | 7 +- .../NestedObjectIntegrationTests.java | 76 +++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java index 8ba4bffcc..8b32842b9 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java @@ -98,6 +98,7 @@ public SimpleElasticsearchPersistentProperty(Property property, this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType()); boolean isField = isAnnotationPresent(Field.class); + boolean isMultiField = isAnnotationPresent(MultiField.class); if (isVersionProperty() && !getType().equals(Long.class)) { throw new MappingException(String.format("Version property %s must be of type Long!", property.getName())); @@ -109,8 +110,10 @@ public SimpleElasticsearchPersistentProperty(Property property, initPropertyValueConverter(); - storeNullValue = isField && getRequiredAnnotation(Field.class).storeNullValue(); - storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() : true; + storeNullValue = isField ? getRequiredAnnotation(Field.class).storeNullValue() + : isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue(); + storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() + : !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue(); } @Override diff --git a/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java index 85b9c8a65..1815172ef 100644 --- a/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java @@ -15,6 +15,8 @@ */ package org.springframework.data.elasticsearch; +import static co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders.match; +import static java.util.UUID.randomUUID; import static org.assertj.core.api.Assertions.*; import static org.springframework.data.elasticsearch.utils.IdGenerator.*; @@ -28,6 +30,7 @@ import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -37,6 +40,7 @@ import org.springframework.data.elasticsearch.annotations.FieldType; import org.springframework.data.elasticsearch.annotations.InnerField; import org.springframework.data.elasticsearch.annotations.MultiField; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; import org.springframework.data.elasticsearch.core.ElasticsearchOperations; import org.springframework.data.elasticsearch.core.IndexOperations; import org.springframework.data.elasticsearch.core.SearchHits; @@ -373,6 +377,42 @@ public void shouldIndexAndSearchMapAsNestedType() { assertThat(books.getSearchHit(0).getContent().getId()).isEqualTo(book2.getId()); } + @Test // #2952 + @DisplayName("should handle null and empty field parameters in the mapping process") + void shouldSupportMappingNullAndEmptyFieldParameter() { + // Given + operations.indexOps(MultiFieldWithNullEmptyParameters.class).createWithMapping(); + List indexQueries = new ArrayList<>(); + MultiFieldWithNullEmptyParameters nullObj = new MultiFieldWithNullEmptyParameters(); + nullObj.addFieldWithInner(randomUUID().toString()); + MultiFieldWithNullEmptyParameters objWithValue = new MultiFieldWithNullEmptyParameters(); + objWithValue.addEmptyField(randomUUID().toString()); + + IndexQuery indexQuery1 = new IndexQuery(); + indexQuery1.setId(nextIdAsString()); + indexQuery1.setObject(nullObj); + indexQueries.add(indexQuery1); + + IndexQuery indexQuery2 = new IndexQuery(); + indexQuery2.setId(nextIdAsString()); + indexQuery2.setObject(objWithValue); + indexQueries.add(indexQuery2); + + // When + operations.bulkIndex(indexQueries, MultiFieldWithNullEmptyParameters.class); + + // Then + SearchHits nullResults = operations.search( + NativeQuery.builder().withQuery(match(bm -> bm.field("empty-field").query("EMPTY"))).build(), + MultiFieldWithNullEmptyParameters.class); + assertThat(nullResults.getSearchHits()).hasSize(1); + + nullResults = operations.search( + NativeQuery.builder().withQuery(match(bm -> bm.field("inner-field.prefix").query("EMPTY"))).build(), + MultiFieldWithNullEmptyParameters.class); + assertThat(nullResults.getSearchHits()).hasSize(1); + } + @NotNull abstract protected Query getNestedQuery4(); @@ -622,4 +662,40 @@ public void setName(@Nullable String name) { } } + @Document(indexName = "#{@indexNameProvider.indexName()}-multi-field") + static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField(mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", + storeNullValue = true)) private List emptyField; + + @Nullable + @MultiField(mainField = @Field(name = "inner-field", type = FieldType.Text, storeNullValue = true), + otherFields = { @InnerField(suffix = "prefix", type = FieldType.Keyword, + nullValue = "EMPTY") }) private List fieldWithInner; + + public List getEmptyField() { + if (emptyField == null) { + emptyField = new ArrayList<>(); + } + + return emptyField; + } + + public void addEmptyField(String value) { + getEmptyField().add(value); + } + + public List getFieldWithInner() { + if (fieldWithInner == null) { + fieldWithInner = new ArrayList<>(); + } + + return fieldWithInner; + } + + public void addFieldWithInner(@Nullable String value) { + getFieldWithInner().add(value); + } + } + } From 6702e5e7529d8861df0a88aec77d4ecd4b2435f6 Mon Sep 17 00:00:00 2001 From: Youssef Aouichaoui Date: Wed, 14 Aug 2024 12:39:13 +0200 Subject: [PATCH 2/2] Add unit tests. Signed-off-by: Youssef Aouichaoui --- .../core/index/MappingBuilderUnitTests.java | 41 ++++++++++++++++ .../ReactiveMappingBuilderUnitTests.java | 47 +++++++++++++++++++ 2 files changed, 88 insertions(+) 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 2335d2c61..eb6f96b08 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 @@ -1296,6 +1296,38 @@ void shouldUseCustomMappedNameMultiField() throws JSONException { assertEquals(expected, mapping, true); } + @Test // #2952 + void shouldMapNullityParameters() throws JSONException { + // Given + String expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "empty-field": { + "type": "keyword", + "null_value": "EMPTY", + "fields": { + "suffix": { + "type": "keyword", + "null_value": "EMPTY_TEXT" + } + } + } + } + } + """; + + // When + String result = getMappingBuilder().buildPropertyMapping(MultiFieldWithNullEmptyParameters.class); + + // Then + assertEquals(expected, result, true); + } + // region entities @Document(indexName = "ignore-above-index") @@ -2570,5 +2602,14 @@ private static class MultiFieldMappedNameEntity { @MultiField(mainField = @Field(type = FieldType.Text, mappedTypeName = "match_only_text"), otherFields = { @InnerField(suffix = "lower_case", type = FieldType.Keyword, normalizer = "lower_case_normalizer", mappedTypeName = "constant_keyword") }) private String description; } + + @SuppressWarnings("unused") + private static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField( + mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true), + otherFields = { + @InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List emptyField; + } // endregion } diff --git a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java index e7a193b03..82e250395 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java @@ -18,10 +18,14 @@ import static org.skyscreamer.jsonassert.JSONAssert.*; import static org.springframework.data.elasticsearch.annotations.FieldType.*; +import org.springframework.data.elasticsearch.annotations.FieldType; +import org.springframework.data.elasticsearch.annotations.InnerField; +import org.springframework.data.elasticsearch.annotations.MultiField; import reactor.core.publisher.Mono; import reactor.core.scheduler.Schedulers; import java.time.Instant; +import java.util.List; import org.json.JSONException; import org.junit.jupiter.api.DisplayName; @@ -78,6 +82,40 @@ void shouldWriteRuntimeFields() throws JSONException { assertEquals(expected, mapping, true); } + + @Test // #2952 + void shouldMapNullityParameters() throws JSONException { + // Given + ReactiveMappingBuilder mappingBuilder = getReactiveMappingBuilder(); + String expected = """ + { + "properties": { + "_class": { + "type": "keyword", + "index": false, + "doc_values": false + }, + "empty-field": { + "type": "keyword", + "null_value": "EMPTY", + "fields": { + "suffix": { + "type": "keyword", + "null_value": "EMPTY_TEXT" + } + } + } + } + } + """; + + // When + String result = Mono.defer(() -> mappingBuilder.buildReactivePropertyMapping(MultiFieldWithNullEmptyParameters.class)) + .subscribeOn(Schedulers.parallel()).block(); + + // Then + assertEquals(expected, result, true); + } // region entities @Document(indexName = "runtime-fields") @@ -88,5 +126,14 @@ private static class RuntimeFieldEntity { @Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp") @Nullable private Instant timestamp; } + + @SuppressWarnings("unused") + private static class MultiFieldWithNullEmptyParameters { + @Nullable + @MultiField( + mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true), + otherFields = { + @InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List emptyField; + } // endregion }