Allow for null and empty parameters in the MultiField annotation.

Original Pull Request #2960
Closes #2952
This commit is contained in:
Aouichaoui Youssef 2024-08-19 20:06:56 +02:00 committed by GitHub
parent d079a59cb4
commit 9149c1bc2e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 169 additions and 2 deletions

View File

@ -98,6 +98,7 @@ public class SimpleElasticsearchPersistentProperty extends
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 class SimpleElasticsearchPersistentProperty extends
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

View File

@ -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 java.util.Map;
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.Field;
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 abstract class NestedObjectIntegrationTests {
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<IndexQuery> 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<MultiFieldWithNullEmptyParameters> 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 abstract class NestedObjectIntegrationTests {
}
}
@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<String> emptyField;
@Nullable
@MultiField(mainField = @Field(name = "inner-field", type = FieldType.Text, storeNullValue = true),
otherFields = { @InnerField(suffix = "prefix", type = FieldType.Keyword,
nullValue = "EMPTY") }) private List<String> fieldWithInner;
public List<String> getEmptyField() {
if (emptyField == null) {
emptyField = new ArrayList<>();
}
return emptyField;
}
public void addEmptyField(String value) {
getEmptyField().add(value);
}
public List<String> getFieldWithInner() {
if (fieldWithInner == null) {
fieldWithInner = new ArrayList<>();
}
return fieldWithInner;
}
public void addFieldWithInner(@Nullable String value) {
getFieldWithInner().add(value);
}
}
}

View File

@ -1296,6 +1296,38 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
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 @@ public class MappingBuilderUnitTests extends MappingContextBaseTests {
@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<String> emptyField;
}
// endregion
}

View File

@ -18,10 +18,14 @@ package org.springframework.data.elasticsearch.core.index;
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;
@ -79,6 +83,40 @@ public class ReactiveMappingBuilderUnitTests extends MappingContextBaseTests {
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")
@Mapping(runtimeFieldsPath = "/mappings/runtime-fields.json")
@ -88,5 +126,14 @@ public class ReactiveMappingBuilderUnitTests extends MappingContextBaseTests {
@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<String> emptyField;
}
// endregion
}