Skip to content

Commit ecc3f8f

Browse files
Polishing.
Switch to Spring Framework duration formatting. Favour expireAfter with string parameter over the seconds based variant. Deprecate the existing expireAfterSeconds attribute of the Indexed annotation. Consider property value syntax when parsing timeout expressions. Remove DurationStyle (package visible). Update documentation. Original Pull Request: #4114
1 parent 197998f commit ecc3f8f

File tree

10 files changed

+236
-444
lines changed

10 files changed

+236
-444
lines changed

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717

1818
import java.time.Duration;
1919
import java.util.Optional;
20+
import java.util.function.Function;
2021

2122
import org.springframework.data.mongodb.core.mapping.Field;
2223
import org.springframework.data.mongodb.core.query.Collation;
@@ -90,7 +91,7 @@ public static CollectionOptions empty() {
9091

9192
/**
9293
* Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use
93-
* {@link #timeSeries(TimeSeriesOptions)}.
94+
* {@link #timeSeries(String, Function)}.
9495
*
9596
* @param timeField The name of the property which contains the date in each time series document. Must not be
9697
* {@literal null}.
@@ -99,7 +100,19 @@ public static CollectionOptions empty() {
99100
* @since 3.3
100101
*/
101102
public static CollectionOptions timeSeries(String timeField) {
102-
return empty().timeSeries(TimeSeriesOptions.timeSeries(timeField));
103+
return timeSeries(timeField, it -> it);
104+
}
105+
106+
/**
107+
* Set up {@link CollectionOptions} for a Time Series collection.
108+
*
109+
* @param timeField the name of the field that contains the date in each time series document.
110+
* @param options a function to apply additional settings to {@link TimeSeriesOptions}.
111+
* @return new instance of {@link CollectionOptions}.
112+
* @since 4.4
113+
*/
114+
public static CollectionOptions timeSeries(String timeField, Function<TimeSeriesOptions, TimeSeriesOptions> options) {
115+
return empty().timeSeries(options.apply(TimeSeriesOptions.timeSeries(timeField)));
103116
}
104117

105118
/**
@@ -619,9 +632,9 @@ public int hashCode() {
619632
* Options applicable to Time Series collections.
620633
*
621634
* @author Christoph Strobl
622-
* @since 3.3
623635
* @see <a href=
624636
* "https://docs.mongodb.com/manual/core/timeseries-collections">https://docs.mongodb.com/manual/core/timeseries-collections</a>
637+
* @since 3.3
625638
*/
626639
public static class TimeSeriesOptions {
627640

@@ -631,15 +644,16 @@ public static class TimeSeriesOptions {
631644

632645
private final GranularityDefinition granularity;
633646

634-
private final long expireAfterSeconds;
647+
private final Duration expireAfter;
635648

636-
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity, long expireAfterSeconds) {
649+
private TimeSeriesOptions(String timeField, @Nullable String metaField, GranularityDefinition granularity,
650+
Duration expireAfter) {
637651
Assert.hasText(timeField, "Time field must not be empty or null");
638652

639653
this.timeField = timeField;
640654
this.metaField = metaField;
641655
this.granularity = granularity;
642-
this.expireAfterSeconds = expireAfterSeconds;
656+
this.expireAfter = expireAfter;
643657
}
644658

645659
/**
@@ -651,7 +665,7 @@ private TimeSeriesOptions(String timeField, @Nullable String metaField, Granular
651665
* @return new instance of {@link TimeSeriesOptions}.
652666
*/
653667
public static TimeSeriesOptions timeSeries(String timeField) {
654-
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, -1);
668+
return new TimeSeriesOptions(timeField, null, Granularity.DEFAULT, Duration.ofSeconds(-1));
655669
}
656670

657671
/**
@@ -664,7 +678,7 @@ public static TimeSeriesOptions timeSeries(String timeField) {
664678
* @return new instance of {@link TimeSeriesOptions}.
665679
*/
666680
public TimeSeriesOptions metaField(String metaField) {
667-
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
681+
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
668682
}
669683

670684
/**
@@ -675,17 +689,19 @@ public TimeSeriesOptions metaField(String metaField) {
675689
* @see Granularity
676690
*/
677691
public TimeSeriesOptions granularity(GranularityDefinition granularity) {
678-
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfterSeconds);
692+
return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter);
679693
}
680694

