Add SpEL support for settingPath in @Settings annotation.

Original Pull Request #3188
Closes #3187
Signed-off-by: Peter-Josef Meisch <pj.meisch@sothawo.com>
This commit is contained in:
Peter-Josef Meisch 2025-10-26 18:25:56 +01:00 committed by GitHub
parent b552128198
commit 21bc62b78c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 151 additions and 23 deletions

View File

@ -4,10 +4,11 @@
[[new-features.6-0-0]]
== New in Spring Data Elasticsearch 6.0
* Upgarde to Spring 7
* Upgrade to Spring 7
* Switch to jspecify nullability annotations
* Upgrade to Elasticsearch 9.1.5
* Use the new Elasticsearch Rest5Client as default
* Add support for SpEL expressions in the `settingPath` parameter of the `@Setting` annotation
[[new-features.5-5-0]]

View File

@ -47,6 +47,7 @@ import org.springframework.data.util.TypeInformation;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
@ -298,7 +299,7 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
Assert.notNull(fieldName, "fieldName must not be null");
return fieldNamePropertyCache.computeIfAbsent(fieldName, key -> {
AtomicReference<ElasticsearchPersistentProperty> propertyRef = new AtomicReference<>();
AtomicReference<@Nullable ElasticsearchPersistentProperty> propertyRef = new AtomicReference<>();
doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> {
if (key.equals(property.getFieldName())) {
propertyRef.set(property);
@ -423,9 +424,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
try {
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression);
ExpressionDependencies expressionDependencies = ExpressionDependencies.discover(expression);
ExpressionDependencies expressionDependencies = expression != null ? ExpressionDependencies.discover(expression)
: ExpressionDependencies.none();
// noinspection ConstantConditions
EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean);
@ -440,8 +441,20 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
// region index settings
@Override
public String settingPath() {
return settingsParameter.get().settingPath;
public @Nullable String settingPath() {
String settingPathFromParameter = settingsParameter.get().settingPath;
if (settingPathFromParameter == null) {
return null;
}
try {
Expression expression = PARSER.parseExpression(settingPathFromParameter, ParserContext.TEMPLATE_EXPRESSION);
return (expression instanceof LiteralExpression) ? settingPathFromParameter
: expression.getValue(getEvaluationContext(null, ExpressionDependencies.discover(expression)), String.class);
} catch (ExpressionException e) {
throw new InvalidDataAccessApiUsageException(
"Could not resolve expression: " + settingPathFromParameter + " for @Setting.settingPath ", e);
}
}
@Override

View File

@ -84,7 +84,7 @@ public class QueryStringSpELEvaluator {
if (expr != null) {
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getValues())
.getRequiredEvaluationContext();
.getEvaluationContext();
if (context instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter);

View File

@ -0,0 +1,17 @@
package org.springframework.data.elasticsearch.core;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.elasticsearch.junit.jupiter.ElasticsearchTemplateConfiguration;
public class IndexSettingsELCIntegrationTests extends IndexSettingsIntegrationTests {
@Configuration
@Import({ ElasticsearchTemplateConfiguration.class })
static class Config {
@Bean
public SpelSettingPath spelSettingPath() {
return new SpelSettingPath();
}
}
}

View File

@ -0,0 +1,47 @@
package org.springframework.data.elasticsearch.core;
import static org.assertj.core.api.Assertions.*;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Setting;
import org.springframework.data.elasticsearch.junit.jupiter.SpringIntegrationTest;
/**
* IndexSettings test that need an regular conext setup for SpEL resolution for example.
*/
@SpringIntegrationTest
public abstract class IndexSettingsIntegrationTests {
@Autowired protected ElasticsearchOperations operations;
@Test // #3187
@DisplayName("should evaluate SpEL expression in settingPath")
void shouldEvaluateSpElExpressionInSettingPath() {
var settingPath = operations.getElasticsearchConverter().getMappingContext()
.getRequiredPersistentEntity(SettingPathWithSpel.class).settingPath();
assertThat(settingPath).isEqualTo(SpelSettingPath.SETTING_PATH);
}
protected static class SpelSettingPath {
public static String SETTING_PATH = "test-setting-path";
public String settingPath() {
return SETTING_PATH;
}
}
@Document(indexName = "foo")
@Setting(settingPath = "#{@spelSettingPath.settingPath}")
private static class SettingPathWithSpel {
@Nullable
@Id String id;
}
}

View File

@ -19,10 +19,16 @@ import static org.assertj.core.api.Assertions.*;
import static org.skyscreamer.jsonassert.JSONAssert.*;
import org.json.JSONException;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Version;
import org.springframework.data.elasticsearch.annotations.Document;
@ -38,6 +44,7 @@ import org.springframework.data.mapping.model.Property;
import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.TypeInformation;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.ReflectionUtils;
/**
@ -60,9 +67,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test
public void shouldThrowExceptionGivenVersionPropertyIsNotLong() {
TypeInformation<EntityWithWrongVersionType> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithWrongVersionType> typeInformation = TypeInformation
.of(EntityWithWrongVersionType.class);
SimpleElasticsearchPersistentEntity<EntityWithWrongVersionType> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithWrongVersionType> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
assertThatThrownBy(() -> createProperty(entity, "version")).isInstanceOf(MappingException.class);
@ -71,9 +78,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test
public void shouldThrowExceptionGivenMultipleVersionPropertiesArePresent() {
TypeInformation<EntityWithMultipleVersionField> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithMultipleVersionField> typeInformation = TypeInformation
.of(EntityWithMultipleVersionField.class);
SimpleElasticsearchPersistentEntity<EntityWithMultipleVersionField> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithMultipleVersionField> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
SimpleElasticsearchPersistentProperty persistentProperty1 = createProperty(entity, "version1");
SimpleElasticsearchPersistentProperty persistentProperty2 = createProperty(entity, "version2");
@ -100,9 +107,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test
// DATAES-799
void shouldReportThatThereIsNoSeqNoPrimaryTermPropertyWhenThereIsNoSuchProperty() {
TypeInformation<EntityWithoutSeqNoPrimaryTerm> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithoutSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithoutSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse();
@ -111,9 +118,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test
// DATAES-799
void shouldReportThatThereIsSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
@ -125,9 +132,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
// DATAES-799
void shouldReturnSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm();
@ -144,9 +151,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test
// DATAES-799
void shouldNotAllowMoreThanOneSeqNoPrimaryTermProperties() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
@ -164,7 +171,24 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nested
@DisplayName("index settings")
@SpringJUnitConfig({ SettingsTests.Config.class })
class SettingsTests {
@Autowired private ApplicationContext applicationContext;
@Configuration
static class Config {
@Bean
public SpelTestBean spelTestBean() {
return new SpelTestBean();
}
}
@BeforeEach
void setUp() {
((SimpleElasticsearchMappingContext) elasticsearchConverter
.get().getMappingContext()).setApplicationContext(applicationContext);
}
@Test // #1719
@DisplayName("should error if index sorting parameters do not have the same number of arguments")
@ -205,6 +229,24 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
String json = entity.getDefaultSettings().toJson();
assertEquals(expected, json, false);
}
@Test // #3187
@DisplayName("should evaluate SpEL expression in settingPath")
void shouldEvaluateSpElExpressionInSettingPath() {
var settingPath = elasticsearchConverter.get().getMappingContext()
.getRequiredPersistentEntity(SettingPathWithSpel.class).settingPath();
assertThat(settingPath).isEqualTo(SpelTestBean.SETTING_PATH);
}
private static class SpelTestBean {
public static String SETTING_PATH = "test-setting-path";
public String settingPath() {
return SETTING_PATH;
}
}
}
@Nested
@ -271,7 +313,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
}
}
// region helper functions
// region helper
private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity<?> entity,
String fieldName) {
@ -282,6 +324,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT);
}
// endregion
// region entities
@ -295,7 +338,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version;
}
public void setVersion(String version) {
public void setVersion(@Nullable String version) {
this.version = version;
}
}
@ -313,7 +356,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version1;
}
public void setVersion1(Long version1) {
public void setVersion1(@Nullable Long version1) {
this.version1 = version1;
}
@ -322,7 +365,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version2;
}
public void setVersion2(Long version2) {
public void setVersion2(@Nullable Long version2) {
this.version2 = version2;
}
}
@ -397,5 +440,12 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nullable
@Id String id;
}
@Document(indexName = "foo")
@Setting(settingPath = "#{@spelTestBean.settingPath}")
private static class SettingPathWithSpel {
@Nullable
@Id String id;
}
// endregion
}