diff --git a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc index 932c00a5b..eb4d01ad8 100644 --- a/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc +++ b/src/main/antora/modules/ROOT/pages/elasticsearch/elasticsearch-new.adoc @@ -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]] diff --git a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java index 6d106015b..761a90a8e 100644 --- a/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java +++ b/src/main/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntity.java @@ -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 extends BasicPersistentEntit Assert.notNull(fieldName, "fieldName must not be null"); return fieldNamePropertyCache.computeIfAbsent(fieldName, key -> { - AtomicReference 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 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 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 diff --git a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java index 9d982d928..b29cf8781 100644 --- a/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java +++ b/src/main/java/org/springframework/data/elasticsearch/repository/support/spel/QueryStringSpELEvaluator.java @@ -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); diff --git a/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsELCIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsELCIntegrationTests.java new file mode 100644 index 000000000..d9d043b31 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsELCIntegrationTests.java @@ -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(); + } + } +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsIntegrationTests.java b/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsIntegrationTests.java new file mode 100644 index 000000000..aef3d83e1 --- /dev/null +++ b/src/test/java/org/springframework/data/elasticsearch/core/IndexSettingsIntegrationTests.java @@ -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; + } + +} diff --git a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java index 7c59723d3..246042d5c 100644 --- a/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java +++ b/src/test/java/org/springframework/data/elasticsearch/core/mapping/SimpleElasticsearchPersistentEntityTests.java @@ -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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithWrongVersionType> typeInformation = TypeInformation .of(EntityWithWrongVersionType.class); - SimpleElasticsearchPersistentEntity 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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithMultipleVersionField> typeInformation = TypeInformation .of(EntityWithMultipleVersionField.class); - SimpleElasticsearchPersistentEntity 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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithoutSeqNoPrimaryTerm> typeInformation = TypeInformation .of(EntityWithoutSeqNoPrimaryTerm.class); - SimpleElasticsearchPersistentEntity 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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation .of(EntityWithSeqNoPrimaryTerm.class); - SimpleElasticsearchPersistentEntity 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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation .of(EntityWithSeqNoPrimaryTerm.class); - SimpleElasticsearchPersistentEntity 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 typeInformation = TypeInformation + TypeInformation<@NonNull EntityWithSeqNoPrimaryTerm> typeInformation = TypeInformation .of(EntityWithSeqNoPrimaryTerm.class); - SimpleElasticsearchPersistentEntity 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 }