681695
/**
682-
* Select the expire parameter to define automatic removal of documents older than a specified
683-
* duration.
696+
* Set the {@link Duration} for automatic removal of documents older than a specified value.
684697
*
698+
* @param ttl must not be {@literal null}.
685699
* @return new instance of {@link TimeSeriesOptions}.
700+
* @see com.mongodb.client.model.CreateCollectionOptions#expireAfter(long, java.util.concurrent.TimeUnit)
701+
* @since 4.4
686702
*/
687-
public TimeSeriesOptions expireAfter(Duration timeout) {
688-
return new TimeSeriesOptions(timeField, metaField, granularity, timeout.getSeconds());
703+
public TimeSeriesOptions expireAfter(Duration ttl) {
704+
return new TimeSeriesOptions(timeField, metaField, granularity, ttl);
689705
}
690706

691707
/**
@@ -712,10 +728,13 @@ public GranularityDefinition getGranularity() {
712728
}
713729

714730
/**
715-
* @return {@literal -1} if not specified
731+
* Get the {@link Duration} for automatic removal of documents.
732+
*
733+
* @return a {@link Duration#isNegative() negative} value if not specified.
734+
* @since 4.4
716735
*/
717-
public long getExpireAfterSeconds() {
718-
return expireAfterSeconds;
736+
public Duration getExpireAfter() {
737+
return expireAfter;
719738
}
720739

721740
@Override

spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java

Lines changed: 42 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,11 @@
2626
import org.bson.BsonNull;
2727
import org.bson.Document;
2828
import org.springframework.core.convert.ConversionService;
29+
import org.springframework.core.env.Environment;
30+
import org.springframework.core.env.EnvironmentCapable;
2931
import org.springframework.dao.InvalidDataAccessApiUsageException;
3032
import org.springframework.data.convert.CustomConversions;
33+
import org.springframework.data.expression.ValueEvaluationContext;
3134
import org.springframework.data.mapping.IdentifierAccessor;
3235
import org.springframework.data.mapping.MappingException;
3336
import org.springframework.data.mapping.PersistentEntity;
@@ -41,26 +44,25 @@
4144
import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper;
4245
import org.springframework.data.mongodb.core.convert.MongoWriter;
4346
import org.springframework.data.mongodb.core.convert.QueryMapper;
47+
import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity;
4448
import org.springframework.data.mongodb.core.mapping.FieldName;
45-
import org.springframework.data.mongodb.core.index.DurationStyle;
46-
import org.springframework.data.mongodb.core.mapping.*;
49+
import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
50+
import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
51+
import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
52+
import org.springframework.data.mongodb.core.mapping.TimeSeries;
4753
import org.springframework.data.mongodb.core.query.Collation;
4854
import org.springframework.data.mongodb.core.query.Criteria;
4955
import org.springframework.data.mongodb.core.query.Query;
5056
import org.springframework.data.mongodb.core.timeseries.Granularity;
5157
import org.springframework.data.mongodb.core.validation.Validator;
5258
import org.springframework.data.mongodb.util.BsonUtils;
59+
import org.springframework.data.mongodb.util.DurationUtil;
5360
import org.springframework.data.projection.EntityProjection;
5461
import org.springframework.data.projection.EntityProjectionIntrospector;
5562
import org.springframework.data.projection.ProjectionFactory;
5663
import org.springframework.data.projection.TargetAware;
57-
import org.springframework.data.spel.EvaluationContextProvider;
5864
import org.springframework.data.util.Optionals;
59-
import org.springframework.expression.EvaluationContext;
60-
import org.springframework.expression.Expression;
61-
import org.springframework.expression.ParserContext;
62-
import org.springframework.expression.common.LiteralExpression;
63-
import org.springframework.expression.spel.standard.SpelExpressionParser;
65+
import org.springframework.expression.spel.support.SimpleEvaluationContext;
6466
import org.springframework.lang.Nullable;
6567
import org.springframework.util.Assert;
6668
import org.springframework.util.ClassUtils;
@@ -96,7 +98,7 @@ class EntityOperations {
9698

9799
private final MongoJsonSchemaMapper schemaMapper;
98100

99-
private EvaluationContextProvider evaluationContextProvider = EvaluationContextProvider.DEFAULT;
101+
private @Nullable Environment environment;
100102

101103
EntityOperations(MongoConverter converter) {
102104
this(converter, new QueryMapper(converter));
@@ -117,6 +119,9 @@ class EntityOperations {
117119
.and(((target, underlyingType) -> !conversions.isSimpleType(target))),
118120
context);
119121
this.schemaMapper = new MongoJsonSchemaMapper(converter);
122+
if (converter instanceof EnvironmentCapable environmentCapable) {
123+
this.environment = environmentCapable.getEnvironment();
124+
}
120125
}
121126

122127
/**
@@ -285,7 +290,7 @@ public <T> TypedOperations<T> forType(@Nullable Class<T> entityClass) {
285290
MongoPersistentEntity<?> entity = context.getPersistentEntity(entityClass);
286291

287292
if (entity != null) {
288-
return new TypedEntityOperations(entity, evaluationContextProvider);
293+
return new TypedEntityOperations(entity, environment);
289294
}
290295

291296
}
@@ -363,8 +368,8 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec
363368
options.granularity(TimeSeriesGranularity.valueOf(it.getGranularity().name().toUpperCase()));
364369
}
365370

366-
if (it.getExpireAfterSeconds() >= 0) {
367-
result.expireAfter(it.getExpireAfterSeconds(), TimeUnit.SECONDS);
371+
if (!it.getExpireAfter().isNegative()) {
372+
result.expireAfter(it.getExpireAfter().toSeconds(), TimeUnit.SECONDS);
368373
}
369374

370375
result.timeSeriesOptions(options);
@@ -1039,13 +1044,14 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions options) {
10391044
*/
10401045
static class TypedEntityOperations<T> implements TypedOperations<T> {
10411046

1042-
private static final SpelExpressionParser PARSER = new SpelExpressionParser();
10431047
private final MongoPersistentEntity<T> entity;
1044-
private final EvaluationContextProvider evaluationContextProvider;
10451048

1046-
protected TypedEntityOperations(MongoPersistentEntity<T> entity, EvaluationContextProvider evaluationContextProvider) {
1049+
@Nullable private final Environment environment;
1050+
1051+
protected TypedEntityOperations(MongoPersistentEntity<T> entity, @Nullable Environment environment) {
1052+
10471053
this.entity = entity;
1048-
this.evaluationContextProvider = evaluationContextProvider;
1054+
this.environment = environment;
10491055
}
10501056

10511057
@Override
@@ -1094,21 +1100,10 @@ public CollectionOptions getCollectionOptions() {
10941100
options = options.granularity(timeSeries.granularity());
10951101
}
10961102

1097-
if (timeSeries.expireAfterSeconds() >= 0) {
1098-
options = options.expireAfter(Duration.ofSeconds(timeSeries.expireAfterSeconds()));
1099-
}
1100-
11011103
if (StringUtils.hasText(timeSeries.expireAfter())) {
11021104

1103-
if (timeSeries.expireAfterSeconds() >= 0) {
1104-
throw new IllegalStateException(String.format(
1105-
"@TimeSeries already defines an expiration timeout of %s seconds via TimeSeries#expireAfterSeconds; Please make to use either expireAfterSeconds or expireAfter",
1106-
timeSeries.expireAfterSeconds()));
1107-
}
1108-
1109-
Duration timeout = computeIndexTimeout(timeSeries.expireAfter(),
1110-
getEvaluationContextForProperty(entity));
1111-
if (!timeout.isZero() && !timeout.isNegative()) {
1105+
Duration timeout = computeIndexTimeout(timeSeries.expireAfter(), getEvaluationContextForEntity(entity));
1106+
if (!timeout.isNegative()) {
11121107
options = options.expireAfter(timeout);
11131108
}
11141109
}
@@ -1127,105 +1122,45 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) {
11271122
if (StringUtils.hasText(source.getMetaField())) {
11281123
target = target.metaField(mappedNameOrDefault(source.getMetaField()));
11291124
}
1130-
return target.granularity(source.getGranularity())
1131-
.expireAfter(Duration.ofSeconds(source.getExpireAfterSeconds()));
1132-
}
1133-
1134-
private String mappedNameOrDefault(String name) {
1135-
MongoPersistentProperty persistentProperty = entity.getPersistentProperty(name);
1136-
return persistentProperty != null ? persistentProperty.getFieldName() : name;
1125+
return target.granularity(source.getGranularity()).expireAfter(source.getExpireAfter());
11371126
}
11381127

11391128
@Override
11401129
public String getIdKeyName() {
11411130
return entity.getIdProperty().getName();
11421131
}
1143-
}
1144-
1145-
1146-
/**
1147-
* Compute the index timeout value by evaluating a potential
1148-
* {@link org.springframework.expression.spel.standard.SpelExpression} and parsing the final value.
1149-
*
1150-
* @param timeoutValue must not be {@literal null}.
1151-
* @param evaluationContext must not be {@literal null}.
1152-
* @return never {@literal null}
1153-
* @since 2.2
1154-
* @throws IllegalArgumentException for invalid duration values.
1155-
*/
1156-
private static Duration computeIndexTimeout(String timeoutValue, EvaluationContext evaluationContext) {
1157-
1158-
Object evaluatedTimeout = evaluate(timeoutValue, evaluationContext);
1159-
1160-
if (evaluatedTimeout == null) {
1161-
return Duration.ZERO;
1162-
}
11631132

1164-
if (evaluatedTimeout instanceof Duration) {
1165-
return (Duration) evaluatedTimeout;
1166-
}
1167-
1168-
String val = evaluatedTimeout.toString();
1169-
1170-
if (val == null) {
1171-
return Duration.ZERO;
1172-
}
1173-
1174-
return DurationStyle.detectAndParse(val);
1175-
}
1176-
1177-
@Nullable
1178-
private static Object evaluate(String value, EvaluationContext evaluationContext) {
1179-
1180-
Expression expression = PARSER.parseExpression(value, ParserContext.TEMPLATE_EXPRESSION);
1181-
if (expression instanceof LiteralExpression) {
1182-
return value;
1183-
}
1184-
1185-
return expression.getValue(evaluationContext, Object.class);
1133+
private String mappedNameOrDefault(String name) {
1134+
MongoPersistentProperty persistentProperty = entity.getPersistentProperty(name);
1135+
return persistentProperty != null ? persistentProperty.getFieldName() : name;
11861136
}
11871137

1188-
11891138
/**
1190-
* Get the {@link EvaluationContext} for a given {@link PersistentEntity entity} the default one.
1139+
* Get the {@link ValueEvaluationContext} for a given {@link PersistentEntity entity} the default one.
11911140
*
11921141
* @param persistentEntity can be {@literal null}
1193-
* @return
1142+
* @return the context to use.
11941143
*/
1195-
private EvaluationContext getEvaluationContextForProperty(@Nullable PersistentEntity<?, ?> persistentEntity) {
1144+
private ValueEvaluationContext getEvaluationContextForEntity(@Nullable PersistentEntity<?, ?> persistentEntity) {
11961145

1197-
if (!(persistentEntity instanceof BasicMongoPersistentEntity)) {
1198-
return getEvaluationContext();
1146+
if (persistentEntity instanceof BasicMongoPersistentEntity<?> mongoEntity) {
1147+
return mongoEntity.getValueEvaluationContext(null);
11991148
}
12001149

1201-
EvaluationContext contextFromEntity = ((BasicMongoPersistentEntity<?>) persistentEntity).getEvaluationContext(null);
1202-
1203-
if (!EvaluationContextProvider.DEFAULT.equals(contextFromEntity)) {
1204-
return contextFromEntity;
1205-
}
1206-
1207-
return getEvaluationContext();
1150+
return ValueEvaluationContext.of(this.environment, SimpleEvaluationContext.forReadOnlyDataBinding().build());
12081151
}
12091152

12101153
/**
1211-
* Get the default {@link EvaluationContext}.
1154+
* Compute the index timeout value by evaluating a potential
1155+
* {@link org.springframework.expression.spel.standard.SpelExpression} and parsing the final value.
12121156
*
1213-
* @return never {@literal null}.
1214-
* @since 2.2
1157+
* @param timeoutValue must not be {@literal null}.
1158+
* @param evaluationContext must not be {@literal null}.
1159+
* @return never {@literal null}
1160+
* @throws IllegalArgumentException for invalid duration values.
12151161
*/
1216-
protected EvaluationContext getEvaluationContext() {
1217-
return evaluationContextProvider.getEvaluationContext(null);
1162+
private static Duration computeIndexTimeout(String timeoutValue, ValueEvaluationContext evaluationContext) {
1163+
return DurationUtil.evaluate(timeoutValue, evaluationContext);
12181164
}
12191165
}
1220-
1221-
/**
1222-
* Set the {@link EvaluationContextProvider} used for obtaining the {@link EvaluationContext} used to compute
1223-
* {@link org.springframework.expression.spel.standard.SpelExpression expressions}.
1224-
*
1225-
* @param evaluationContextProvider must not be {@literal null}.
1226-
* @since 2.2
1227-
*/
1228-
public void setEvaluationContextProvider(EvaluationContextProvider evaluationContextProvider) {
1229-
this.evaluationContextProvider = evaluationContextProvider;
1230-
}
12311166
}

0 commit comments

Comments
 (0)