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-features.6-0-0]]
== New in Spring Data Elasticsearch 6.0 == New in Spring Data Elasticsearch 6.0
* Upgarde to Spring 7 * Upgrade to Spring 7
* Switch to jspecify nullability annotations * Switch to jspecify nullability annotations
* Upgrade to Elasticsearch 9.1.5 * Upgrade to Elasticsearch 9.1.5
* Use the new Elasticsearch Rest5Client as default * 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]] [[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.EvaluationContext;
import org.springframework.expression.EvaluationException; import org.springframework.expression.EvaluationException;
import org.springframework.expression.Expression; import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionException;
import org.springframework.expression.ParserContext; import org.springframework.expression.ParserContext;
import org.springframework.expression.common.LiteralExpression; import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser; 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"); Assert.notNull(fieldName, "fieldName must not be null");
return fieldNamePropertyCache.computeIfAbsent(fieldName, key -> { return fieldNamePropertyCache.computeIfAbsent(fieldName, key -> {
AtomicReference<ElasticsearchPersistentProperty> propertyRef = new AtomicReference<>(); AtomicReference<@Nullable ElasticsearchPersistentProperty> propertyRef = new AtomicReference<>();
doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> { doWithProperties((PropertyHandler<@NonNull ElasticsearchPersistentProperty>) property -> {
if (key.equals(property.getFieldName())) { if (key.equals(property.getFieldName())) {
propertyRef.set(property); propertyRef.set(property);
@ -423,9 +424,9 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
try { try {
Expression expression = routingExpressions.computeIfAbsent(routing, PARSER::parseExpression); 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); EvaluationContext context = getEvaluationContext(null, expressionDependencies);
context.setVariable("entity", bean); context.setVariable("entity", bean);
@ -440,8 +441,20 @@ public class SimpleElasticsearchPersistentEntity<T> extends BasicPersistentEntit
// region index settings // region index settings
@Override @Override
public String settingPath() { public @Nullable String settingPath() {
return settingsParameter.get().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 @Override

View File

@ -84,7 +84,7 @@ public class QueryStringSpELEvaluator {
if (expr != null) { if (expr != null) {
EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getValues()) EvaluationContext context = evaluationContextProvider.getEvaluationContext(parameterAccessor.getValues())
.getRequiredEvaluationContext(); .getEvaluationContext();
if (context instanceof StandardEvaluationContext standardEvaluationContext) { if (context instanceof StandardEvaluationContext standardEvaluationContext) {
standardEvaluationContext.setTypeConverter(elasticsearchSpELTypeConverter); 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 static org.skyscreamer.jsonassert.JSONAssert.*;
import org.json.JSONException; import org.json.JSONException;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable; import org.jspecify.annotations.Nullable;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test; 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.Id;
import org.springframework.data.annotation.Version; import org.springframework.data.annotation.Version;
import org.springframework.data.elasticsearch.annotations.Document; 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.PropertyNameFieldNamingStrategy;
import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.mapping.model.SimpleTypeHolder;
import org.springframework.data.util.TypeInformation; import org.springframework.data.util.TypeInformation;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
/** /**
@ -60,9 +67,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test @Test
public void shouldThrowExceptionGivenVersionPropertyIsNotLong() { public void shouldThrowExceptionGivenVersionPropertyIsNotLong() {
TypeInformation<EntityWithWrongVersionType> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithWrongVersionType> typeInformation = TypeInformation
.of(EntityWithWrongVersionType.class); .of(EntityWithWrongVersionType.class);
SimpleElasticsearchPersistentEntity<EntityWithWrongVersionType> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithWrongVersionType> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
assertThatThrownBy(() -> createProperty(entity, "version")).isInstanceOf(MappingException.class); assertThatThrownBy(() -> createProperty(entity, "version")).isInstanceOf(MappingException.class);
@ -71,9 +78,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test @Test
public void shouldThrowExceptionGivenMultipleVersionPropertiesArePresent() { public void shouldThrowExceptionGivenMultipleVersionPropertiesArePresent() {
TypeInformation<EntityWithMultipleVersionField> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithMultipleVersionField> typeInformation = TypeInformation
.of(EntityWithMultipleVersionField.class); .of(EntityWithMultipleVersionField.class);
SimpleElasticsearchPersistentEntity<EntityWithMultipleVersionField> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithMultipleVersionField> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
SimpleElasticsearchPersistentProperty persistentProperty1 = createProperty(entity, "version1"); SimpleElasticsearchPersistentProperty persistentProperty1 = createProperty(entity, "version1");
SimpleElasticsearchPersistentProperty persistentProperty2 = createProperty(entity, "version2"); SimpleElasticsearchPersistentProperty persistentProperty2 = createProperty(entity, "version2");
@ -100,9 +107,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test @Test
// DATAES-799 // DATAES-799
void shouldReportThatThereIsNoSeqNoPrimaryTermPropertyWhenThereIsNoSuchProperty() { void shouldReportThatThereIsNoSeqNoPrimaryTermPropertyWhenThereIsNoSuchProperty() {
TypeInformation<EntityWithoutSeqNoPrimaryTerm> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithoutSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithoutSeqNoPrimaryTerm.class); .of(EntityWithoutSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithoutSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse(); assertThat(entity.hasSeqNoPrimaryTermProperty()).isFalse();
@ -111,9 +118,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test @Test
// DATAES-799 // DATAES-799
void shouldReportThatThereIsSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() { void shouldReportThatThereIsSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class); .of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
@ -125,9 +132,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
// DATAES-799 // DATAES-799
void shouldReturnSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() { void shouldReturnSeqNoPrimaryTermPropertyWhenThereIsSuchProperty() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class); .of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm(); EntityWithSeqNoPrimaryTerm instance = new EntityWithSeqNoPrimaryTerm();
@ -144,9 +151,9 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Test @Test
// DATAES-799 // DATAES-799
void shouldNotAllowMoreThanOneSeqNoPrimaryTermProperties() { void shouldNotAllowMoreThanOneSeqNoPrimaryTermProperties() {
TypeInformation<EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation
.of(EntityWithSeqNoPrimaryTerm.class); .of(EntityWithSeqNoPrimaryTerm.class);
SimpleElasticsearchPersistentEntity<EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>( SimpleElasticsearchPersistentEntity<@NonNull EntityWithSeqNoPrimaryTerm> entity = new SimpleElasticsearchPersistentEntity<>(
typeInformation, contextConfiguration); typeInformation, contextConfiguration);
entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm")); entity.addPersistentProperty(createProperty(entity, "seqNoPrimaryTerm"));
@ -164,7 +171,24 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nested @Nested
@DisplayName("index settings") @DisplayName("index settings")
@SpringJUnitConfig({ SettingsTests.Config.class })
class SettingsTests { 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 @Test // #1719
@DisplayName("should error if index sorting parameters do not have the same number of arguments") @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(); String json = entity.getDefaultSettings().toJson();
assertEquals(expected, json, false); 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 @Nested
@ -271,7 +313,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
} }
} }
// region helper functions // region helper
private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity<?> entity, private static SimpleElasticsearchPersistentProperty createProperty(SimpleElasticsearchPersistentEntity<?> entity,
String fieldName) { String fieldName) {
@ -282,6 +324,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT); return new SimpleElasticsearchPersistentProperty(property, entity, SimpleTypeHolder.DEFAULT);
} }
// endregion // endregion
// region entities // region entities
@ -295,7 +338,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version; return version;
} }
public void setVersion(String version) { public void setVersion(@Nullable String version) {
this.version = version; this.version = version;
} }
} }
@ -313,7 +356,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version1; return version1;
} }
public void setVersion1(Long version1) { public void setVersion1(@Nullable Long version1) {
this.version1 = version1; this.version1 = version1;
} }
@ -322,7 +365,7 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
return version2; return version2;
} }
public void setVersion2(Long version2) { public void setVersion2(@Nullable Long version2) {
this.version2 = version2; this.version2 = version2;
} }
} }
@ -397,5 +440,12 @@ public class SimpleElasticsearchPersistentEntityTests extends MappingContextBase
@Nullable @Nullable
@Id String id; @Id String id;
} }
@Document(indexName = "foo")
@Setting(settingPath = "#{@spelTestBean.settingPath}")
private static class SettingPathWithSpel {
@Nullable
@Id String id;
}
// endregion // endregion
} }