Skip to content

Allow for null and empty parameters in the MultiField annotation. #2960

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Aug 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand All @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<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();

Expand Down Expand Up @@ -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<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);
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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<String> emptyField;
}
// endregion
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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")
Expand All @@ -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<String> emptyField;
}
// endregion
}