DATAMONGO-2112 - Allow usage of SpEL expression for index timeout.
We added expireAfter which accepts numeric values followed by the unit of measure (d(ays), h(ours), m(inutes), s(econds)) or a Spring template expression to the Indexed annotation.
@Indexed(expireAfter = "10s")
String expireAfterTenSeconds;
@Indexed(expireAfter = "1d")
String expireAfterOneDay;
@Indexed(expireAfter = "#{@mySpringBean.timeout}")
String expireAfterTimeoutObtainedFromSpringBean;
Original pull request: #647.
This commit is contained in:
committed by
Mark Paluch
parent
9c26859c04
commit
82da8027f5
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core.index;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
@@ -116,6 +117,20 @@ public class Index implements IndexDefinition {
|
||||
return expire(value, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies the TTL.
|
||||
*
|
||||
* @param timeout must not be {@literal null}.
|
||||
* @return this.
|
||||
* @throws IllegalArgumentException if given {@literal timeout} is {@literal null}.
|
||||
* @since 2.2
|
||||
*/
|
||||
public Index expire(Duration timeout) {
|
||||
|
||||
Assert.notNull(timeout, "Timeout must not be null!");
|
||||
return expire(timeout.getSeconds());
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies TTL with given {@link TimeUnit}.
|
||||
*
|
||||
|
||||
@@ -131,4 +131,28 @@ public @interface Indexed {
|
||||
* "https://docs.mongodb.org/manual/tutorial/expire-data/">https://docs.mongodb.org/manual/tutorial/expire-data/</a>
|
||||
*/
|
||||
int expireAfterSeconds() default -1;
|
||||
|
||||
/**
|
||||
* Alternative for {@link #expireAfterSeconds()} to configure the timeout after which the collection should expire.
|
||||
* Defaults to an empty String for no expiry. Accepts numeric values followed by their unit of measure (d(ays),
|
||||
* h(ours), m(inutes), s(seconds)) or a Spring {@literal template expression}.
|
||||
*
|
||||
* <pre>
|
||||
* <code>
|
||||
*
|
||||
* @Indexed(expireAfter = "10s")
|
||||
* String expireAfterTenSeconds;
|
||||
*
|
||||
* @Indexed(expireAfter = "1d")
|
||||
* String expireAfterOneDay;
|
||||
*
|
||||
* @Indexed(expireAfter = "#{@mySpringBean.timeout}")
|
||||
* String expireAfterTimeoutObtainedFromSpringBean;
|
||||
* </code>
|
||||
* </pre>
|
||||
*
|
||||
* @return {@literal 0s} by default.
|
||||
* @since 2.2
|
||||
*/
|
||||
String expireAfter() default "0s";
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
@@ -28,6 +29,8 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
@@ -43,14 +46,22 @@ import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexRes
|
||||
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexResolver.TextIndexIncludeOptions.IncludeStrategy;
|
||||
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexDefinitionBuilder;
|
||||
import org.springframework.data.mongodb.core.index.TextIndexDefinition.TextIndexedFieldSpec;
|
||||
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
import org.springframework.expression.spel.standard.SpelExpressionParser;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.NumberUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -68,8 +79,11 @@ import org.springframework.util.StringUtils;
|
||||
public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
|
||||
private static final Logger LOGGER = LoggerFactory.getLogger(MongoPersistentEntityIndexResolver.class);
|
||||
private static final Pattern TIMEOUT_PATTERN = Pattern.compile("(\\d+)(\\W+)?([dhms])");
|
||||
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
|
||||
|
||||
private final MongoMappingContext mappingContext;
|
||||
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
|
||||
|
||||
/**
|
||||
* Create new {@link MongoPersistentEntityIndexResolver}.
|
||||
@@ -428,9 +442,54 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
indexDefinition.expire(index.expireAfterSeconds(), TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
if (!index.expireAfter().isEmpty() && !index.expireAfter().equals("0s")) {
|
||||
|
||||
if (index.expireAfterSeconds() >= 0) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"@Indexed already defines an expiration timeout of %s sec. via Indexed#expireAfterSeconds. Please make to use either expireAfterSeconds or expireAfter.", index.expireAfterSeconds()));
|
||||
}
|
||||
|
||||
EvaluationContext ctx = getEvaluationContext();
|
||||
|
||||
if (persitentProperty.getOwner() instanceof BasicMongoPersistentEntity) {
|
||||
|
||||
EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity<?>) persitentProperty.getOwner())
|
||||
.getEvaluationContext(null);
|
||||
if (contextFromEntity != null && !EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
|
||||
ctx = contextFromEntity;
|
||||
}
|
||||
}
|
||||
|
||||
Duration timeout = computeIndexTimeout(index.expireAfter(), ctx);
|
||||
if (!timeout.isZero() && !timeout.isNegative()) {
|
||||
indexDefinition.expire(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
return new IndexDefinitionHolder(dotPath, indexDefinition, collection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default {@link EvaluationContext}.
|
||||
*
|
||||
* @return never {@literal null}.
|
||||
* @since 2.2
|
||||
*/
|
||||
protected EvaluationContext getEvaluationContext() {
|
||||
return evaluationContextProvider.getEvaluationContext(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression expressions}.
|
||||
*
|
||||
* @param evaluationContextProvider must not be {@literal null}.
|
||||
* @since 2.2
|
||||
*/
|
||||
public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
|
||||
this.evaluationContextProvider = evaluationContextProvider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates {@link IndexDefinition} wrapped in {@link IndexDefinitionHolder} out of {@link GeoSpatialIndexed} for
|
||||
* {@link MongoPersistentProperty}.
|
||||
@@ -511,6 +570,58 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compute the index timeout value by evaluating a potential
|
||||
* {@link org.springframework.expression.spel.standard.SpelExpression} and parsing the final value.
|
||||
*
|
||||
* @param timeoutValue must not be {@literal null}.
|
||||
* @param evaluationContext must not be {@literal null}.
|
||||
* @return never {@literal null}
|
||||
* @since 2.2
|
||||
* @throws IllegalArgumentException for invalid duration values.
|
||||
*/
|
||||
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
|
||||
|
||||
String val = evaluatePotentialTemplateExpression(timeoutValue, evaluationContext);
|
||||
|
||||
if (val == null) {
|
||||
return Duration.ZERO;
|
||||
}
|
||||
|
||||
Matcher matcher = TIMEOUT_PATTERN.matcher(val);
|
||||
if (matcher.find()) {
|
||||
|
||||
Long timeout = NumberUtils.parseNumber(matcher.group(1), Long.class);
|
||||
String unit = matcher.group(3);
|
||||
|
||||
switch (unit) {
|
||||
case "d":
|
||||
return Duration.ofDays(timeout);
|
||||
case "h":
|
||||
return Duration.ofHours(timeout);
|
||||
case "m":
|
||||
return Duration.ofMinutes(timeout);
|
||||
case "s":
|
||||
return Duration.ofSeconds(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Index timeout %s cannot be parsed. Please use the following pattern '\\d+\\W?[dhms]'.", val));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String evaluatePotentialTemplateExpression(String value, EvaluationContext evaluationContext) {
|
||||
|
||||
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
|
||||
if (expression instanceof LiteralExpression) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return expression.getValue(evaluationContext, String.class);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@link CycleGuard} holds information about properties and the paths for accessing those. This information is used
|
||||
* to detect potential cycles within the references.
|
||||
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.mapping.PropertyHandler;
|
||||
import org.springframework.data.mapping.model.BasicPersistentEntity;
|
||||
import org.springframework.data.mongodb.MongoCollectionUtils;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
import org.springframework.expression.EvaluationContext;
|
||||
import org.springframework.expression.Expression;
|
||||
import org.springframework.expression.ParserContext;
|
||||
import org.springframework.expression.common.LiteralExpression;
|
||||
@@ -138,6 +139,15 @@ public class BasicMongoPersistentEntity<T> extends BasicPersistentEntity<T, Mong
|
||||
verifyFieldTypes();
|
||||
}
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
* @see org.springframework.data.mapping.model.BasicPersistentEntity#getEvaluationContext(java.lang.Object)
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext getEvaluationContext(Object rootObject) {
|
||||
return super.getEvaluationContext(rootObject);
|
||||
}
|
||||
|
||||
private void verifyFieldUniqueness() {
|
||||
|
||||
AssertFieldNameUniquenessHandler handler = new AssertFieldNameUniquenessHandler();
|
||||
|
||||
@@ -18,20 +18,27 @@ package org.springframework.data.mongodb.core.index;
|
||||
import static org.hamcrest.CoreMatchers.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.mongodb.MongoCollectionUtils;
|
||||
import org.springframework.data.mongodb.MongoDbFactory;
|
||||
import org.springframework.data.mongodb.config.AbstractMongoConfiguration;
|
||||
import org.springframework.data.mongodb.core.MongoOperations;
|
||||
import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
|
||||
@@ -39,10 +46,13 @@ import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
|
||||
import org.springframework.data.mongodb.core.mapping.Document;
|
||||
import org.springframework.data.mongodb.core.mapping.Field;
|
||||
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
|
||||
import org.springframework.data.mongodb.test.util.Assertions;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
import com.mongodb.MongoClient;
|
||||
|
||||
/**
|
||||
* Integration tests for index handling.
|
||||
*
|
||||
@@ -52,13 +62,32 @@ import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
* @author Mark Paluch
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@ContextConfiguration("classpath:infrastructure.xml")
|
||||
@ContextConfiguration
|
||||
public class IndexingIntegrationTests {
|
||||
|
||||
@Autowired MongoOperations operations;
|
||||
@Autowired MongoDbFactory mongoDbFactory;
|
||||
@Autowired ConfigurableApplicationContext context;
|
||||
|
||||
@Configuration
|
||||
static class Config extends AbstractMongoConfiguration {
|
||||
|
||||
@Override
|
||||
public MongoClient mongoClient() {
|
||||
return new MongoClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getDatabaseName() {
|
||||
return "database";
|
||||
}
|
||||
|
||||
@Bean
|
||||
TimeoutResolver myTimeoutResolver() {
|
||||
return new TimeoutResolver("11s");
|
||||
}
|
||||
}
|
||||
|
||||
@After
|
||||
public void tearDown() {
|
||||
operations.dropCollection(IndexedPerson.class);
|
||||
@@ -97,6 +126,24 @@ public class IndexingIntegrationTests {
|
||||
assertThat(hasIndex("_lastname", IndexedPerson.class), is(true));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
@DirtiesContext
|
||||
public void evaluatesTimeoutSpelExpresssionWithBeanReference() {
|
||||
|
||||
operations.getConverter().getMappingContext().getPersistentEntity(WithSpelIndexTimeout.class);
|
||||
|
||||
Optional<org.bson.Document> indexInfo = operations.execute("withSpelIndexTimeout", collection -> {
|
||||
|
||||
return collection.listIndexes(org.bson.Document.class).into(new ArrayList<>()) //
|
||||
.stream() //
|
||||
.filter(it -> it.get("name").equals("someString")) //
|
||||
.findFirst();
|
||||
});
|
||||
|
||||
Assertions.assertThat(indexInfo).isPresent();
|
||||
Assertions.assertThat(indexInfo.get()).containsEntry("expireAfterSeconds", 11L);
|
||||
}
|
||||
|
||||
@Target({ ElementType.FIELD })
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Indexed
|
||||
@@ -110,6 +157,17 @@ public class IndexingIntegrationTests {
|
||||
@Field("_lastname") @IndexedFieldAnnotation String lastname;
|
||||
}
|
||||
|
||||
@RequiredArgsConstructor
|
||||
@Getter
|
||||
static class TimeoutResolver {
|
||||
final String timeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
class WithSpelIndexTimeout {
|
||||
@Indexed(expireAfter = "#{@myTimeoutResolver?.timeout}") String someString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether an index with the given name exists for the given entity type.
|
||||
*
|
||||
|
||||
@@ -27,6 +27,7 @@ import java.lang.annotation.Target;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.assertj.core.api.Assertions;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.runners.Suite;
|
||||
@@ -201,6 +202,45 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
isBsonObject().containing("sparse", true).containing("name", "different_name").notContaining("unique"));
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldResolveTimeoutFromString() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
WithExpireAfterAsPlainString.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 600L);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldResolveTimeoutFromExpression() {
|
||||
|
||||
List<IndexDefinitionHolder> indexDefinitions = prepareMappingContextAndResolveIndexForType(
|
||||
WithExpireAfterAsExpression.class);
|
||||
|
||||
Assertions.assertThat(indexDefinitions.get(0).getIndexOptions()).containsEntry("expireAfterSeconds", 11L);
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldErrorOnInvalidTimeoutExpression() {
|
||||
|
||||
MongoMappingContext mappingContext = prepareMappingContext(WithInvalidExpireAfter.class);
|
||||
MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext);
|
||||
|
||||
Assertions.assertThatExceptionOfType(IllegalArgumentException.class).isThrownBy(() -> indexResolver
|
||||
.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithInvalidExpireAfter.class)));
|
||||
|
||||
}
|
||||
|
||||
@Test // DATAMONGO-2112
|
||||
public void shouldErrorOnDuplicateTimeoutExpression() {
|
||||
|
||||
MongoMappingContext mappingContext = prepareMappingContext(WithDuplicateExpiry.class);
|
||||
MongoPersistentEntityIndexResolver indexResolver = new MongoPersistentEntityIndexResolver(mappingContext);
|
||||
|
||||
Assertions.assertThatExceptionOfType(IllegalStateException.class).isThrownBy(() -> indexResolver
|
||||
.resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(WithDuplicateExpiry.class)));
|
||||
}
|
||||
|
||||
@Document("Zero")
|
||||
static class IndexOnLevelZero {
|
||||
@Indexed String indexedProperty;
|
||||
@@ -290,6 +330,26 @@ public class MongoPersistentEntityIndexResolverUnitTests {
|
||||
@AliasFor(annotation = org.springframework.data.mongodb.core.mapping.Field.class, attribute = "value")
|
||||
String name() default "_id";
|
||||
}
|
||||
|
||||
@Document
|
||||
static class WithExpireAfterAsPlainString {
|
||||
@Indexed(expireAfter = "10m") String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
static class WithExpireAfterAsExpression {
|
||||
@Indexed(expireAfter = "#{10 + 1 + 's'}") String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
class WithInvalidExpireAfter {
|
||||
@Indexed(expireAfter = "123ops") String withTimeout;
|
||||
}
|
||||
|
||||
@Document
|
||||
class WithDuplicateExpiry {
|
||||
@Indexed(expireAfter = "1s", expireAfterSeconds = 2) String withTimeout;
|
||||
}
|
||||
}
|
||||
|
||||
@Target({ ElementType.FIELD })
|
||||
|
||||
Reference in New Issue
Block a user