Skip to content

Commit 9149c1b

Browse files
authored
Allow for null and empty parameters in the MultiField annotation.
Original Pull Request #2960 Closes #2952
1 parent d079a59 commit 9149c1b

File tree

4 files changed

+169
-2
lines changed

4 files changed

+169
-2
lines changed

src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentProperty.java

+5-2
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public SimpleElasticsearchPersistentProperty(Property property,
9898
this.isSeqNoPrimaryTerm = SeqNoPrimaryTerm.class.isAssignableFrom(getRawType());
9999

100100
boolean isField = isAnnotationPresent(Field.class);
101+
boolean isMultiField = isAnnotationPresent(MultiField.class);
101102

102103
if (isVersionProperty() && !getType().equals(Long.class)) {
103104
throw new MappingException(String.format("Version property %s must be of type Long!", property.getName()));
@@ -109,8 +110,10 @@ public SimpleElasticsearchPersistentProperty(Property property,
109110

110111
initPropertyValueConverter();
111112

112-
storeNullValue = isField && getRequiredAnnotation(Field.class).storeNullValue();
113-
storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue() : true;
113+
storeNullValue = isField ? getRequiredAnnotation(Field.class).storeNullValue()
114+
: isMultiField && getRequiredAnnotation(MultiField.class).mainField().storeNullValue();
115+
storeEmptyValue = isField ? getRequiredAnnotation(Field.class).storeEmptyValue()
116+
: !isMultiField || getRequiredAnnotation(MultiField.class).mainField().storeEmptyValue();
114117
}
115118

116119
@Override

src/test/java/org/springframework/data/elasticsearch/NestedObjectIntegrationTests.java

+76
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@
1515
*/
1616
package org.springframework.data.elasticsearch;
1717

18+
import static co.elastic.clients.elasticsearch._types.query_dsl.QueryBuilders.match;
19+
import static java.util.UUID.randomUUID;
1820
import static org.assertj.core.api.Assertions.*;
1921
import static org.springframework.data.elasticsearch.utils.IdGenerator.*;
2022

@@ -28,6 +30,7 @@
2830

2931
import org.jetbrains.annotations.NotNull;
3032
import org.junit.jupiter.api.BeforeEach;
33+
import org.junit.jupiter.api.DisplayName;
3134
import org.junit.jupiter.api.Order;
3235
import org.junit.jupiter.api.Test;
3336
import org.springframework.beans.factory.annotation.Autowired;
@@ -37,6 +40,7 @@
3740
import org.springframework.data.elasticsearch.annotations.FieldType;
3841
import org.springframework.data.elasticsearch.annotations.InnerField;
3942
import org.springframework.data.elasticsearch.annotations.MultiField;
43+
import org.springframework.data.elasticsearch.client.elc.NativeQuery;
4044
import org.springframework.data.elasticsearch.core.ElasticsearchOperations;
4145
import org.springframework.data.elasticsearch.core.IndexOperations;
4246
import org.springframework.data.elasticsearch.core.SearchHits;
@@ -373,6 +377,42 @@ public void shouldIndexAndSearchMapAsNestedType() {
373377
assertThat(books.getSearchHit(0).getContent().getId()).isEqualTo(book2.getId());
374378
}
375379

380+
@Test // #2952
381+
@DisplayName("should handle null and empty field parameters in the mapping process")
382+
void shouldSupportMappingNullAndEmptyFieldParameter() {
383+
// Given
384+
operations.indexOps(MultiFieldWithNullEmptyParameters.class).createWithMapping();
385+
List<IndexQuery> indexQueries = new ArrayList<>();
386+
MultiFieldWithNullEmptyParameters nullObj = new MultiFieldWithNullEmptyParameters();
387+
nullObj.addFieldWithInner(randomUUID().toString());
388+
MultiFieldWithNullEmptyParameters objWithValue = new MultiFieldWithNullEmptyParameters();
389+
objWithValue.addEmptyField(randomUUID().toString());
390+
391+
IndexQuery indexQuery1 = new IndexQuery();
392+
indexQuery1.setId(nextIdAsString());
393+
indexQuery1.setObject(nullObj);
394+
indexQueries.add(indexQuery1);
395+
396+
IndexQuery indexQuery2 = new IndexQuery();
397+
indexQuery2.setId(nextIdAsString());
398+
indexQuery2.setObject(objWithValue);
399+
indexQueries.add(indexQuery2);
400+
401+
// When
402+
operations.bulkIndex(indexQueries, MultiFieldWithNullEmptyParameters.class);
403+
404+
// Then
405+
SearchHits<MultiFieldWithNullEmptyParameters> nullResults = operations.search(
406+
NativeQuery.builder().withQuery(match(bm -> bm.field("empty-field").query("EMPTY"))).build(),
407+
MultiFieldWithNullEmptyParameters.class);
408+
assertThat(nullResults.getSearchHits()).hasSize(1);
409+
410+
nullResults = operations.search(
411+
NativeQuery.builder().withQuery(match(bm -> bm.field("inner-field.prefix").query("EMPTY"))).build(),
412+
MultiFieldWithNullEmptyParameters.class);
413+
assertThat(nullResults.getSearchHits()).hasSize(1);
414+
}
415+
376416
@NotNull
377417
abstract protected Query getNestedQuery4();
378418

@@ -622,4 +662,40 @@ public void setName(@Nullable String name) {
622662
}
623663
}
624664

665+
@Document(indexName = "#{@indexNameProvider.indexName()}-multi-field")
666+
static class MultiFieldWithNullEmptyParameters {
667+
@Nullable
668+
@MultiField(mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY",
669+
storeNullValue = true)) private List<String> emptyField;
670+
671+
@Nullable
672+
@MultiField(mainField = @Field(name = "inner-field", type = FieldType.Text, storeNullValue = true),
673+
otherFields = { @InnerField(suffix = "prefix", type = FieldType.Keyword,
674+
nullValue = "EMPTY") }) private List<String> fieldWithInner;
675+
676+
public List<String> getEmptyField() {
677+
if (emptyField == null) {
678+
emptyField = new ArrayList<>();
679+
}
680+
681+
return emptyField;
682+
}
683+
684+
public void addEmptyField(String value) {
685+
getEmptyField().add(value);
686+
}
687+
688+
public List<String> getFieldWithInner() {
689+
if (fieldWithInner == null) {
690+
fieldWithInner = new ArrayList<>();
691+
}
692+
693+
return fieldWithInner;
694+
}
695+
696+
public void addFieldWithInner(@Nullable String value) {
697+
getFieldWithInner().add(value);
698+
}
699+
}
700+
625701
}

src/test/java/org/springframework/data/elasticsearch/core/index/MappingBuilderUnitTests.java

+41
Original file line numberDiff line numberDiff line change
@@ -1296,6 +1296,38 @@ void shouldUseCustomMappedNameMultiField() throws JSONException {
12961296
assertEquals(expected, mapping, true);
12971297
}
12981298

1299+
@Test // #2952
1300+
void shouldMapNullityParameters() throws JSONException {
1301+
// Given
1302+
String expected = """
1303+
{
1304+
"properties": {
1305+
"_class": {
1306+
"type": "keyword",
1307+
"index": false,
1308+
"doc_values": false
1309+
},
1310+
"empty-field": {
1311+
"type": "keyword",
1312+
"null_value": "EMPTY",
1313+
"fields": {
1314+
"suffix": {
1315+
"type": "keyword",
1316+
"null_value": "EMPTY_TEXT"
1317+
}
1318+
}
1319+
}
1320+
}
1321+
}
1322+
""";
1323+
1324+
// When
1325+
String result = getMappingBuilder().buildPropertyMapping(MultiFieldWithNullEmptyParameters.class);
1326+
1327+
// Then
1328+
assertEquals(expected, result, true);
1329+
}
1330+
12991331
// region entities
13001332

13011333
@Document(indexName = "ignore-above-index")
@@ -2570,5 +2602,14 @@ private static class MultiFieldMappedNameEntity {
25702602
@MultiField(mainField = @Field(type = FieldType.Text, mappedTypeName = "match_only_text"), otherFields = { @InnerField(suffix = "lower_case",
25712603
type = FieldType.Keyword, normalizer = "lower_case_normalizer", mappedTypeName = "constant_keyword") }) private String description;
25722604
}
2605+
2606+
@SuppressWarnings("unused")
2607+
private static class MultiFieldWithNullEmptyParameters {
2608+
@Nullable
2609+
@MultiField(
2610+
mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true),
2611+
otherFields = {
2612+
@InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List<String> emptyField;
2613+
}
25732614
// endregion
25742615
}

src/test/java/org/springframework/data/elasticsearch/core/index/ReactiveMappingBuilderUnitTests.java

+47
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,14 @@
1818
import static org.skyscreamer.jsonassert.JSONAssert.*;
1919
import static org.springframework.data.elasticsearch.annotations.FieldType.*;
2020

21+
import org.springframework.data.elasticsearch.annotations.FieldType;
22+
import org.springframework.data.elasticsearch.annotations.InnerField;
23+
import org.springframework.data.elasticsearch.annotations.MultiField;
2124
import reactor.core.publisher.Mono;
2225
import reactor.core.scheduler.Schedulers;
2326

2427
import java.time.Instant;
28+
import java.util.List;
2529

2630
import org.json.JSONException;
2731
import org.junit.jupiter.api.DisplayName;
@@ -78,6 +82,40 @@ void shouldWriteRuntimeFields() throws JSONException {
7882

7983
assertEquals(expected, mapping, true);
8084
}
85+
86+
@Test // #2952
87+
void shouldMapNullityParameters() throws JSONException {
88+
// Given
89+
ReactiveMappingBuilder mappingBuilder = getReactiveMappingBuilder();
90+
String expected = """
91+
{
92+
"properties": {
93+
"_class": {
94+
"type": "keyword",
95+
"index": false,
96+
"doc_values": false
97+
},
98+
"empty-field": {
99+
"type": "keyword",
100+
"null_value": "EMPTY",
101+
"fields": {
102+
"suffix": {
103+
"type": "keyword",
104+
"null_value": "EMPTY_TEXT"
105+
}
106+
}
107+
}
108+
}
109+
}
110+
""";
111+
112+
// When
113+
String result = Mono.defer(() -> mappingBuilder.buildReactivePropertyMapping(MultiFieldWithNullEmptyParameters.class))
114+
.subscribeOn(Schedulers.parallel()).block();
115+
116+
// Then
117+
assertEquals(expected, result, true);
118+
}
81119

82120
// region entities
83121
@Document(indexName = "runtime-fields")
@@ -88,5 +126,14 @@ private static class RuntimeFieldEntity {
88126
@Field(type = Date, format = DateFormat.epoch_millis, name = "@timestamp")
89127
@Nullable private Instant timestamp;
90128
}
129+
130+
@SuppressWarnings("unused")
131+
private static class MultiFieldWithNullEmptyParameters {
132+
@Nullable
133+
@MultiField(
134+
mainField = @Field(name = "empty-field", type = FieldType.Keyword, nullValue = "EMPTY", storeNullValue = true),
135+
otherFields = {
136+
@InnerField(suffix = "suffix", type = Keyword, nullValue = "EMPTY_TEXT") }) private List<String> emptyField;
137+
}
91138
// endregion
92139
}

0 commit comments

Comments
 (0)