From 9b7a4e5a3787de134fc3b291c146bad3f3e49e7c Mon Sep 17 00:00:00 2001 From: Ross Lawley Date: Fri, 24 Jan 2025 13:13:20 +0000 Subject: [PATCH 01/74] Adding queryable encryption range support Supports range style queries for encrypted fields Closes: #4185 Original Pull Request: #4885 --- .../data/mongodb/core/CollectionOptions.java | 56 ++- .../mongodb/core/EncryptionAlgorithms.java | 2 + .../data/mongodb/core/EntityOperations.java | 2 + .../core/convert/MongoConversionContext.java | 24 +- .../mongodb/core/convert/QueryMapper.java | 22 +- .../encryption/ExplicitEncryptionContext.java | 7 + .../encryption/MongoEncryptionConverter.java | 83 +++- .../mongodb/core/encryption/Encryption.java | 15 + .../core/encryption/EncryptionContext.java | 10 + .../core/encryption/EncryptionOptions.java | 187 ++++++++- .../encryption/MongoClientEncryption.java | 23 +- .../core/mapping/ExplicitEncrypted.java | 23 +- .../util/MongoCompatibilityAdapter.java | 10 + .../core/encryption/RangeEncryptionTests.java | 365 ++++++++++++++++++ 14 files changed, 803 insertions(+), 26 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index d627ba2468..97cbfb536d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.function.Function; +import org.bson.conversions.Bson; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; @@ -41,6 +42,7 @@ * @author Mark Paluch * @author Andreas Zink * @author Ben Foster + * @author Ross Lawley */ public class CollectionOptions { @@ -51,10 +53,11 @@ public class CollectionOptions { private ValidationOptions validationOptions; private @Nullable TimeSeriesOptions timeSeriesOptions; private @Nullable CollectionChangeStreamOptions changeStreamOptions; + private @Nullable Bson encryptedFields; private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped, @Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions, - @Nullable CollectionChangeStreamOptions changeStreamOptions) { + @Nullable CollectionChangeStreamOptions changeStreamOptions, @Nullable Bson encryptedFields) { this.maxDocuments = maxDocuments; this.size = size; @@ -63,6 +66,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul this.validationOptions = validationOptions; this.timeSeriesOptions = timeSeriesOptions; this.changeStreamOptions = changeStreamOptions; + this.encryptedFields = encryptedFields; } /** @@ -76,7 +80,7 @@ public static CollectionOptions just(Collation collation) { Assert.notNull(collation, "Collation must not be null"); - return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null); + return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null); } /** @@ -86,7 +90,7 @@ public static CollectionOptions just(Collation collation) { * @since 2.0 */ public static CollectionOptions empty() { - return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null); + return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null); } /** @@ -136,7 +140,7 @@ public static CollectionOptions emitChangedRevisions() { */ public CollectionOptions capped() { return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -148,7 +152,7 @@ public CollectionOptions capped() { */ public CollectionOptions maxDocuments(long maxDocuments) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -160,7 +164,7 @@ public CollectionOptions maxDocuments(long maxDocuments) { */ public CollectionOptions size(long size) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -172,7 +176,7 @@ public CollectionOptions size(long size) { */ public CollectionOptions collation(@Nullable Collation collation) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -293,7 +297,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) { Assert.notNull(validationOptions, "ValidationOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -307,7 +311,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) { Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); } /** @@ -321,7 +325,19 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions); + changeStreamOptions, encryptedFields); + } + + /** + * Create new {@link CollectionOptions} with the given {@code encryptedFields}. + * + * @param encryptedFields can be null + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + public CollectionOptions encryptedFields(@Nullable Bson encryptedFields) { + return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, + changeStreamOptions, encryptedFields); } /** @@ -392,12 +408,22 @@ public Optional getChangeStreamOptions() { return Optional.ofNullable(changeStreamOptions); } + /** + * Get the {@code encryptedFields} if available. + * + * @return {@link Optional#empty()} if not specified. + * @since 4.5.0 + */ + public Optional getEncryptedFields() { + return Optional.ofNullable(encryptedFields); + } + @Override public String toString() { return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped + ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions=" - + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", disableValidation=" - + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation=" + + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedFields=" + encryptedFields + + ", disableValidation=" + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}'; } @@ -431,7 +457,10 @@ public boolean equals(@Nullable Object o) { if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) { return false; } - return ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions); + if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) { + return false; + } + return ObjectUtils.nullSafeEquals(encryptedFields, that.encryptedFields); } @Override @@ -443,6 +472,7 @@ public int hashCode() { result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions); + result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFields); return result; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java index f64391e8cd..601b6898b8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EncryptionAlgorithms.java @@ -19,11 +19,13 @@ * Encryption algorithms supported by MongoDB Client Side Field Level Encryption. * * @author Christoph Strobl + * @author Ross Lawley * @since 3.3 */ public final class EncryptionAlgorithms { public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic = "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"; public static final String AEAD_AES_256_CBC_HMAC_SHA_512_Random = "AEAD_AES_256_CBC_HMAC_SHA_512-Random"; + public static final String RANGE = "Range"; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 65a5131dd1..b7a2380ce9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -83,6 +83,7 @@ * @author Mark Paluch * @author Christoph Strobl * @author Ben Foster + * @author Ross Lawley * @since 2.1 * @see MongoTemplate * @see ReactiveMongoTemplate @@ -378,6 +379,7 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec collectionOptions.getChangeStreamOptions().ifPresent(it -> result .changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages()))); + collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields); return result; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index 5fde0acddd..acc8dfacb7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -28,29 +28,44 @@ * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}. * * @author Christoph Strobl + * @author Ross Lawley * @since 3.4 */ public class MongoConversionContext implements ValueConversionContext { private final PropertyValueProvider accessor; // TODO: generics - private final @Nullable MongoPersistentProperty persistentProperty; private final MongoConverter mongoConverter; + @Nullable private final MongoPersistentProperty persistentProperty; @Nullable private final SpELContext spELContext; + @Nullable private final String fieldNameAndQueryOperator; public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { - this(accessor, persistentProperty, mongoConverter, null); + this(accessor, persistentProperty, mongoConverter, null, null); } public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, @Nullable SpELContext spELContext) { + this(accessor, persistentProperty, mongoConverter, spELContext, null); + } + + public MongoConversionContext(PropertyValueProvider accessor, + @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, + @Nullable String fieldNameAndQueryOperator) { + this(accessor, persistentProperty, mongoConverter, null, fieldNameAndQueryOperator); + } + + public MongoConversionContext(PropertyValueProvider accessor, + @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, + @Nullable SpELContext spELContext, @Nullable String fieldNameAndQueryOperator) { this.accessor = accessor; this.persistentProperty = persistentProperty; this.mongoConverter = mongoConverter; this.spELContext = spELContext; + this.fieldNameAndQueryOperator = fieldNameAndQueryOperator; } @Override @@ -84,4 +99,9 @@ public T read(@Nullable Object value, TypeInformation target) { public SpELContext getSpELContext() { return spELContext; } + + @Nullable + public String getFieldNameAndQueryOperator() { + return fieldNameAndQueryOperator; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index cce809adc6..1cd0b3bed2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -88,6 +88,7 @@ * @author David Julia * @author Divya Srivastava * @author Gyungrai Wang + * @author Ross Lawley */ public class QueryMapper { @@ -670,9 +671,23 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu PropertyValueConverter> valueConverter) { MongoPersistentProperty property = documentField.getProperty(); + + String fieldNameAndQueryOperator = property != null && !property.getFieldName().equals(documentField.name) + ? property.getFieldName() + "." + documentField.name + : documentField.name; + MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, - property, converter); + property, converter, fieldNameAndQueryOperator); + + return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext); + } + @Nullable + private Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, + PropertyValueConverter> valueConverter, + MongoConversionContext conversionContext) { + + MongoPersistentProperty property = documentField.getProperty(); /* might be an $in clause with multiple entries */ if (property != null && !property.isCollectionLike() && sourceValue instanceof Collection collection) { @@ -692,7 +707,10 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu return BsonUtils.mapValues(document, (key, val) -> { if (isKeyword(key)) { - return getMappedValue(documentField, val); + MongoConversionContext fieldConversionContext = new MongoConversionContext( + NoPropertyPropertyValueProvider.INSTANCE, property, converter, + conversionContext.getFieldNameAndQueryOperator() + "." + key); + return convertValueWithConversionContext(documentField, val, val, valueConverter, fieldConversionContext); } return val; }); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index f8d814fee4..e78feba732 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -26,6 +26,7 @@ * Default {@link EncryptionContext} implementation. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ class ExplicitEncryptionContext implements EncryptionContext { @@ -66,4 +67,10 @@ public T read(@Nullable Object value, TypeInformation target) { public T write(@Nullable Object value, TypeInformation target) { return conversionContext.write(value, target); } + + @Override + @Nullable + public String getFieldNameAndQueryOperator() { + return conversionContext.getFieldNameAndQueryOperator(); + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 1ce24b25fe..e3fdbe37cf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -15,8 +15,14 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*; + import java.util.Collection; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.apache.commons.logging.Log; @@ -31,9 +37,11 @@ import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.encryption.Encryption; import org.springframework.data.mongodb.core.encryption.EncryptionContext; +import org.springframework.data.mongodb.core.encryption.EncryptionKey; import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; import org.springframework.data.mongodb.core.encryption.EncryptionOptions; import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; @@ -44,11 +52,14 @@ * {@link Encrypted @Encrypted} to provide key and algorithm metadata. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class MongoEncryptionConverter implements EncryptingConverter { private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); + private static final String EQUALITY_OPERATOR = "$eq"; + private static final List RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte"); private final Encryption encryption; private final EncryptionKeyResolver keyResolver; @@ -161,8 +172,42 @@ public Object encrypt(Object value, EncryptionContext context) { getProperty(context).getOwner().getName(), getProperty(context).getName())); } - EncryptionOptions encryptionOptions = new EncryptionOptions(annotation.algorithm(), keyResolver.getKey(context)); + boolean encryptExpression = false; + String algorithm = annotation.algorithm(); + EncryptionKey key = keyResolver.getKey(context); + EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key); + String fieldNameAndQueryOperator = context.getFieldNameAndQueryOperator(); + + ExplicitEncrypted explicitEncryptedAnnotation = persistentProperty.findAnnotation(ExplicitEncrypted.class); + if (explicitEncryptedAnnotation != null) { + QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none(); + String rangeOptions = explicitEncryptedAnnotation.rangeOptions(); + if (!rangeOptions.isEmpty()) { + queryableEncryptionOptions = queryableEncryptionOptions.rangeOptions(Document.parse(rangeOptions)); + } + if (explicitEncryptedAnnotation.contentionFactor() >= 0) { + queryableEncryptionOptions = queryableEncryptionOptions + .contentionFactor(explicitEncryptedAnnotation.contentionFactor()); + } + + boolean isPartOfARangeQuery = algorithm.equalsIgnoreCase(RANGE) && fieldNameAndQueryOperator != null; + if (isPartOfARangeQuery) { + encryptExpression = true; + queryableEncryptionOptions = queryableEncryptionOptions.queryType("range"); + } + encryptionOptions = new EncryptionOptions(algorithm, key, queryableEncryptionOptions); + } + + if (encryptExpression) { + return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions); + } else { + return encryptValue(value, context, persistentProperty, encryptionOptions); + } + } + + private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty, + EncryptionOptions encryptionOptions) { if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { @@ -187,6 +232,42 @@ public Object encrypt(Object value, EncryptionContext context) { return encryption.encrypt(BsonUtils.simpleToBsonValue(write), encryptionOptions); } + /** + * Encrypts a range query expression. + * + *

The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method + * ensures these requirements are met and then picks out and returns just the value for use with a range query. + * + * @param fieldNameAndQueryOperator field name and query operator + * @param value the value of the expression to be encrypted + * @param encryptionOptions the options + * @return the encrypted range value for use in a range query + */ + private BsonValue encryptExpression(String fieldNameAndQueryOperator, Object value, + EncryptionOptions encryptionOptions) { + BsonValue doc = BsonUtils.simpleToBsonValue(value); + + String fieldName = fieldNameAndQueryOperator; + String queryOperator = EQUALITY_OPERATOR; + + int pos = fieldNameAndQueryOperator.lastIndexOf(".$"); + if (pos > -1) { + fieldName = fieldNameAndQueryOperator.substring(0, pos); + queryOperator = fieldNameAndQueryOperator.substring(pos + 1); + } + + if (!RANGE_OPERATORS.contains(queryOperator)) { + throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the " + + "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName)); + } + + BsonDocument encryptExpression = new BsonDocument("$and", + new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc))))); + + BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions); + return result.getArray("$and").get(0).asDocument().getDocument(fieldName).getBinary(queryOperator); + } + private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, EncryptionContext context) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java index 5645c1e416..16202598f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -15,10 +15,13 @@ */ package org.springframework.data.mongodb.core.encryption; +import org.bson.BsonDocument; + /** * Component responsible for encrypting and decrypting values. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public interface Encryption { @@ -40,4 +43,16 @@ public interface Encryption { */ S decrypt(T value); + /** + * Encrypt the given expression. + * + * @param value must not be {@literal null}. + * @param options must not be {@literal null}. + * @return the encrypted expression. + * @since 4.5.0 + */ + default BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) { + throw new UnsupportedOperationException("Unsupported encryption method"); + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 89beaadedb..1643e2f950 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -25,6 +25,7 @@ * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public interface EncryptionContext { @@ -128,4 +129,13 @@ default T write(@Nullable Object value, Class target) { EvaluationContext getEvaluationContext(Object source); + /** + * The field name and field query operator + * + * @return can be {@literal null}. + */ + @Nullable + default String getFieldNameAndQueryOperator() { + return null; + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index fe01cfa8ba..5affbeddb1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -15,6 +15,15 @@ */ package org.springframework.data.mongodb.core.encryption; +import java.util.Objects; +import java.util.Optional; + +import com.mongodb.client.model.vault.RangeOptions; +import org.bson.Document; +import org.springframework.data.mongodb.core.FindAndReplaceOptions; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.data.util.Optionals; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -22,20 +31,27 @@ * Options, like the {@link #algorithm()}, to apply when encrypting values. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class EncryptionOptions { private final String algorithm; private final EncryptionKey key; + private final QueryableEncryptionOptions queryableEncryptionOptions; public EncryptionOptions(String algorithm, EncryptionKey key) { + this(algorithm, key, QueryableEncryptionOptions.NONE); + } + public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) { Assert.hasText(algorithm, "Algorithm must not be empty"); Assert.notNull(key, "EncryptionKey must not be empty"); + Assert.notNull(key, "QueryableEncryptionOptions must not be empty"); this.key = key; this.algorithm = algorithm; + this.queryableEncryptionOptions = queryableEncryptionOptions; } public EncryptionKey key() { @@ -46,6 +62,10 @@ public String algorithm() { return algorithm; } + public QueryableEncryptionOptions queryableEncryptionOptions() { + return queryableEncryptionOptions; + } + @Override public boolean equals(Object o) { @@ -61,7 +81,11 @@ public boolean equals(Object o) { if (!ObjectUtils.nullSafeEquals(algorithm, that.algorithm)) { return false; } - return ObjectUtils.nullSafeEquals(key, that.key); + if (!ObjectUtils.nullSafeEquals(key, that.key)) { + return false; + } + + return ObjectUtils.nullSafeEquals(queryableEncryptionOptions, that.queryableEncryptionOptions); } @Override @@ -69,11 +93,170 @@ public int hashCode() { int result = ObjectUtils.nullSafeHashCode(algorithm); result = 31 * result + ObjectUtils.nullSafeHashCode(key); + result = 31 * result + ObjectUtils.nullSafeHashCode(queryableEncryptionOptions); return result; } @Override public String toString() { - return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + '}'; + return "EncryptionOptions{" + "algorithm='" + algorithm + '\'' + ", key=" + key + ", queryableEncryptionOptions='" + + queryableEncryptionOptions + "'}"; + } + + /** + * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values. + * + * @author Ross Lawley + */ + public static class QueryableEncryptionOptions { + + private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, null); + + private final @Nullable String queryType; + private final @Nullable Long contentionFactor; + private final @Nullable Document rangeOptions; + + private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor, + @Nullable Document rangeOptions) { + this.queryType = queryType; + this.contentionFactor = contentionFactor; + this.rangeOptions = rangeOptions; + } + + /** + * Create an empty {@link QueryableEncryptionOptions}. + * + * @return unmodifiable {@link QueryableEncryptionOptions} instance. + */ + public static QueryableEncryptionOptions none() { + return NONE; + } + + /** + * Define the {@code queryType} to be used for queryable document encryption. + * + * @param queryType can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions queryType(@Nullable String queryType) { + return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + } + + /** + * Define the {@code contentionFactor} to be used for queryable document encryption. + * + * @param contentionFactor can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) { + return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + } + + /** + * Define the {@code rangeOptions} to be used for queryable document encryption. + * + * @param rangeOptions can be {@literal null}. + * @return new instance of {@link QueryableEncryptionOptions}. + */ + public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) { + return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + } + + /** + * Get the {@code queryType} to apply. + * + * @return {@link Optional#empty()} if not set. + */ + public Optional getQueryType() { + return Optional.ofNullable(queryType); + } + + /** + * Get the {@code contentionFactor} to apply. + * + * @return {@link Optional#empty()} if not set. + */ + public Optional getContentionFactor() { + return Optional.ofNullable(contentionFactor); + } + + /** + * Get the {@code rangeOptions} to apply. + * + * @return {@link Optional#empty()} if not set. + */ + public Optional getRangeOptions() { + if (rangeOptions == null) { + return Optional.empty(); + } + RangeOptions encryptionRangeOptions = new RangeOptions(); + + if (rangeOptions.containsKey("min")) { + encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(rangeOptions.get("min"))); + } + if (rangeOptions.containsKey("max")) { + encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(rangeOptions.get("max"))); + } + if (rangeOptions.containsKey("trimFactor")) { + Object trimFactor = rangeOptions.get("trimFactor"); + Assert.isInstanceOf(Integer.class, trimFactor, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass())); + + encryptionRangeOptions.trimFactor((Integer) trimFactor); + } + + if (rangeOptions.containsKey("sparsity")) { + Object sparsity = rangeOptions.get("sparsity"); + Assert.isInstanceOf(Number.class, sparsity, + () -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass())); + encryptionRangeOptions.sparsity(((Number) sparsity).longValue()); + } + + if (rangeOptions.containsKey("precision")) { + Object precision = rangeOptions.get("precision"); + Assert.isInstanceOf(Number.class, precision, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass())); + encryptionRangeOptions.precision(((Number) precision).intValue()); + } + return Optional.of(encryptionRangeOptions); + } + + /** + * @return {@literal true} if no arguments set. + */ + boolean isEmpty() { + return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions()); + } + + @Override + public String toString() { + return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor + + ", rangeOptions=" + rangeOptions + '}'; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + QueryableEncryptionOptions that = (QueryableEncryptionOptions) o; + + if (!ObjectUtils.nullSafeEquals(queryType, that.queryType)) { + return false; + } + + if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) { + return false; + } + return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions); + } + + @Override + public int hashCode() { + return Objects.hash(queryType, contentionFactor, rangeOptions); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index 92350ce7d7..4d250fba05 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -18,6 +18,7 @@ import java.util.function.Supplier; import org.bson.BsonBinary; +import org.bson.BsonDocument; import org.bson.BsonValue; import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; import org.springframework.util.Assert; @@ -29,6 +30,7 @@ * {@link ClientEncryption} based {@link Encryption} implementation. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 */ public class MongoClientEncryption implements Encryption { @@ -59,7 +61,19 @@ public BsonValue decrypt(BsonBinary value) { @Override public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { + return getClientEncryption().encrypt(value, createEncryptOptions(options)); + } + + @Override + public BsonDocument encryptExpression(BsonDocument value, EncryptionOptions options) { + return getClientEncryption().encryptExpression(value, createEncryptOptions(options)); + } + + public ClientEncryption getClientEncryption() { + return source.get(); + } + private EncryptOptions createEncryptOptions(EncryptionOptions options) { EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); if (Type.ALT.equals(options.key().type())) { @@ -68,11 +82,10 @@ public BsonBinary encrypt(BsonValue value, EncryptionOptions options) { encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value()); } - return getClientEncryption().encrypt(value, encryptOptions); - } - - public ClientEncryption getClientEncryption() { - return source.get(); + options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType); + options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor); + options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions); + return encryptOptions; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java index 5f08e5c787..a8aedce8bc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java @@ -47,6 +47,7 @@ * * * @author Christoph Strobl + * @author Ross Lawley * @since 4.1 * @see ValueConverter */ @@ -60,7 +61,8 @@ * Define the algorithm to use. *

* A {@literal Deterministic} algorithm ensures that a given input value always encrypts to the same output while a - * {@literal randomized} one will produce different results every time. + * {@literal randomized} one will produce different results every time. A {@literal range} algorithm allows for + * the value to be queried whilst encrypted. *

* Please make sure to use an algorithm that is in line with MongoDB's encryption rules for simple types, complex * objects and arrays as well as the query limitations that come with each of them. @@ -84,6 +86,24 @@ */ String keyAltName() default ""; + /** + * Set the contention factor + *

+ * Only required when using {@literal range} encryption. + * @return the contention factor + */ + long contentionFactor() default -1; + + /** + * Set the {@literal range} options + *

+ * Should be valid extended json representing the range options and including the following values: + * {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}. + * + * @return the json representation of range options + */ + String rangeOptions() default ""; + /** * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property. * @@ -91,4 +111,5 @@ */ @AliasFor(annotation = ValueConverter.class, value = "value") Class value() default MongoEncryptionConverter.class; + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index f85be98c1f..b17b9f1963 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -34,6 +34,7 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.vault.RangeOptions; import com.mongodb.reactivestreams.client.MapReducePublisher; /** @@ -42,11 +43,13 @@ * This class is for internal use within the framework and should not be used by applications. * * @author Christoph Strobl + * @author Ross Lawley * @since 4.3 */ public class MongoCompatibilityAdapter { private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer"; + private static final String NOT_SUPPORTED_ON_4 = "%s is not supported on Mongo Client 4"; private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, "getStreamFactoryFactory"); @@ -54,6 +57,9 @@ public class MongoCompatibilityAdapter { private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class); + private static final @Nullable Method setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor", + Integer.class); + /** * Return a compatibility adapter for {@link MongoClientSettings.Builder}. * @@ -199,6 +205,10 @@ public interface MongoDatabaseAdapterBuilder { MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db); } + public interface RangeOptionsAdapter { + void trimFactor(Integer trimFactor); + } + @SuppressWarnings({ "unchecked", "DataFlowIssue" }) public static class MongoDatabaseAdapter { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java new file mode 100644 index 0000000000..2c5e3abc6b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java @@ -0,0 +1,365 @@ +/* + * Copyright 2023-2024 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.encryption; + +import static java.util.Arrays.*; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; + +import java.security.SecureRandom; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicReference; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; + +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonDocument; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonNull; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.Document; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.data.convert.PropertyValueConverterFactory; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; +import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; +import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; +import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.util.Lazy; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +/** + * @author Ross Lawley + */ +@ExtendWith({ MongoClientExtension.class, SpringExtension.class }) +@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") +@EnableIfReplicaSetAvailable +@ContextConfiguration(classes = RangeEncryptionTests.EncryptionConfig.class) +class RangeEncryptionTests { + + @Autowired MongoTemplate template; + + @AfterEach + void tearDown() { + template.getDb().getCollection("test").deleteMany(new BsonDocument()); + } + + @Test + void canGreaterThanEqualMatchRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("encryptedInt").gte(source.encryptedInt)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test + void canLesserThanEqualMatchRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test + void canRangeMatchRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + Person loaded = template.query(Person.class).matching(where("encryptedLong").lte(1001L).gte(1001L)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test + void canUpdateRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + source.encryptedInt = 123; + source.encryptedLong = 9999L; + template.save(source); + + Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test + void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + assertThatThrownBy( + () -> template.query(Person.class).matching(where("encryptedInt").is(source.encryptedInt)).firstValue()) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + + "the query operator '$eq' for field path 'encryptedInt' is not a range query."); + + } + + @Test + void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() { + Person source = createPerson(); + template.insert(source); + + assertThatThrownBy( + () -> template.query(Person.class).matching(where("encryptedLong").in(1001L, 9999L)).firstValue()) + .isInstanceOf(AssertionError.class) + .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + + "the query operator '$in' for field path 'encryptedLong' is not a range query."); + + } + + private Person createPerson() { + Person source = new Person(); + source.id = "id-1"; + source.encryptedInt = 101; + source.encryptedLong = 1001L; + return source; + } + + protected static class EncryptionConfig extends AbstractMongoClientConfiguration { + + private static final String LOCAL_KMS_PROVIDER = "local"; + + private static final Lazy>> LAZY_KMS_PROVIDERS = Lazy.of(() -> { + byte[] localMasterKey = new byte[96]; + new SecureRandom().nextBytes(localMasterKey); + return Map.of(LOCAL_KMS_PROVIDER, Map.of("key", localMasterKey)); + }); + + @Autowired ApplicationContext applicationContext; + + @Override + protected String getDatabaseName() { + return "qe-test"; + } + + @Bean + public MongoClient mongoClient() { + return super.mongoClient(); + } + + @Override + protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) { + converterConfigurationAdapter + .registerPropertyValueConverterFactory(PropertyValueConverterFactory.beanFactoryAware(applicationContext)) + .useNativeDriverJavaTimeCodecs(); + } + + @Bean + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { + Lazy> lazyDataKeyMap = Lazy.of(() -> { + try (MongoClient client = mongoClient()) { + MongoDatabase database = client.getDatabase(getDatabaseName()); + database.getCollection("test").drop(); + + ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption(); + BsonDocument encryptedFields = new BsonDocument().append("fields", + new BsonArray(asList( + new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedInt")) + .append("bsonType", new BsonString("int")) + .append("queries", + new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L)) + .append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1)) + .append("min", new BsonInt32(0)).append("max", new BsonInt32(200))), + new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedLong")) + .append("bsonType", new BsonString("long")).append("queries", + new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L)) + .append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1)) + .append("min", new BsonInt64(1000)).append("max", new BsonInt64(9999)))))); + + BsonDocument local = clientEncryption.createEncryptedCollection(database, "test", + new CreateCollectionOptions().encryptedFields(encryptedFields), + new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER)); + + return local.getArray("fields").stream().map(BsonValue::asDocument).collect( + Collectors.toMap(field -> field.getString("path").getValue(), field -> field.getBinary("keyId"))); + } + }); + return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver + .annotated((ctx) -> EncryptionKey.keyId(lazyDataKeyMap.get().get(ctx.getProperty().getFieldName())))); + } + + @Bean + CachingMongoClientEncryption clientEncryption(ClientEncryptionSettings encryptionSettings) { + return new CachingMongoClientEncryption(() -> ClientEncryptions.create(encryptionSettings)); + } + + @Override + protected void configureClientSettings(MongoClientSettings.Builder builder) { + try (MongoClient client = MongoClients.create()) { + ClientEncryptionSettings clientEncryptionSettings = encryptionSettings(client); + + builder.autoEncryptionSettings(AutoEncryptionSettings.builder() // + .kmsProviders(clientEncryptionSettings.getKmsProviders()) // + .keyVaultNamespace(clientEncryptionSettings.getKeyVaultNamespace()) // + .bypassQueryAnalysis(true).build()); + } + } + + @Bean + ClientEncryptionSettings encryptionSettings(MongoClient mongoClient) { + MongoNamespace keyVaultNamespace = new MongoNamespace("encryption.testKeyVault"); + MongoCollection keyVaultCollection = mongoClient.getDatabase(keyVaultNamespace.getDatabaseName()) + .getCollection(keyVaultNamespace.getCollectionName()); + keyVaultCollection.drop(); + // Ensure that two data keys cannot share the same keyAltName. + keyVaultCollection.createIndex(Indexes.ascending("keyAltNames"), + new IndexOptions().unique(true).partialFilterExpression(Filters.exists("keyAltNames"))); + + mongoClient.getDatabase(getDatabaseName()).getCollection("test").drop(); // Clear old data + + // Create the ClientEncryption instance + return ClientEncryptionSettings.builder() // + .keyVaultMongoClientSettings( + MongoClientSettings.builder().applyConnectionString(new ConnectionString("mongodb://localhost")).build()) // + .keyVaultNamespace(keyVaultNamespace.getFullName()) // + .kmsProviders(LAZY_KMS_PROVIDERS.get()) // + .build(); + } + } + + static class CachingMongoClientEncryption extends MongoClientEncryption implements DisposableBean { + + static final AtomicReference cache = new AtomicReference<>(); + + CachingMongoClientEncryption(Supplier source) { + super(() -> { + ClientEncryption clientEncryption = cache.get(); + if (clientEncryption == null) { + clientEncryption = source.get(); + cache.set(clientEncryption); + } + + return clientEncryption; + }); + } + + @Override + public void destroy() { + ClientEncryption clientEncryption = cache.get(); + if (clientEncryption != null) { + clientEncryption.close(); + cache.set(null); + } + } + } + + @org.springframework.data.mongodb.core.mapping.Document("test") + static class Person { + + String id; + String name; + + @ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L, + rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer encryptedInt; + @ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L, + rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") Long encryptedLong; + + public String getId() { + return this.id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return this.name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getEncryptedInt() { + return this.encryptedInt; + } + + public void setEncryptedInt(Integer encryptedInt) { + this.encryptedInt = encryptedInt; + } + + public Long getEncryptedLong() { + return this.encryptedLong; + } + + public void setEncryptedLong(Long encryptedLong) { + this.encryptedLong = encryptedLong; + } + + @Override + public boolean equals(Object o) { + if (this == o) + return true; + if (o == null || getClass() != o.getClass()) + return false; + + Person person = (Person) o; + return Objects.equals(id, person.id) && Objects.equals(name, person.name) + && Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(name); + result = 31 * result + Objects.hashCode(encryptedInt); + result = 31 * result + Objects.hashCode(encryptedLong); + return result; + } + + @Override + public String toString() { + return "Person{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", encryptedInt=" + encryptedInt + + ", encryptedLong=" + encryptedLong + '}'; + } + } + +} From 5235aabf297791cf901c2c0447ebe3493db848ca Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 25 Feb 2025 10:46:30 +0100 Subject: [PATCH 02/74] Introduce Queryable annotation and add Schema derivation. This commit decouples queryable encryption from explicit encryption and introduces the Queryable annotation to represent different query types like range and equality. Additionally it removes value conversion from range encryption and fixes update mapping of range encrypted fields. Original Pull Request: #4885 --- .../data/mongodb/core/CollectionOptions.java | 286 ++++++++++++-- .../data/mongodb/core/EntityOperations.java | 7 +- .../core/MappingMongoJsonSchemaCreator.java | 35 +- .../core/convert/MongoConversionContext.java | 98 ++++- .../mongodb/core/convert/QueryMapper.java | 27 +- .../mongodb/core/convert/UpdateMapper.java | 10 + .../encryption/ExplicitEncryptionContext.java | 5 +- .../encryption/MongoEncryptionConverter.java | 91 ++--- .../core/encryption/EncryptionContext.java | 3 +- .../core/encryption/EncryptionOptions.java | 105 ++--- .../encryption/MongoClientEncryption.java | 61 ++- .../core/mapping/ExplicitEncrypted.java | 18 - .../data/mongodb/core/mapping/Queryable.java | 49 +++ .../mongodb/core/mapping/RangeEncrypted.java | 56 +++ .../IdentifiableJsonSchemaProperty.java | 97 ++++- .../core/schema/JsonSchemaProperty.java | 26 +- .../mongodb/core/schema/MergedJsonSchema.java | 2 + .../core/schema/QueryCharacteristic.java | 34 ++ .../core/schema/QueryCharacteristics.java | 147 +++++++ .../util/MongoCompatibilityAdapter.java | 31 +- .../core/CollectionOptionsUnitTests.java | 105 ++++- ...efaultIndexOperationsIntegrationTests.java | 20 +- ...appingMongoJsonSchemaCreatorUnitTests.java | 74 +++- ...ableEncryptionCollectionCreationTests.java | 140 +++++++ .../core/encryption/RangeEncryptionTests.java | 362 ++++++++++++++---- .../core/schema/MongoJsonSchemaUnitTests.java | 27 +- .../ROOT/pages/mongodb/mongo-encryption.adoc | 155 +++++++- 27 files changed, 1784 insertions(+), 287 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 97cbfb536d..28215dc645 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -15,18 +15,35 @@ */ package org.springframework.data.mongodb.core; +import java.nio.charset.StandardCharsets; import java.time.Duration; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; +import java.util.stream.Collectors; -import org.bson.conversions.Bson; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonNull; +import org.bson.Document; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.core.schema.QueryCharacteristic; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.timeseries.GranularityDefinition; import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.util.Optionals; +import org.springframework.lang.CheckReturnValue; +import org.springframework.lang.Contract; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -53,11 +70,12 @@ public class CollectionOptions { private ValidationOptions validationOptions; private @Nullable TimeSeriesOptions timeSeriesOptions; private @Nullable CollectionChangeStreamOptions changeStreamOptions; - private @Nullable Bson encryptedFields; + private @Nullable EncryptedFieldsOptions encryptedFieldsOptions; private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped, @Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions, - @Nullable CollectionChangeStreamOptions changeStreamOptions, @Nullable Bson encryptedFields) { + @Nullable CollectionChangeStreamOptions changeStreamOptions, + @Nullable EncryptedFieldsOptions encryptedFieldsOptions) { this.maxDocuments = maxDocuments; this.size = size; @@ -66,7 +84,7 @@ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nul this.validationOptions = validationOptions; this.timeSeriesOptions = timeSeriesOptions; this.changeStreamOptions = changeStreamOptions; - this.encryptedFields = encryptedFields; + this.encryptedFieldsOptions = encryptedFieldsOptions; } /** @@ -131,6 +149,46 @@ public static CollectionOptions emitChangedRevisions() { return empty().changeStream(CollectionChangeStreamOptions.preAndPostImages(true)); } + /** + * Create new {@link CollectionOptions} with the given {@code encryptedFields}. + * + * @param encryptedFieldsOptions can be null + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection(@Nullable EncryptedFieldsOptions encryptedFieldsOptions) { + return new CollectionOptions(null, null, null, null, ValidationOptions.NONE, null, null, encryptedFieldsOptions); + } + + /** + * Create new {@link CollectionOptions} reading encryption options from the given {@link MongoJsonSchema}. + * + * @param schema must not be {@literal null}. + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection(MongoJsonSchema schema) { + return encryptedCollection(EncryptedFieldsOptions.fromSchema(schema)); + } + + /** + * Create new {@link CollectionOptions} building encryption options in a fluent style. + * + * @param optionsFunction must not be {@literal null}. + * @return new instance of {@link CollectionOptions}. + * @since 4.5.0 + */ + @Contract("_ -> new") + @CheckReturnValue + public static CollectionOptions encryptedCollection( + Function optionsFunction) { + return encryptedCollection(optionsFunction.apply(new EncryptedFieldsOptions())); + } + /** * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}.
* NOTE: Using capped collections requires defining {@link #size(long)}. @@ -140,7 +198,7 @@ public static CollectionOptions emitChangedRevisions() { */ public CollectionOptions capped() { return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -152,7 +210,7 @@ public CollectionOptions capped() { */ public CollectionOptions maxDocuments(long maxDocuments) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -164,7 +222,7 @@ public CollectionOptions maxDocuments(long maxDocuments) { */ public CollectionOptions size(long size) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -176,7 +234,7 @@ public CollectionOptions size(long size) { */ public CollectionOptions collation(@Nullable Collation collation) { return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -297,7 +355,7 @@ public CollectionOptions validation(ValidationOptions validationOptions) { Assert.notNull(validationOptions, "ValidationOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -311,7 +369,7 @@ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) { Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -325,19 +383,22 @@ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStream Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** - * Create new {@link CollectionOptions} with the given {@code encryptedFields}. + * Set the {@link EncryptedFieldsOptions} for collections using queryable encryption. * - * @param encryptedFields can be null + * @param encryptedFieldsOptions must not be {@literal null}. * @return new instance of {@link CollectionOptions}. - * @since 4.5.0 */ - public CollectionOptions encryptedFields(@Nullable Bson encryptedFields) { + @Contract("_ -> new") + @CheckReturnValue + public CollectionOptions encrypted(EncryptedFieldsOptions encryptedFieldsOptions) { + + Assert.notNull(encryptedFieldsOptions, "EncryptedCollectionOptions must not be null"); return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions, - changeStreamOptions, encryptedFields); + changeStreamOptions, encryptedFieldsOptions); } /** @@ -414,18 +475,18 @@ public Optional getChangeStreamOptions() { * @return {@link Optional#empty()} if not specified. * @since 4.5.0 */ - public Optional getEncryptedFields() { - return Optional.ofNullable(encryptedFields); + public Optional getEncryptedFieldsOptions() { + return Optional.ofNullable(encryptedFieldsOptions); } @Override public String toString() { return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped + ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions=" - + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedFields=" + encryptedFields - + ", disableValidation=" + disableValidation() + ", strictValidation=" + strictValidation() + ", moderateValidation=" - + moderateValidation() + ", warnOnValidationError=" + warnOnValidationError() + ", failOnValidationError=" - + failOnValidationError() + '}'; + + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedCollectionOptions=" + + encryptedFieldsOptions + ", disableValidation=" + disableValidation() + ", strictValidation=" + + strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError=" + + warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}'; } @Override @@ -460,7 +521,7 @@ public boolean equals(@Nullable Object o) { if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) { return false; } - return ObjectUtils.nullSafeEquals(encryptedFields, that.encryptedFields); + return ObjectUtils.nullSafeEquals(encryptedFieldsOptions, that.encryptedFieldsOptions); } @Override @@ -472,7 +533,7 @@ public int hashCode() { result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions); result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions); - result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFields); + result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFieldsOptions); return result; } @@ -606,6 +667,183 @@ public int hashCode() { } } + /** + * Encapsulation of Encryption options for collections. + * + * @author Christoph Strobl + * @since 4.5 + */ + public static class EncryptedFieldsOptions { + + private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions(); + + private @Nullable MongoJsonSchema schema; + private List queryableProperties; + + /** + * @return {@link EncryptedFieldsOptions#NONE} + */ + public static EncryptedFieldsOptions none() { + return NONE; + } + + /** + * @return new instance of {@link EncryptedFieldsOptions}. + */ + public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) { + return new EncryptedFieldsOptions(schema, List.of()); + } + + /** + * @return new instance of {@link EncryptedFieldsOptions}. + */ + public static EncryptedFieldsOptions fromProperties(List properties) { + return new EncryptedFieldsOptions(null, List.copyOf(properties)); + } + + EncryptedFieldsOptions() { + this(null, List.of()); + } + + private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, + List queryableProperties) { + + this.schema = schema; + this.queryableProperties = queryableProperties; + } + + /** + * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property. + *

+ * Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if + * set. + * + * @param property the queryable source - typically + * {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty + * encrypted}. + * @param characteristics the query options to set. + * @return new instance of {@link EncryptedFieldsOptions}. + */ + @Contract("_, _ -> new") + @CheckReturnValue + public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) { + + List targetPropertyList = new ArrayList<>(queryableProperties.size() + 1); + targetPropertyList.addAll(queryableProperties); + targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics))); + + return new EncryptedFieldsOptions(schema, targetPropertyList); + } + + public Document toDocument() { + return new Document("fields", selectPaths()); + } + + @NonNull + private List selectPaths() { + + Map fields = new LinkedHashMap<>(); + for (Document field : fromSchema()) { + fields.put(field.get("path", String.class), field); + } + for (Document field : fromProperties()) { + fields.put(field.get("path", String.class), field); + } + return List.copyOf(fields.values()); + } + + private List fromProperties() { + + if (queryableProperties.isEmpty()) { + return List.of(); + } + + List converted = new ArrayList<>(queryableProperties.size()); + for (QueryableJsonSchemaProperty property : queryableProperties) { + Document field = new Document("path", property.getIdentifier()); + if (!property.getTypes().isEmpty()) { + field.append("bsonType", property.getTypes().iterator().next().toBsonType().value()); + } + if (property + .getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) { + if (encrypted.getKeyId() != null) { + if (encrypted.getKeyId() instanceof String stringKey) { + field.append("keyId", + new BsonBinary(BsonBinarySubType.UUID_STANDARD, stringKey.getBytes(StandardCharsets.UTF_8))); + } else { + field.append("keyId", encrypted.getKeyId()); + } + } + } + field.append("queries", property.getCharacteristics().getCharacteristics().stream() + .map(QueryCharacteristic::toDocument).collect(Collectors.toList())); + if (!field.containsKey("keyId")) { + field.append("keyId", BsonNull.VALUE); + } + converted.add(field); + } + return converted; + } + + private List fromSchema() { + + if (schema == null) { + return List.of(); + } + + Document root = schema.schemaDocument(); + Map paths = new LinkedHashMap<>(); + collectPaths(root, null, paths); + + List fields = new ArrayList<>(); + if (!paths.isEmpty()) { + + for (Entry entry : paths.entrySet()) { + Document field = new Document("path", entry.getKey()); + field.append("keyId", entry.getValue().getOrDefault("keyId", BsonNull.VALUE)); + if (entry.getValue().containsKey("bsonType")) { + field.append("bsonType", entry.getValue().get("bsonType")); + } + field.put("queries", entry.getValue().get("queries")); + fields.add(field); + } + } + + return fields; + } + } + + private static void collectPaths(Document document, String currentPath, Map paths) { + + if (document.containsKey("type") && document.get("type").equals("object")) { + Object o = document.get("properties"); + if (o == null) { + return; + } + + if (o instanceof Document properties) { + for (Entry entry : properties.entrySet()) { + if (entry.getValue() instanceof Document nested) { + + String path = currentPath == null ? entry.getKey() : (currentPath + "." + entry.getKey()); + if (nested.containsKey("encrypt")) { + Document target = new Document(nested.get("encrypt", Document.class)); + if (nested.containsKey("queries")) { + List queries = nested.get("queries", List.class); + if (!queries.isEmpty() && queries.iterator().next() instanceof Document qd) { + target.putAll(qd); + } + } + paths.put(path, target); + } else { + collectPaths(nested, path, paths); + } + } + } + } + } + } + /** * Encapsulation of options applied to define collections change stream behaviour. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index b7a2380ce9..24977c5af1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -39,6 +39,7 @@ import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ConvertingPropertyAccessor; +import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; import org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; @@ -379,7 +380,11 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec collectionOptions.getChangeStreamOptions().ifPresent(it -> result .changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages()))); - collectionOptions.getEncryptedFields().ifPresent(result::encryptedFields); + collectionOptions.getEncryptedFieldsOptions().map(EncryptedFieldsOptions::toDocument).ifPresent(encryptedFields -> { + if (!encryptedFields.isEmpty()) { + result.encryptedFields(encryptedFields); + } + }); return result; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index 839f49c7da..790fa94293 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -31,14 +31,19 @@ import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Queryable; +import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.JsonSchemaObject; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.MongoJsonSchemaBuilder; +import org.springframework.data.mongodb.core.schema.QueryCharacteristic; +import org.springframework.data.mongodb.core.schema.QueryCharacteristics; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -291,7 +296,35 @@ private JsonSchemaProperty applyEncryptionDataIfNecessary(MongoPersistentPropert if (!ObjectUtils.isEmpty(encrypted.keyId())) { enc = enc.keys(property.getEncryptionKeyIds()); } - return enc; + + Queryable queryable = property.findAnnotation(Queryable.class); + if (queryable == null || !StringUtils.hasText(queryable.queryType())) { + return enc; + } + + QueryCharacteristic characteristic = new QueryCharacteristic() { + + @Override + public String queryType() { + return queryable.queryType(); + } + + @Override + public Document toDocument() { + + Document options = QueryCharacteristic.super.toDocument(); + + if (queryable.contentionFactor() >= 0) { + options.put("contention", queryable.contentionFactor()); + } + if (!queryable.queryAttributes().isEmpty()) { + options.putAll(Document.parse(queryable.queryAttributes())); + } + + return options; + } + }; + return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(List.of(characteristic))); } private JsonSchemaProperty createObjectSchemaPropertyForEntity(List path, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index acc8dfacb7..9d672ea929 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; - import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Nullable; /** @@ -38,7 +38,7 @@ public class MongoConversionContext implements ValueConversionContext accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter) { @@ -53,19 +53,19 @@ public MongoConversionContext(PropertyValueProvider acc public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, - @Nullable String fieldNameAndQueryOperator) { - this(accessor, persistentProperty, mongoConverter, null, fieldNameAndQueryOperator); + @Nullable OperatorContext operatorContext) { + this(accessor, persistentProperty, mongoConverter, null, operatorContext); } public MongoConversionContext(PropertyValueProvider accessor, @Nullable MongoPersistentProperty persistentProperty, MongoConverter mongoConverter, - @Nullable SpELContext spELContext, @Nullable String fieldNameAndQueryOperator) { + @Nullable SpELContext spELContext, @Nullable OperatorContext operatorContext) { this.accessor = accessor; this.persistentProperty = persistentProperty; this.mongoConverter = mongoConverter; this.spELContext = spELContext; - this.fieldNameAndQueryOperator = fieldNameAndQueryOperator; + this.operatorContext = operatorContext; } @Override @@ -78,6 +78,17 @@ public MongoPersistentProperty getProperty() { return persistentProperty; } + /** + * + * @param operatorContext + * @return new instance of {@link MongoConversionContext}. + * @since 4.5 + */ + @CheckReturnValue + public MongoConversionContext forOperator(@Nullable OperatorContext operatorContext) { + return new MongoConversionContext(accessor, persistentProperty, mongoConverter, spELContext, operatorContext); + } + @Nullable public Object getValue(String propertyPath) { return accessor.getPropertyValue(getProperty().getOwner().getRequiredPersistentProperty(propertyPath)); @@ -101,7 +112,78 @@ public SpELContext getSpELContext() { } @Nullable - public String getFieldNameAndQueryOperator() { - return fieldNameAndQueryOperator; + public OperatorContext getOperatorContext() { + return operatorContext; + } + + /** + * The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query + * operator such as {@literal $gte}. + * + * @since 4.5 + */ + public interface OperatorContext { + + /** + * The operator the conversion is used in. + * @return {@literal write} for simple write operations during save, or a query operator. + */ + String getOperator(); + + /** + * The context path the operator is used in. + * @return never {@literal null}. + */ + String getPath(); + + boolean isWriteOperation(); + } + + public static class WriteOperatorContext implements OperatorContext { + + private final String path; + + public WriteOperatorContext(String path) { + this.path = path; + } + + @Override + public String getOperator() { + return "write"; + } + + @Override + public String getPath() { + return path; + } + + @Override + public boolean isWriteOperation() { + return true; + } + } + + public static class QueryOperatorContext implements OperatorContext { + + private final String operator; + private final String path; + + public QueryOperatorContext(@Nullable String operator, String path) { + this.operator = operator != null ? operator : "$eq"; + this.path = path; + } + + public String getOperator() { + return operator; + } + + public String getPath() { + return path; + } + + @Override + public boolean isWriteOperation() { + return false; + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index 1cd0b3bed2..a0af734d69 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -37,7 +37,6 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; - import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Reference; @@ -58,6 +57,8 @@ import org.springframework.data.mongodb.core.aggregation.AggregationExpression; import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.convert.MappingMongoConverter.NestedDocument; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.QueryOperatorContext; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -672,18 +673,21 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu MongoPersistentProperty property = documentField.getProperty(); - String fieldNameAndQueryOperator = property != null && !property.getFieldName().equals(documentField.name) - ? property.getFieldName() + "." + documentField.name - : documentField.name; - - MongoConversionContext conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, - property, converter, fieldNameAndQueryOperator); + OperatorContext criteriaContext = new QueryOperatorContext( + isKeyword(documentField.name) ? documentField.name : "$eq", property.getFieldName()); + MongoConversionContext conversionContext; + if (valueConverter instanceof MongoConversionContext mcc) { + conversionContext = mcc.forOperator(criteriaContext); + } else { + conversionContext = new MongoConversionContext(NoPropertyPropertyValueProvider.INSTANCE, property, converter, + criteriaContext); + } return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext); } @Nullable - private Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, + protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, PropertyValueConverter> valueConverter, MongoConversionContext conversionContext) { @@ -707,10 +711,7 @@ private Object convertValueWithConversionContext(Field documentField, Object sou return BsonUtils.mapValues(document, (key, val) -> { if (isKeyword(key)) { - MongoConversionContext fieldConversionContext = new MongoConversionContext( - NoPropertyPropertyValueProvider.INSTANCE, property, converter, - conversionContext.getFieldNameAndQueryOperator() + "." + key); - return convertValueWithConversionContext(documentField, val, val, valueConverter, fieldConversionContext); + return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().getPath()))); } return val; }); @@ -1624,7 +1625,7 @@ public MongoConverter getConverter() { return converter; } - private enum NoPropertyPropertyValueProvider implements PropertyValueProvider { + enum NoPropertyPropertyValueProvider implements PropertyValueProvider { INSTANCE; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index 35cb578c23..805bafe974 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -24,10 +24,13 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.convert.PropertyValueConverter; +import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.WriteOperatorContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.query.Query; @@ -160,6 +163,13 @@ protected Entry getMappedObjectForField(Field field, Object rawV return super.getMappedObjectForField(field, rawValue); } + protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, + PropertyValueConverter> valueConverter, + MongoConversionContext conversionContext) { + + return super.convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext.forOperator(new WriteOperatorContext(documentField.name))); + } + private Entry getMappedUpdateModifier(Field field, Object rawValue) { Object value; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index e78feba732..67c30fcf94 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.convert.encryption; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; @@ -70,7 +71,7 @@ public T write(@Nullable Object value, TypeInformation target) { @Override @Nullable - public String getFieldNameAndQueryOperator() { - return conversionContext.getFieldNameAndQueryOperator(); + public OperatorContext getOperatorContext() { + return conversionContext.getOperatorContext(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index e3fdbe37cf..c69653d2da 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -15,10 +15,9 @@ */ package org.springframework.data.mongodb.core.convert.encryption; -import static java.util.Arrays.*; -import static java.util.Collections.*; -import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; -import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions; import java.util.Collection; import java.util.LinkedHashMap; @@ -35,17 +34,19 @@ import org.bson.types.Binary; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.encryption.Encryption; import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.encryption.EncryptionKey; import org.springframework.data.mongodb.core.encryption.EncryptionKeyResolver; import org.springframework.data.mongodb.core.encryption.EncryptionOptions; import org.springframework.data.mongodb.core.mapping.Encrypted; -import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Default implementation of {@link EncryptingConverter}. Properties used with this converter must be annotated with @@ -169,45 +170,53 @@ public Object encrypt(Object value, EncryptionContext context) { if (annotation == null) { throw new IllegalStateException(String.format("Property %s.%s is not annotated with @Encrypted", - getProperty(context).getOwner().getName(), getProperty(context).getName())); + persistentProperty.getOwner().getName(), persistentProperty.getName())); } - boolean encryptExpression = false; String algorithm = annotation.algorithm(); EncryptionKey key = keyResolver.getKey(context); - EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key); - String fieldNameAndQueryOperator = context.getFieldNameAndQueryOperator(); - - ExplicitEncrypted explicitEncryptedAnnotation = persistentProperty.findAnnotation(ExplicitEncrypted.class); - if (explicitEncryptedAnnotation != null) { - QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none(); - String rangeOptions = explicitEncryptedAnnotation.rangeOptions(); - if (!rangeOptions.isEmpty()) { - queryableEncryptionOptions = queryableEncryptionOptions.rangeOptions(Document.parse(rangeOptions)); - } - - if (explicitEncryptedAnnotation.contentionFactor() >= 0) { - queryableEncryptionOptions = queryableEncryptionOptions - .contentionFactor(explicitEncryptedAnnotation.contentionFactor()); - } + OperatorContext operatorContext = context.getOperatorContext(); - boolean isPartOfARangeQuery = algorithm.equalsIgnoreCase(RANGE) && fieldNameAndQueryOperator != null; - if (isPartOfARangeQuery) { - encryptExpression = true; - queryableEncryptionOptions = queryableEncryptionOptions.queryType("range"); - } - encryptionOptions = new EncryptionOptions(algorithm, key, queryableEncryptionOptions); - } + EncryptionOptions encryptionOptions = new EncryptionOptions(algorithm, key, + getEQOptions(persistentProperty, operatorContext)); - if (encryptExpression) { - return encryptExpression(fieldNameAndQueryOperator, value, encryptionOptions); + if (operatorContext != null && !operatorContext.isWriteOperation() && encryptionOptions.queryableEncryptionOptions() != null + && !encryptionOptions.queryableEncryptionOptions().getQueryType().equals("equality")) { + return encryptExpression(operatorContext, value, encryptionOptions); } else { return encryptValue(value, context, persistentProperty, encryptionOptions); } } + private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty, + OperatorContext operatorContext) { + + Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class); + if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) { + return null; + } + + QueryableEncryptionOptions queryableEncryptionOptions = QueryableEncryptionOptions.none(); + + String queryAttributes = queryableAnnotation.queryAttributes(); + if (!queryAttributes.isEmpty()) { + queryableEncryptionOptions = queryableEncryptionOptions.attributes(Document.parse(queryAttributes)); + } + + if (queryableAnnotation.contentionFactor() >= 0) { + queryableEncryptionOptions = queryableEncryptionOptions.contentionFactor(queryableAnnotation.contentionFactor()); + } + + boolean isPartOfARangeQuery = operatorContext != null && !operatorContext.isWriteOperation(); + if (isPartOfARangeQuery) { + queryableEncryptionOptions = queryableEncryptionOptions.queryType(queryableAnnotation.queryType()); + } + return queryableEncryptionOptions; + } + private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPersistentProperty persistentProperty, EncryptionOptions encryptionOptions) { + if (!persistentProperty.isEntity()) { if (persistentProperty.isCollectionLike()) { @@ -221,6 +230,7 @@ private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPe } return encryption.encrypt(BsonUtils.simpleToBsonValue(value), encryptionOptions); } + if (persistentProperty.isCollectionLike()) { return encryption.encrypt(collectionLikeToBsonValue(value, persistentProperty, context), encryptionOptions); } @@ -234,27 +244,22 @@ private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPe /** * Encrypts a range query expression. - * - *

The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method - * ensures these requirements are met and then picks out and returns just the value for use with a range query. + *

+ * The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these + * requirements are met and then picks out and returns just the value for use with a range query. * * @param fieldNameAndQueryOperator field name and query operator * @param value the value of the expression to be encrypted * @param encryptionOptions the options * @return the encrypted range value for use in a range query */ - private BsonValue encryptExpression(String fieldNameAndQueryOperator, Object value, + private BsonValue encryptExpression(OperatorContext operatorContext, Object value, EncryptionOptions encryptionOptions) { - BsonValue doc = BsonUtils.simpleToBsonValue(value); - String fieldName = fieldNameAndQueryOperator; - String queryOperator = EQUALITY_OPERATOR; + BsonValue doc = BsonUtils.simpleToBsonValue(value); - int pos = fieldNameAndQueryOperator.lastIndexOf(".$"); - if (pos > -1) { - fieldName = fieldNameAndQueryOperator.substring(0, pos); - queryOperator = fieldNameAndQueryOperator.substring(pos + 1); - } + String fieldName = operatorContext.getPath(); + String queryOperator = operatorContext.getOperator(); if (!RANGE_OPERATORS.contains(queryOperator)) { throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the " diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 1643e2f950..5f5e29578d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.encryption; import org.springframework.data.mapping.PersistentProperty; +import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; @@ -135,7 +136,7 @@ default T write(@Nullable Object value, Class target) { * @return can be {@literal null}. */ @Nullable - default String getFieldNameAndQueryOperator() { + default OperatorContext getOperatorContext() { return null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index 5affbeddb1..73a66e4a8a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -15,21 +15,17 @@ */ package org.springframework.data.mongodb.core.encryption; +import java.util.Map; import java.util.Objects; -import java.util.Optional; -import com.mongodb.client.model.vault.RangeOptions; -import org.bson.Document; -import org.springframework.data.mongodb.core.FindAndReplaceOptions; -import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Options, like the {@link #algorithm()}, to apply when encrypting values. - * + * Options used to provide additional information when {@link Encryption encrypting} values. like the + * {@link #algorithm()} to be used. + * * @author Christoph Strobl * @author Ross Lawley * @since 4.1 @@ -38,13 +34,15 @@ public class EncryptionOptions { private final String algorithm; private final EncryptionKey key; - private final QueryableEncryptionOptions queryableEncryptionOptions; + private final @Nullable QueryableEncryptionOptions queryableEncryptionOptions; public EncryptionOptions(String algorithm, EncryptionKey key) { - this(algorithm, key, QueryableEncryptionOptions.NONE); + this(algorithm, key, null); } - public EncryptionOptions(String algorithm, EncryptionKey key, QueryableEncryptionOptions queryableEncryptionOptions) { + public EncryptionOptions(String algorithm, EncryptionKey key, + @Nullable QueryableEncryptionOptions queryableEncryptionOptions) { + Assert.hasText(algorithm, "Algorithm must not be empty"); Assert.notNull(key, "EncryptionKey must not be empty"); Assert.notNull(key, "QueryableEncryptionOptions must not be empty"); @@ -62,7 +60,11 @@ public String algorithm() { return algorithm; } - public QueryableEncryptionOptions queryableEncryptionOptions() { + /** + * @return {@literal null} if not set. + * @since 4.5 + */ + public @Nullable QueryableEncryptionOptions queryableEncryptionOptions() { return queryableEncryptionOptions; } @@ -107,20 +109,23 @@ public String toString() { * Options, like the {@link #getQueryType()}, to apply when encrypting queryable values. * * @author Ross Lawley + * @author Christoph Strobl + * @since 4.5 */ public static class QueryableEncryptionOptions { - private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, null); + private static final QueryableEncryptionOptions NONE = new QueryableEncryptionOptions(null, null, Map.of()); private final @Nullable String queryType; private final @Nullable Long contentionFactor; - private final @Nullable Document rangeOptions; + private final Map attributes; private QueryableEncryptionOptions(@Nullable String queryType, @Nullable Long contentionFactor, - @Nullable Document rangeOptions) { + Map attributes) { + this.queryType = queryType; this.contentionFactor = contentionFactor; - this.rangeOptions = rangeOptions; + this.attributes = attributes; } /** @@ -139,7 +144,7 @@ public static QueryableEncryptionOptions none() { * @return new instance of {@link QueryableEncryptionOptions}. */ public QueryableEncryptionOptions queryType(@Nullable String queryType) { - return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); } /** @@ -149,89 +154,57 @@ public QueryableEncryptionOptions queryType(@Nullable String queryType) { * @return new instance of {@link QueryableEncryptionOptions}. */ public QueryableEncryptionOptions contentionFactor(@Nullable Long contentionFactor) { - return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); } /** * Define the {@code rangeOptions} to be used for queryable document encryption. * - * @param rangeOptions can be {@literal null}. + * @param attributes can be {@literal null}. * @return new instance of {@link QueryableEncryptionOptions}. */ - public QueryableEncryptionOptions rangeOptions(@Nullable Document rangeOptions) { - return new QueryableEncryptionOptions(queryType, contentionFactor, rangeOptions); + public QueryableEncryptionOptions attributes(Map attributes) { + return new QueryableEncryptionOptions(queryType, contentionFactor, attributes); } /** * Get the {@code queryType} to apply. * - * @return {@link Optional#empty()} if not set. + * @return {@literal null} if not set. */ - public Optional getQueryType() { - return Optional.ofNullable(queryType); + public @Nullable String getQueryType() { + return queryType; } /** * Get the {@code contentionFactor} to apply. * - * @return {@link Optional#empty()} if not set. + * @return {@literal null} if not set. */ - public Optional getContentionFactor() { - return Optional.ofNullable(contentionFactor); + public @Nullable Long getContentionFactor() { + return contentionFactor; } /** * Get the {@code rangeOptions} to apply. * - * @return {@link Optional#empty()} if not set. + * @return never {@literal null}. */ - public Optional getRangeOptions() { - if (rangeOptions == null) { - return Optional.empty(); - } - RangeOptions encryptionRangeOptions = new RangeOptions(); - - if (rangeOptions.containsKey("min")) { - encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(rangeOptions.get("min"))); - } - if (rangeOptions.containsKey("max")) { - encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(rangeOptions.get("max"))); - } - if (rangeOptions.containsKey("trimFactor")) { - Object trimFactor = rangeOptions.get("trimFactor"); - Assert.isInstanceOf(Integer.class, trimFactor, () -> String - .format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass())); - - encryptionRangeOptions.trimFactor((Integer) trimFactor); - } - - if (rangeOptions.containsKey("sparsity")) { - Object sparsity = rangeOptions.get("sparsity"); - Assert.isInstanceOf(Number.class, sparsity, - () -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass())); - encryptionRangeOptions.sparsity(((Number) sparsity).longValue()); - } - - if (rangeOptions.containsKey("precision")) { - Object precision = rangeOptions.get("precision"); - Assert.isInstanceOf(Number.class, precision, () -> String - .format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass())); - encryptionRangeOptions.precision(((Number) precision).intValue()); - } - return Optional.of(encryptionRangeOptions); + public Map getAttributes() { + return Map.copyOf(attributes); } /** * @return {@literal true} if no arguments set. */ boolean isEmpty() { - return !Optionals.isAnyPresent(getQueryType(), getContentionFactor(), getRangeOptions()); + return getQueryType() == null && getContentionFactor() == null && getAttributes().isEmpty(); } @Override public String toString() { return "QueryableEncryptionOptions{" + "queryType='" + queryType + '\'' + ", contentionFactor=" + contentionFactor - + ", rangeOptions=" + rangeOptions + '}'; + + ", attributes=" + attributes + '}'; } @Override @@ -251,12 +224,12 @@ public boolean equals(Object o) { if (!ObjectUtils.nullSafeEquals(contentionFactor, that.contentionFactor)) { return false; } - return ObjectUtils.nullSafeEquals(rangeOptions, that.rangeOptions); + return ObjectUtils.nullSafeEquals(attributes, that.attributes); } @Override public int hashCode() { - return Objects.hash(queryType, contentionFactor, rangeOptions); + return Objects.hash(queryType, contentionFactor, attributes); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index 4d250fba05..f83f98d4ac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -15,15 +15,21 @@ */ package org.springframework.data.mongodb.core.encryption; +import static org.springframework.data.mongodb.util.MongoCompatibilityAdapter.rangeOptionsAdapter; + +import java.util.Map; import java.util.function.Supplier; import org.bson.BsonBinary; import org.bson.BsonDocument; import org.bson.BsonValue; import org.springframework.data.mongodb.core.encryption.EncryptionKey.Type; +import org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions; +import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.util.Assert; import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.RangeOptions; import com.mongodb.client.vault.ClientEncryption; /** @@ -74,6 +80,7 @@ public ClientEncryption getClientEncryption() { } private EncryptOptions createEncryptOptions(EncryptionOptions options) { + EncryptOptions encryptOptions = new EncryptOptions(options.algorithm()); if (Type.ALT.equals(options.key().type())) { @@ -82,10 +89,58 @@ private EncryptOptions createEncryptOptions(EncryptionOptions options) { encryptOptions = encryptOptions.keyId((BsonBinary) options.key().value()); } - options.queryableEncryptionOptions().getQueryType().map(encryptOptions::queryType); - options.queryableEncryptionOptions().getContentionFactor().map(encryptOptions::contentionFactor); - options.queryableEncryptionOptions().getRangeOptions().map(encryptOptions::rangeOptions); + if (options.queryableEncryptionOptions() == null) { + return encryptOptions; + } + + QueryableEncryptionOptions qeOptions = options.queryableEncryptionOptions(); + if (qeOptions.getQueryType() != null) { + encryptOptions.queryType(qeOptions.getQueryType()); + } + if (qeOptions.getContentionFactor() != null) { + encryptOptions.contentionFactor(qeOptions.getContentionFactor()); + } + if (!qeOptions.getAttributes().isEmpty()) { + encryptOptions.rangeOptions(rangeOptions(qeOptions.getAttributes())); + } return encryptOptions; } + protected RangeOptions rangeOptions(Map attributes) { + + RangeOptions encryptionRangeOptions = new RangeOptions(); + if (attributes.isEmpty()) { + return encryptionRangeOptions; + } + + if (attributes.containsKey("min")) { + encryptionRangeOptions.min(BsonUtils.simpleToBsonValue(attributes.get("min"))); + } + if (attributes.containsKey("max")) { + encryptionRangeOptions.max(BsonUtils.simpleToBsonValue(attributes.get("max"))); + } + if (attributes.containsKey("trimFactor")) { + Object trimFactor = attributes.get("trimFactor"); + Assert.isInstanceOf(Integer.class, trimFactor, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass())); + + rangeOptionsAdapter(encryptionRangeOptions).trimFactor((Integer) trimFactor); + } + + if (attributes.containsKey("sparsity")) { + Object sparsity = attributes.get("sparsity"); + Assert.isInstanceOf(Number.class, sparsity, + () -> String.format("Expected to find a %s but it turned out to be %s.", Long.class, sparsity.getClass())); + encryptionRangeOptions.sparsity(((Number) sparsity).longValue()); + } + + if (attributes.containsKey("precision")) { + Object precision = attributes.get("precision"); + Assert.isInstanceOf(Number.class, precision, () -> String + .format("Expected to find a %s but it turned out to be %s.", Integer.class, precision.getClass())); + encryptionRangeOptions.precision(((Number) precision).intValue()); + } + return encryptionRangeOptions; + } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java index a8aedce8bc..37d1019f62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/ExplicitEncrypted.java @@ -86,24 +86,6 @@ */ String keyAltName() default ""; - /** - * Set the contention factor - *

- * Only required when using {@literal range} encryption. - * @return the contention factor - */ - long contentionFactor() default -1; - - /** - * Set the {@literal range} options - *

- * Should be valid extended json representing the range options and including the following values: - * {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}. - * - * @return the json representation of range options - */ - String rangeOptions() default ""; - /** * The {@link EncryptingConverter} type handling the {@literal en-/decryption} of the annotated property. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java new file mode 100644 index 0000000000..abebc11a5a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java @@ -0,0 +1,49 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * @author Christoph Strobl + * @since 4.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.FIELD, ElementType.ANNOTATION_TYPE }) +public @interface Queryable { + + /** + * @return empty {@link String} if not set. + * @since 4.5 + */ + String queryType() default ""; + + /** + * @return empty {@link String} if not set. + * @since 4.5 + */ + String queryAttributes() default ""; + + /** + * Set the contention factor + * + * @return the contention factor + */ + long contentionFactor() default -1; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java new file mode 100644 index 0000000000..5710c081fa --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java @@ -0,0 +1,56 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; + +/** + * @author Christoph Strobl + * @author Ross Lawley + * @since 4.5 + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.FIELD) +@Encrypted(algorithm = "Range") +@Queryable(queryType = "range") +public @interface RangeEncrypted { + + /** + * Set the contention factor + * + * @return the contention factor + */ + @AliasFor(annotation = Queryable.class, value = "contentionFactor") + long contentionFactor() default -1; + + /** + * Set the {@literal range} options + *

+ * Should be valid extended json representing the range options and including the following values: {@code min}, + * {@code max}, {@code trimFactor} and {@code sparsity}. + *

+ * Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}. + * + * @return the json representation of range options + */ + @AliasFor(annotation = Queryable.class, value = "queryAttributes") + String rangeOptions() default ""; +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java index 29cedfd6ce..c95bd4c734 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -23,8 +23,8 @@ import java.util.UUID; import org.bson.Document; - import org.springframework.data.domain.Range; +import org.springframework.data.mongodb.core.EncryptionAlgorithms; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject; @@ -1036,7 +1036,7 @@ public static class EncryptedJsonSchemaProperty implements JsonSchemaProperty { private final JsonSchemaProperty targetProperty; private final @Nullable String algorithm; - private final @Nullable String keyId; + private final @Nullable Object keyId; private final @Nullable List keyIds; /** @@ -1048,7 +1048,7 @@ public EncryptedJsonSchemaProperty(JsonSchemaProperty target) { this(target, null, null, null); } - private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable String keyId, + private EncryptedJsonSchemaProperty(JsonSchemaProperty target, @Nullable String algorithm, @Nullable Object keyId, @Nullable List keyIds) { Assert.notNull(target, "Target must not be null"); @@ -1068,13 +1068,25 @@ public static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty target) { return new EncryptedJsonSchemaProperty(target); } + /** + * Create new instance of {@link EncryptedJsonSchemaProperty} with {@literal Range} encryption, wrapping the given + * {@link JsonSchemaProperty target}. + * + * @param target must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + * @since 4.5 + */ + public static EncryptedJsonSchemaProperty rangeEncrypted(JsonSchemaProperty target) { + return new EncryptedJsonSchemaProperty(target).algorithm(EncryptionAlgorithms.RANGE); + } + /** * Use {@literal AEAD_AES_256_CBC_HMAC_SHA_512-Random} algorithm. * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { - return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Random"); + return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Random); } /** @@ -1083,7 +1095,7 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { * @return new instance of {@link EncryptedJsonSchemaProperty}. */ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() { - return algorithm("AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic"); + return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); } /** @@ -1103,6 +1115,15 @@ public EncryptedJsonSchemaProperty keyId(String keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null); } + /** + * @param keyId must not be {@literal null}. + * @return new instance of {@link EncryptedJsonSchemaProperty}. + * @since 4.5 + */ + public EncryptedJsonSchemaProperty keyId(Object keyId) { + return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null); + } + /** * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. @@ -1171,5 +1192,71 @@ private Type extractPropertyType(Document source) { return null; } + + public Object getKeyId() { + if (keyId != null) { + return keyId; + } + if (keyIds != null && keyIds.size() == 1) { + return keyIds.iterator().next(); + } + return null; + } + } + + /** + * {@link JsonSchemaProperty} implementation typically wrapping {@link EncryptedJsonSchemaProperty encrypted + * properties} to mark them as queryable. + * + * @author Christoph Strobl + * @since 4.5 + */ + public static class QueryableJsonSchemaProperty implements JsonSchemaProperty { + + private final JsonSchemaProperty targetProperty; + private final QueryCharacteristics characteristics; + + public QueryableJsonSchemaProperty(JsonSchemaProperty target, QueryCharacteristics characteristics) { + this.targetProperty = target; + this.characteristics = characteristics; + } + + @Override + public Document toDocument() { + + Document doc = targetProperty.toDocument(); + Document propertySpecification = doc.get(targetProperty.getIdentifier(), Document.class); + + if (propertySpecification.containsKey("encrypt")) { + Document encrypt = propertySpecification.get("encrypt", Document.class); + List queries = characteristics.getCharacteristics().stream().map(QueryCharacteristic::toDocument) + .toList(); + encrypt.append("queries", queries); + } + + return doc; + } + + @Override + public String getIdentifier() { + return targetProperty.getIdentifier(); + } + + @Override + public Set getTypes() { + return targetProperty.getTypes(); + } + + boolean isEncrypted() { + return targetProperty instanceof EncryptedJsonSchemaProperty; + } + + public JsonSchemaProperty getTargetProperty() { + return targetProperty; + } + + public QueryCharacteristics getCharacteristics() { + return characteristics; + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java index 8529951db2..a854c6184a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java @@ -16,10 +16,22 @@ package org.springframework.data.mongodb.core.schema; import java.util.Collection; +import java.util.List; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NullJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.NumericJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.RequiredJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.StringJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.TimestampJsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.*; import org.springframework.lang.Nullable; /** @@ -69,6 +81,18 @@ static EncryptedJsonSchemaProperty encrypted(JsonSchemaProperty property) { return EncryptedJsonSchemaProperty.encrypted(property); } + /** + * Turns the given target property into a {@link QueryableJsonSchemaProperty queryable} one, eg. for {@literal range} + * encrypted properties. + * + * @param property the queryable property. Must not be {@literal null}. + * @param queries predefined query characteristics. + * @since 4.5 + */ + static QueryableJsonSchemaProperty queryable(JsonSchemaProperty property, List queries) { + return new QueryableJsonSchemaProperty(property, new QueryCharacteristics(queries)); + } + /** * Creates a new {@link StringJsonSchemaProperty} with given {@literal identifier} of {@code type : 'string'}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java index e0f3e26100..a6fc3ab8bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MergedJsonSchema.java @@ -19,7 +19,9 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.BiFunction; +import java.util.stream.Collectors; import org.bson.Document; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java new file mode 100644 index 0000000000..2b405c56ce --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java @@ -0,0 +1,34 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.schema; + +import org.bson.Document; + +/** + * @author Christoph Strobl + * @since 4.5 + */ +public interface QueryCharacteristic { + + /** + * @return the query type, eg. {@literal range}. + */ + String queryType(); + + default Document toDocument() { + return new Document("queryType", queryType()); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java new file mode 100644 index 0000000000..ad64e85ea1 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java @@ -0,0 +1,147 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.schema; + +import java.util.Arrays; +import java.util.List; + +import org.bson.BsonNull; +import org.bson.Document; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Range.Bound; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 4.5 + */ +public class QueryCharacteristics { + + private static final QueryCharacteristics NONE = new QueryCharacteristics(List.of()); + + private final List characteristics; + + QueryCharacteristics(List characteristics) { + this.characteristics = characteristics; + } + + public static QueryCharacteristics none() { + return NONE; + } + + public static QueryCharacteristics of(List characteristics) { + return new QueryCharacteristics(List.copyOf(characteristics)); + } + + QueryCharacteristics(QueryCharacteristic... characteristics) { + this.characteristics = Arrays.asList(characteristics); + } + + public List getCharacteristics() { + return characteristics; + } + + public static RangeQuery range() { + return new RangeQuery<>(); + } + + public static EqualityQuery equality() { + return new EqualityQuery<>(null); + } + + public static class EqualityQuery implements QueryCharacteristic { + + private final @Nullable Long contention; + + public EqualityQuery(@Nullable Long contention) { + this.contention = contention; + } + + public EqualityQuery contention(long contention) { + return new EqualityQuery<>(contention); + } + + @Override + public String queryType() { + return "equality"; + } + + @Override + public Document toDocument() { + return QueryCharacteristic.super.toDocument().append("contention", contention); + } + } + + public static class RangeQuery implements QueryCharacteristic { + + private final @Nullable Range valueRange; + private final @Nullable Integer trimFactor; + private final @Nullable Long sparsity; + private final @Nullable Long contention; + + private RangeQuery() { + this(Range.unbounded(), null, null, null); + } + + public RangeQuery(Range valueRange, Integer trimFactor, Long sparsity, Long contention) { + this.valueRange = valueRange; + this.trimFactor = trimFactor; + this.sparsity = sparsity; + this.contention = contention; + } + + @Override + public String queryType() { + return "range"; + } + + public RangeQuery min(T lower) { + + Range range = Range.of(Bound.inclusive(lower), + valueRange != null ? valueRange.getUpperBound() : Bound.unbounded()); + return new RangeQuery<>(range, trimFactor, sparsity, contention); + } + + public RangeQuery max(T upper) { + + Range range = Range.of(valueRange != null ? valueRange.getLowerBound() : Bound.unbounded(), + Bound.inclusive(upper)); + return new RangeQuery<>(range, trimFactor, sparsity, contention); + } + + public RangeQuery trimFactor(int trimFactor) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + } + + public RangeQuery sparsity(long sparsity) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + } + + public RangeQuery contention(long contention) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + } + + @Override + @SuppressWarnings("unchecked") + public Document toDocument() { + + return QueryCharacteristic.super.toDocument().append("contention", contention).append("trimFactor", trimFactor) + .append("sparsity", sparsity).append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)) + .append("max", valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE)); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index b17b9f1963..a61fe0eca8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -57,8 +57,18 @@ public class MongoCompatibilityAdapter { private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class); - private static final @Nullable Method setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor", - Integer.class); + private static final @Nullable Method setTrimFactor; + + static { + + // method name changed in between + Method trimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor", Integer.class); + if (trimFactor != null) { + setTrimFactor = trimFactor; + } else { + setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "trimFactor", Integer.class); + } + } /** * Return a compatibility adapter for {@link MongoClientSettings.Builder}. @@ -128,6 +138,23 @@ public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) }; } + /** + * Return a compatibility adapter for {@link RangeOptions}. + * + * @param options + * @return + */ + public static RangeOptionsAdapter rangeOptionsAdapter(RangeOptions options) { + return trimFactor -> { + + if (!MongoClientVersion.isVersion5orNewer() || setTrimFactor == null) { + throw new UnsupportedOperationException(NOT_SUPPORTED_ON_4.formatted("RangeOptions.trimFactor")); + } + + ReflectionUtils.invokeMethod(setTrimFactor, options, trimFactor); + }; + } + /** * Return a compatibility adapter for {@code MapReducePublisher}. * diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java index f2691275c3..9de0863cd2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/CollectionOptionsUnitTests.java @@ -15,12 +15,24 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.CollectionOptions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; +import static org.springframework.data.mongodb.core.CollectionOptions.TimeSeriesOptions; +import static org.springframework.data.mongodb.core.CollectionOptions.emitChangedRevisions; +import static org.springframework.data.mongodb.core.CollectionOptions.empty; +import static org.springframework.data.mongodb.core.CollectionOptions.encryptedCollection; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; +import java.util.List; + +import org.bson.BsonNull; import org.bson.Document; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.core.schema.QueryCharacteristics; import org.springframework.data.mongodb.core.validation.Validator; /** @@ -76,4 +88,93 @@ void validatorEquals() { .isNotEqualTo(empty().validator(Validator.document(new Document("three", "four")))) .isNotEqualTo(empty().validator(Validator.document(new Document("one", "two"))).moderateValidation()); } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionOptionsFromSchemaRenderCorrectly() { + + MongoJsonSchema schema = MongoJsonSchema.builder() + .property(JsonSchemaProperty.object("spring") + .properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of()))) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build(); + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(schema); + + assertThat(encryptionOptions.toDocument().get("fields", List.class)).hasSize(2) + .contains(new Document("path", "mongodb").append("bsonType", "long").append("queries", List.of()) + .append("keyId", BsonNull.VALUE)) + .contains(new Document("path", "spring.data").append("bsonType", "int").append("queries", List.of()) + .append("keyId", BsonNull.VALUE)); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverrideByPath() { + + CollectionOptions collectionOptions = encryptedCollection(options -> options // + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring"))) + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data"))) + + // override first with data type long + .queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverridesPathFromSchema() { + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder() + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("spring")), List.of())) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("data")), List.of())).build()); + + // override spring from schema with data type long + CollectionOptions collectionOptions = CollectionOptions.encryptedCollection( + encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } + + @Test // GH-4185 + void encryptionOptionsAreImmutable() { + + EncryptedFieldsOptions source = EncryptedFieldsOptions + .fromProperties(List.of(queryable(int32("spring.data"), List.of(QueryCharacteristics.range().min(1))))); + + assertThat(source.queryable(queryable(int32("mongodb"), List.of(QueryCharacteristics.range().min(1))))) + .isNotSameAs(source).satisfies(it -> { + assertThat(it.toDocument().get("fields", List.class)).hasSize(2); + }); + + assertThat(source.toDocument().get("fields", List.class)).hasSize(1); + } + + @Test // GH-4185 + @SuppressWarnings("unchecked") + void queryableEncryptionPropertiesOverridesNestedPathFromSchema() { + + EncryptedFieldsOptions encryptionOptions = EncryptedFieldsOptions.fromSchema(MongoJsonSchema.builder() + .property(JsonSchemaProperty.object("spring") + .properties(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int32("data")), List.of()))) + .property(queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("mongodb")), List.of())).build()); + + // override spring from schema with data type long + CollectionOptions collectionOptions = CollectionOptions.encryptedCollection( + encryptionOptions.queryable(JsonSchemaProperty.encrypted(JsonSchemaProperty.int64("spring.data")))); + + assertThat(collectionOptions.getEncryptedFieldsOptions()).map(EncryptedFieldsOptions::toDocument) + .hasValueSatisfying(it -> { + assertThat(it.get("fields", List.class)).hasSize(2).contains(new Document("path", "spring.data") + .append("bsonType", "long").append("queries", List.of()).append("keyId", BsonNull.VALUE)); + }); + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java index af4fac84b1..78a6e6b496 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultIndexOperationsIntegrationTests.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.index.PartialIndexFilter.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.index.PartialIndexFilter.of; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import org.bson.BsonDocument; import org.bson.Document; @@ -79,7 +79,7 @@ public void shouldApplyPartialFilterCorrectly() { IndexDefinition id = new Index().named("partial-with-criteria").on("k3y", Direction.ASC) .partial(of(where("q-t-y").gte(10))); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-criteria"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -92,7 +92,7 @@ public void shouldApplyPartialFilterWithMappedPropertyCorrectly() { IndexDefinition id = new Index().named("partial-with-mapped-criteria").on("k3y", Direction.ASC) .partial(of(where("quantity").gte(10))); - template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).ensureIndex(id); + template.indexOps(DefaultIndexOperationsIntegrationTestsSample.class).createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-mapped-criteria"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -105,7 +105,7 @@ public void shouldApplyPartialDBOFilterCorrectly() { IndexDefinition id = new Index().named("partial-with-dbo").on("k3y", Direction.ASC) .partial(of(new org.bson.Document("qty", new org.bson.Document("$gte", 10)))); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-dbo"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -120,7 +120,7 @@ public void shouldFavorExplicitMappingHintViaClass() { indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(id); + indexOps.createIndex(id); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "partial-with-inheritance"); assertThat(Document.parse(info.getPartialFilterExpression())) @@ -150,7 +150,7 @@ public void shouldCreateIndexWithCollationCorrectly() { new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(id); + indexOps.createIndex(id); Document expected = new Document("locale", "de_AT") // .append("caseLevel", false) // @@ -179,7 +179,7 @@ void indexShouldNotBeHiddenByDefault() { IndexDefinition index = new Index().named("my-index").on("a", Direction.ASC); indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(index); + indexOps.createIndex(index); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-index"); assertThat(info.isHidden()).isFalse(); @@ -191,7 +191,7 @@ void shouldCreateHiddenIndex() { IndexDefinition index = new Index().named("my-hidden-index").on("a", Direction.ASC).hidden(); indexOps = new DefaultIndexOperations(template, COLLECTION_NAME, MappingToSameCollection.class); - indexOps.ensureIndex(index); + indexOps.createIndex(index); IndexInfo info = findAndReturnIndexInfo(indexOps.getIndexInfo(), "my-hidden-index"); assertThat(info.isHidden()).isTrue(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java index d18ed6f119..adaecad5da 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreatorUnitTests.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatExceptionOfType; import java.util.Collections; import java.util.Date; @@ -38,6 +39,8 @@ import org.springframework.data.mongodb.core.mapping.FieldType; import org.springframework.data.mongodb.core.mapping.MongoId; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.Queryable; +import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; @@ -282,6 +285,48 @@ void wrapEncryptedEntityTypeLikeProperty() { .containsEntry("properties.domainTypeValue", Document.parse("{'encrypt': {'bsonType': 'object' } }")); } + @Test // GH-4185 + void qeRangeEncryptedProperties() { + + MongoJsonSchema schema = MongoJsonSchemaCreator.create() // + .filter(MongoJsonSchemaCreator.encryptedOnly()) // filter non encrypted fields + .createSchemaFor(QueryableEncryptedRoot.class); + + String expectedForInt = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'int', + 'queries' : [ + { 'queryType' : 'range', 'contention' : { '$numberLong' : '0' }, 'max' : 200, 'min' : 0, 'sparsity' : 1, 'trimFactor' : 1 } + ] + }}"""; + + String expectedForRootLong = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'long', + 'queries' : [ + { 'queryType' : 'range', contention : { '$numberLong' : '0' }, 'sparsity' : 0 } + ] + }}"""; + + String expectedForNestedLong = """ + { 'encrypt' : { + 'algorithm' : 'Range', + 'bsonType' : 'long', + 'queries' : [ + { 'queryType' : 'range', contention : { '$numberLong' : '1' }, 'max' : { '$numberLong' : '1' }, 'min' : { '$numberLong' : '-1' }, 'sparsity' : 1, 'trimFactor' : 1 } + ] + }}"""; + + assertThat(schema.schemaDocument()) // + .doesNotContainKey("properties.unencrypted") // + .containsEntry("properties.encryptedInt", Document.parse(expectedForInt)) + .containsEntry("properties.encryptedLong", Document.parse(expectedForRootLong)) + .containsEntry("properties.nested.properties.encrypted_long", Document.parse(expectedForNestedLong)); + + } + // --> TYPES AND JSON // --> ENUM @@ -311,7 +356,8 @@ enum JustSomeEnum { " 'binaryDataProperty' : { 'bsonType' : 'binData' }," + // " 'collectionProperty' : { 'type' : 'array' }," + // " 'simpleTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'string' } }," + // - " 'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }" + // + " 'complexTypeCollectionProperty' : { 'type' : 'array', 'items' : { 'type' : 'object', 'properties' : { 'field' : { 'type' : 'string'} } } }" + + // " 'enumTypeCollectionProperty' : { 'type' : 'array', 'items' : " + JUST_SOME_ENUM + " }" + // " 'mapProperty' : { 'type' : 'object' }," + // " 'objectProperty' : { 'type' : 'object' }," + // @@ -692,4 +738,28 @@ static class PropertyClashWithA { static class WithEncryptedEntityLikeProperty { @Encrypted SomeDomainType domainTypeValue; } + + static class QueryableEncryptedRoot { + + String unencrypted; + + @RangeEncrypted(contentionFactor = 0L, rangeOptions = "{ 'min': 0, 'max': 200, 'trimFactor': 1, 'sparsity': 1}") // + Integer encryptedInt; + + @Encrypted(algorithm = "Range") + @Queryable(contentionFactor = 0L, queryType = "range", queryAttributes = "{ 'sparsity': 0 }") // + Long encryptedLong; + + NestedRangeEncrypted nested; + + } + + static class NestedRangeEncrypted { + + @Field("encrypted_long") + @RangeEncrypted(contentionFactor = 1L, + rangeOptions = "{ 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }, 'trimFactor': 1, 'sparsity': 1}") // + Long encryptedLong; + } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java new file mode 100644 index 0000000000..d88bde68a2 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java @@ -0,0 +1,140 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.encryption; + +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int64; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; +import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.range; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; + +import java.util.List; +import java.util.UUID; +import java.util.stream.Stream; + +import org.bson.BsonBinary; +import org.bson.Document; +import org.bson.UuidRepresentation; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.test.context.ContextConfiguration; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import com.mongodb.client.MongoClient; + +/** + * @author Christoph Strobl + */ +@ExtendWith({ MongoClientExtension.class, SpringExtension.class }) +@ContextConfiguration +public class MongoQueryableEncryptionCollectionCreationTests { + + public static final String COLLECTION_NAME = "enc-collection"; + static @Client MongoClient mongoClient; + + @Configuration + static class Config extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mongoClient; + } + + @Override + protected String getDatabaseName() { + return "encryption-schema-tests"; + } + + } + + @Autowired MongoTemplate template; + + @BeforeEach + void beforeEach() { + template.dropCollection(COLLECTION_NAME); + } + + @ParameterizedTest // GH-4185 + @MethodSource("collectionOptions") + public void createsCollectionWithEncryptedFieldsCorrectly(CollectionOptions collectionOptions) { + + template.createCollection(COLLECTION_NAME, collectionOptions); + + Document encryptedFields = readEncryptedFieldsFromDatabase(COLLECTION_NAME); + assertThat(encryptedFields).containsKey("fields"); + + List fields = encryptedFields.get("fields", List.of()); + assertThat(fields.get(0)).containsEntry("path", "encryptedInt") // + .containsEntry("bsonType", "int") // + .containsEntry("queries", List + .of(Document.parse("{'queryType': 'range', 'contention': { '$numberLong' : '1' }, 'min': 5, 'max': 100}"))); + + assertThat(fields.get(1)).containsEntry("path", "nested.encryptedLong") // + .containsEntry("bsonType", "long") // + .containsEntry("queries", List.of(Document.parse( + "{'queryType': 'range', 'contention': { '$numberLong' : '0' }, 'min': { '$numberLong' : '-1' }, 'max': { '$numberLong' : '1' }}"))); + } + + private static Stream collectionOptions() { + + BsonBinary key1 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD); + BsonBinary key2 = new BsonBinary(UUID.randomUUID(), UuidRepresentation.STANDARD); + + CollectionOptions manualOptions = CollectionOptions.encryptedCollection(options -> options // + .queryable(encrypted(int32("encryptedInt")).keys(key1), range().min(5).max(100).contention(1)) // + .queryable(encrypted(JsonSchemaProperty.int64("nested.encryptedLong")).keys(key2), + range().min(-1L).max(1L).contention(0))); + + CollectionOptions schemaOptions = CollectionOptions.encryptedCollection(MongoJsonSchema.builder() + .property( + queryable(encrypted(int32("encryptedInt")).keyId(key1), List.of(range().min(5).max(100).contention(1)))) + .property(queryable(encrypted(int64("nested.encryptedLong")).keyId(key2), + List.of(range().min(-1L).max(1L).contention(0)))) + .build()); + + return Stream.of(Arguments.of(manualOptions), Arguments.of(schemaOptions)); + } + + Document readEncryptedFieldsFromDatabase(String collectionName) { + + Document collectionInfo = template + .executeCommand(new Document("listCollections", 1).append("filter", new Document("name", collectionName))); + + if (collectionInfo.containsKey("cursor")) { + collectionInfo = (Document) collectionInfo.get("cursor", Document.class).get("firstBatch", List.class).iterator() + .next(); + } + + if (!collectionInfo.containsKey("options")) { + return new Document(); + } + + return collectionInfo.get("options", Document.class).get("encryptedFields", Document.class); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java index 2c5e3abc6b..2ea0b5b218 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java @@ -15,45 +15,28 @@ */ package org.springframework.data.mongodb.core.encryption; -import static java.util.Arrays.*; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.EncryptionAlgorithms.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.springframework.data.mongodb.core.query.Criteria.where; import java.security.SecureRandom; +import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; import java.util.function.Supplier; -import java.util.stream.Collectors; -import com.mongodb.AutoEncryptionSettings; -import com.mongodb.ClientEncryptionSettings; -import com.mongodb.ConnectionString; -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoNamespace; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoClients; -import com.mongodb.client.MongoCollection; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.CreateEncryptedCollectionParams; -import com.mongodb.client.model.Filters; -import com.mongodb.client.model.IndexOptions; -import com.mongodb.client.model.Indexes; -import com.mongodb.client.vault.ClientEncryption; -import com.mongodb.client.vault.ClientEncryptions; - -import org.bson.BsonArray; +import org.assertj.core.api.Assumptions; import org.bson.BsonBinary; import org.bson.BsonDocument; import org.bson.BsonInt32; -import org.bson.BsonInt64; -import org.bson.BsonNull; import org.bson.BsonString; -import org.bson.BsonValue; import org.bson.Document; +import org.junit.Before; import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.beans.factory.DisposableBean; @@ -61,20 +44,53 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.data.convert.PropertyValueConverterFactory; +import org.springframework.data.convert.ValueConverter; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.core.CollectionOptions; +import org.springframework.data.mongodb.core.CollectionOptions.EncryptedFieldsOptions; +import org.springframework.data.mongodb.core.MongoJsonSchemaCreator; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.convert.encryption.MongoEncryptionConverter; -import org.springframework.data.mongodb.core.mapping.ExplicitEncrypted; +import org.springframework.data.mongodb.core.mapping.Encrypted; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.Queryable; +import org.springframework.data.mongodb.core.mapping.RangeEncrypted; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.Update; +import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.util.Lazy; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; +import org.springframework.util.StringUtils; + +import com.mongodb.AutoEncryptionSettings; +import com.mongodb.ClientEncryptionSettings; +import com.mongodb.ConnectionString; +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoNamespace; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateEncryptedCollectionParams; +import com.mongodb.client.model.Filters; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.client.model.Indexes; +import com.mongodb.client.model.vault.EncryptOptions; +import com.mongodb.client.model.vault.RangeOptions; +import com.mongodb.client.result.UpdateResult; +import com.mongodb.client.vault.ClientEncryption; +import com.mongodb.client.vault.ClientEncryptions; /** * @author Ross Lawley + * @author Christoph Strobl */ @ExtendWith({ MongoClientExtension.class, SpringExtension.class }) @EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") @@ -83,41 +99,128 @@ class RangeEncryptionTests { @Autowired MongoTemplate template; + @Autowired MongoClientEncryption clientEncryption; + @Autowired EncryptionKeyHolder keyHolder; + + @BeforeEach + void clientVersionCheck() { + Assumptions.assumeThat(MongoClientVersion.isVersion5orNewer()).isTrue(); + } @AfterEach void tearDown() { template.getDb().getCollection("test").deleteMany(new BsonDocument()); } - @Test - void canGreaterThanEqualMatchRangeEncryptedField() { + @Test // GH-4185 + void manuallyEncryptedValuesCanBeSavedAndRetrievedCorrectly() { + + EncryptOptions encryptOptions = new EncryptOptions("Range").contentionFactor(1L) + .keyId(keyHolder.getEncryptionKey("encryptedInt")) + .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200)).sparsity(1L)); + + EncryptOptions encryptExpressionOptions = new EncryptOptions("Range").contentionFactor(1L) + .rangeOptions(new RangeOptions().min(new BsonInt32(0)).max(new BsonInt32(200))) + .keyId(keyHolder.getEncryptionKey("encryptedInt")).queryType("range"); + + EncryptOptions equalityEncOptions = new EncryptOptions("Indexed").contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("age")); + ; + + EncryptOptions equalityEncOptionsString = new EncryptOptions("Indexed").contentionFactor(0L) + .keyId(keyHolder.getEncryptionKey("name")); + ; + + Document source = new Document("_id", "id-1"); + + source.put("name", + clientEncryption.getClientEncryption().encrypt(new BsonString("It's a Me, Mario!"), equalityEncOptionsString)); + source.put("age", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), equalityEncOptions)); + source.put("encryptedInt", clientEncryption.getClientEncryption().encrypt(new BsonInt32(101), encryptOptions)); + source.put("_class", Person.class.getName()); + + template.execute(Person.class, col -> col.insertOne(source)); + + Document result = template.execute(Person.class, col -> { + + BsonDocument filterSource = new BsonDocument("encryptedInt", new BsonDocument("$gte", new BsonInt32(100))); + BsonDocument filter = clientEncryption.getClientEncryption() + .encryptExpression(new Document("$and", List.of(filterSource)), encryptExpressionOptions); + + return col.find(filter).first(); + }); + + assertThat(result).containsEntry("encryptedInt", 101); + } + + @Test // GH-4185 + void canLesserThanEqualMatchRangeEncryptedField() { + Person source = createPerson(); template.insert(source); - Person loaded = template.query(Person.class).matching(where("encryptedInt").gte(source.encryptedInt)).firstValue(); + Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue(); assertThat(loaded).isEqualTo(source); } - @Test - void canLesserThanEqualMatchRangeEncryptedField() { + @Test // GH-4185 + void canQueryMixOfEqualityEncryptedAndUnencrypted() { + + Person source = template.insert(createPerson()); + + Person loaded = template.query(Person.class) + .matching(where("name").is(source.name).and("unencryptedValue").is(source.unencryptedValue)).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canQueryMixOfRangeEncryptedAndUnencrypted() { + + Person source = template.insert(createPerson()); + + Person loaded = template.query(Person.class) + .matching(where("encryptedInt").lte(source.encryptedInt).and("unencryptedValue").is(source.unencryptedValue)) + .firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 + void canQueryEqualityEncryptedField() { + Person source = createPerson(); template.insert(source); - Person loaded = template.query(Person.class).matching(where("encryptedInt").lte(source.encryptedInt)).firstValue(); + Person loaded = template.query(Person.class).matching(where("age").is(source.age)).firstValue(); assertThat(loaded).isEqualTo(source); } - @Test + @Test // GH-4185 + void canExcludeSafeContentFromResult() { + + Person source = createPerson(); + template.insert(source); + + Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L)); + q.fields().exclude("__safeContent__"); + + Person loaded = template.query(Person.class).matching(q).firstValue(); + assertThat(loaded).isEqualTo(source); + } + + @Test // GH-4185 void canRangeMatchRangeEncryptedField() { + Person source = createPerson(); template.insert(source); - Person loaded = template.query(Person.class).matching(where("encryptedLong").lte(1001L).gte(1001L)).firstValue(); + Query q = Query.query(where("encryptedLong").lte(1001L).gte(1001L)); + Person loaded = template.query(Person.class).matching(q).firstValue(); assertThat(loaded).isEqualTo(source); } - @Test - void canUpdateRangeEncryptedField() { + @Test // GH-4185 + void canReplaceEntityWithRangeEncryptedField() { + Person source = createPerson(); template.insert(source); @@ -129,8 +232,23 @@ void canUpdateRangeEncryptedField() { assertThat(loaded).isEqualTo(source); } - @Test + @Test // GH-4185 + void canUpdateRangeEncryptedField() { + + Person source = createPerson(); + template.insert(source); + + UpdateResult updateResult = template.update(Person.class).matching(where("id").is(source.id)) + .apply(Update.update("encryptedLong", 5000L)).first(); + assertThat(updateResult.getModifiedCount()).isOne(); + + Person loaded = template.query(Person.class).matching(where("id").is(source.id)).firstValue(); + assertThat(loaded.encryptedLong).isEqualTo(5000L); + } + + @Test // GH-4185 void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() { + Person source = createPerson(); template.insert(source); @@ -139,11 +257,11 @@ void errorsWhenUsingNonRangeOperatorEqOnRangeEncryptedField() { .isInstanceOf(AssertionError.class) .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + "the query operator '$eq' for field path 'encryptedInt' is not a range query."); - } - @Test + @Test // GH-4185 void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() { + Person source = createPerson(); template.insert(source); @@ -152,14 +270,19 @@ void errorsWhenUsingNonRangeOperatorInOnRangeEncryptedField() { .isInstanceOf(AssertionError.class) .hasMessageStartingWith("Not a valid range query. Querying a range encrypted field but " + "the query operator '$in' for field path 'encryptedLong' is not a range query."); - } private Person createPerson() { + Person source = new Person(); source.id = "id-1"; + source.unencryptedValue = "y2k"; + source.name = "it's a me mario!"; + source.age = 42; source.encryptedInt = 101; source.encryptedLong = 1001L; + source.nested = new NestedWithQEFields(); + source.nested.value = "Luigi time!"; return source; } @@ -193,37 +316,63 @@ protected void configureConverters(MongoConverterConfigurationAdapter converterC } @Bean - MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption) { + EncryptionKeyHolder keyHolder(MongoClientEncryption mongoClientEncryption) { + Lazy> lazyDataKeyMap = Lazy.of(() -> { try (MongoClient client = mongoClient()) { + MongoDatabase database = client.getDatabase(getDatabaseName()); database.getCollection("test").drop(); ClientEncryption clientEncryption = mongoClientEncryption.getClientEncryption(); - BsonDocument encryptedFields = new BsonDocument().append("fields", - new BsonArray(asList( - new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedInt")) - .append("bsonType", new BsonString("int")) - .append("queries", - new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L)) - .append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1)) - .append("min", new BsonInt32(0)).append("max", new BsonInt32(200))), - new BsonDocument("keyId", BsonNull.VALUE).append("path", new BsonString("encryptedLong")) - .append("bsonType", new BsonString("long")).append("queries", - new BsonDocument("queryType", new BsonString("range")).append("contention", new BsonInt64(0L)) - .append("trimFactor", new BsonInt32(1)).append("sparsity", new BsonInt64(1)) - .append("min", new BsonInt64(1000)).append("max", new BsonInt64(9999)))))); - - BsonDocument local = clientEncryption.createEncryptedCollection(database, "test", - new CreateCollectionOptions().encryptedFields(encryptedFields), + + MongoJsonSchema personSchema = MongoJsonSchemaCreator.create(new MongoMappingContext()) // init schema creator + .filter(MongoJsonSchemaCreator.encryptedOnly()) // + .createSchemaFor(Person.class); // + + Document encryptedFields = CollectionOptions.encryptedCollection(personSchema) // + .getEncryptedFieldsOptions() // + .map(EncryptedFieldsOptions::toDocument) // + .orElseThrow(); + + CreateCollectionOptions createCollectionOptions = new CreateCollectionOptions() + .encryptedFields(encryptedFields); + + BsonDocument local = clientEncryption.createEncryptedCollection(database, "test", createCollectionOptions, new CreateEncryptedCollectionParams(LOCAL_KMS_PROVIDER)); - return local.getArray("fields").stream().map(BsonValue::asDocument).collect( - Collectors.toMap(field -> field.getString("path").getValue(), field -> field.getBinary("keyId"))); + Map keyMap = new LinkedHashMap<>(); + for (Object o : local.getArray("fields")) { + if (o instanceof BsonDocument db) { + String path = db.getString("path").getValue(); + BsonBinary binary = db.getBinary("keyId"); + for (String part : path.split("\\.")) { + keyMap.put(part, binary); + } + } + } + return keyMap; } }); - return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver - .annotated((ctx) -> EncryptionKey.keyId(lazyDataKeyMap.get().get(ctx.getProperty().getFieldName())))); + + return new EncryptionKeyHolder(lazyDataKeyMap); + } + + @Bean + MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEncryption, + EncryptionKeyHolder keyHolder) { + return new MongoEncryptionConverter(mongoClientEncryption, EncryptionKeyResolver.annotated((ctx) -> { + + String path = ctx.getProperty().getFieldName(); + + if (ctx.getProperty().getMongoField().getName().isPath()) { + path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), "."); + } + if (ctx.getOperatorContext() != null) { + path = ctx.getOperatorContext().getPath(); + } + return EncryptionKey.keyId(keyHolder.getEncryptionKey(path)); + })); } @Bean @@ -291,16 +440,47 @@ public void destroy() { } } + static class EncryptionKeyHolder { + + Supplier> lazyDataKeyMap; + + public EncryptionKeyHolder(Supplier> lazyDataKeyMap) { + this.lazyDataKeyMap = Lazy.of(lazyDataKeyMap); + } + + BsonBinary getEncryptionKey(String path) { + return lazyDataKeyMap.get().get(path); + } + } + @org.springframework.data.mongodb.core.mapping.Document("test") static class Person { String id; + + String unencryptedValue; + + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // String name; - @ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L, - rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") Integer encryptedInt; - @ExplicitEncrypted(algorithm = RANGE, contentionFactor = 0L, - rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") Long encryptedLong; + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // + Integer age; + + @ValueConverter(MongoEncryptionConverter.class) + @RangeEncrypted(contentionFactor = 0L, + rangeOptions = "{\"min\": 0, \"max\": 200, \"trimFactor\": 1, \"sparsity\": 1}") // + Integer encryptedInt; + + @ValueConverter(MongoEncryptionConverter.class) + @RangeEncrypted(contentionFactor = 0L, + rangeOptions = "{\"min\": {\"$numberLong\": \"1000\"}, \"max\": {\"$numberLong\": \"9999\"}, \"trimFactor\": 1, \"sparsity\": 1}") // + Long encryptedLong; + + NestedWithQEFields nested; public String getId() { return this.id; @@ -336,29 +516,57 @@ public void setEncryptedLong(Long encryptedLong) { @Override public boolean equals(Object o) { - if (this == o) + if (o == this) { return true; - if (o == null || getClass() != o.getClass()) + } + if (o == null || getClass() != o.getClass()) { return false; - + } Person person = (Person) o; - return Objects.equals(id, person.id) && Objects.equals(name, person.name) + return Objects.equals(id, person.id) && Objects.equals(unencryptedValue, person.unencryptedValue) + && Objects.equals(name, person.name) && Objects.equals(age, person.age) && Objects.equals(encryptedInt, person.encryptedInt) && Objects.equals(encryptedLong, person.encryptedLong); } @Override public int hashCode() { - int result = Objects.hashCode(id); - result = 31 * result + Objects.hashCode(name); - result = 31 * result + Objects.hashCode(encryptedInt); - result = 31 * result + Objects.hashCode(encryptedLong); - return result; + return Objects.hash(id, unencryptedValue, name, age, encryptedInt, encryptedLong); + } + + @Override + public String toString() { + return "Person{" + "id='" + id + '\'' + ", unencryptedValue='" + unencryptedValue + '\'' + ", name='" + name + + '\'' + ", age=" + age + ", encryptedInt=" + encryptedInt + ", encryptedLong=" + encryptedLong + '}'; } + } + + static class NestedWithQEFields { + + @ValueConverter(MongoEncryptionConverter.class) + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) // + String value; @Override public String toString() { - return "Person{" + "id='" + id + '\'' + ", name='" + name + '\'' + ", encryptedInt=" + encryptedInt - + ", encryptedLong=" + encryptedLong + '}'; + return "NestedWithQEFields{" + "value='" + value + '\'' + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + NestedWithQEFields that = (NestedWithQEFields) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return Objects.hash(value); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java index 3514927b18..e2c385464c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java @@ -15,11 +15,17 @@ */ package org.springframework.data.mongodb.core.schema; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.rangeEncrypted; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.number; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.string; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.test.util.Assertions.assertThatIllegalArgumentException; import java.util.Arrays; import java.util.Collections; +import java.util.List; import java.util.UUID; import org.bson.Document; @@ -105,6 +111,23 @@ void rendersEncryptedPropertyWithKeyIdCorrectly() { .append("algorithm", "AEAD_AES_256_CBC_HMAC_SHA_512-Deterministic").append("bsonType", "string")))))); } + @Test // GH-4185 + void rendersQueryablePropertyCorrectly() { + + MongoJsonSchema schema = MongoJsonSchema.builder().properties( // + queryable(rangeEncrypted(number("ssn")), + List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200)))) + .build(); + + assertThat(schema.toDocument()).isEqualTo(new Document("$jsonSchema", + new Document("type", "object").append("properties", + new Document("ssn", + new Document("encrypt", + new Document("bsonType", "long").append("algorithm", "Range").append("queries", + List.of(new Document("contention", 0L).append("trimFactor", 1).append("sparsity", 1L) + .append("queryType", "range").append("min", 0).append("max", 200)))))))); + } + @Test // DATAMONGO-1835 void throwsExceptionOnNullRoot() { assertThatIllegalArgumentException().isThrownBy(() -> MongoJsonSchema.of((JsonSchemaObject) null)); diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc index 98a6d2478a..a4aae23748 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc @@ -1,8 +1,8 @@ [[mongo.encryption]] -= Encryption (CSFLE) += Encryption Client Side Encryption is a feature that encrypts data in your application before it is sent to MongoDB. -We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data. +We recommend you get familiar with the concepts, ideally from the https://www.mongodb.com/docs/manual/core/security-in-use-encryption/[MongoDB Documentation] to learn more about its capabilities and restrictions before you continue applying Encryption through Spring Data. [NOTE] ==== @@ -11,8 +11,13 @@ MongoDB does not support encryption for all field types. Specific data types require deterministic encryption to preserve equality comparison functionality. ==== +== Client Side Field Level Encryption (CSFLE) + +Choosing CSFLE gives you full flexibility and allows you to use different keys for a single field, eg. in a one key per tenant scenario. + +Please make sure to consult the https://www.mongodb.com/docs/manual/core/csfle/[MongoDB CSFLE Documentation] before you continue reading. + [[mongo.encryption.automatic]] -== Automatic Encryption +=== Automatic Encryption (CSFLE) MongoDB supports https://www.mongodb.com/docs/manual/core/csfle/[Client-Side Field Level Encryption] out of the box using the MongoDB driver with its Automatic Encryption feature. Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step. @@ -47,7 +52,7 @@ MongoClientSettingsBuilderCustomizer customizer(MappingContext mappingContext) { ---- [[mongo.encryption.explicit]] -== Explicit Encryption +=== Explicit Encryption (CSFLE) Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks. The `@ExplicitEncrypted` annotation is a combination of the `@Encrypted` annotation used for xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation] and a xref:mongodb/mapping/property-converters.adoc[Property Converter]. @@ -114,8 +119,147 @@ By default, the `@ExplicitEncrypted(value=…)` attribute references a `MongoEnc It is possible to change the default implementation and exchange it with any `PropertyValueConverter` implementation by providing the according type reference. To learn more about custom `PropertyValueConverters` and the required configuration, please refer to the xref:mongodb/mapping/property-converters.adoc[Property Converters - Mapping specific fields] section. +[[mongo.encryption.queryable]] +== Queryable Encryption (QE) + +Choosing QE enables you to run different types of queries, like _range_ or _equality_, against encrypted fields. + +Please make sure to consult the https://www.mongodb.com/docs/manual/core/queryable-encryption/[MongoDB QE Documentation] before you continue reading to learn more about QE features and limitations. + +=== Collection Setup + +Queryable Encryption requires upfront declaration of certain aspects allowed within an actual query against an encrypted field. +The information covers the algorithm in use as well as allowed query types along with their attributes and must be provided when creating the collection. + +`MongoOperations#createCollection(...)` can be used to do the initial setup for collections utilizing QE. +The configuration for QE via Spring Data uses the same building blocks (a xref:mongodb/mapping/mapping-schema.adoc#mongo.jsonSchema.encrypted-fields[JSON Schema creation]) as CSFLE, converting the schema/properties into the configuration format required by MongoDB. + +[tabs] +====== +Manual Collection Setup:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="primary"] +---- +CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(options -> options + .queryable(encrypted(string("ssn")).algorithm("Indexed"), equality().contention(0)) + .queryable(encrypted(int32("age")).algorithm("Range"), range().contention(8).min(0).max(150)) + .queryable(encrypted(int64("address.sign")).algorithm("Range"), range().contention(2).min(-10L).max(10L)) +); + +mongoTemplate.createCollection(Patient.class, collectionOptions); <1> +---- +<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library. +==== + +Derived Collection Setup:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="secondary"] +---- +class Patient { + + @Id String id; + + @Encrypted(algorithm = "Indexed") // + @Queryable(queryType = "equality", contentionFactor = 0) + String ssn; + + @RangeEncrypted(contentionFactor = 8, rangeOptions = "{ 'min' : 0, 'max' : 150 }") + Integer age; + + Address address; +} + +MongoJsonSchema patientSchema = MongoJsonSchemaCreator.create(mappingContext) + .filter(MongoJsonSchemaCreator.encryptedOnly()) + .createSchemaFor(Patient.class); + +CollectionOptions collectionOptions = CollectionOptions.encryptedCollection(patientSchema); + +mongoTemplate.createCollection(Patient.class, collectionOptions); <1> +---- +<1> Using the template to create the collection may prevent capturing generated keyIds. In this case render the `Document` from the options and use the `createEncryptedCollection(...)` method via the encryption library. + +The `Queryable` annotation allows to define allowed query types for encrypted fields. +`@RangeEncrypted` is a combination of `@Encrypted` and `@Queryable` for fields allowing `range` queries. +It is possible to create custom annotations out of the provided ones. +==== + +MongoDB Collection Info:: ++ +==== +[source,java,indent=0,subs="verbatim,quotes",role="thrid"] +---- +{ + name: 'patient', + type: 'collection', + options: { + encryptedFields: { + escCollection: 'enxcol_.test.esc', + ecocCollection: 'enxcol_.test.ecoc', + fields: [ + { + keyId: ..., + path: 'ssn', + bsonType: 'string', + queries: [ { queryType: 'equality', contention: Long('0') } ] + }, + { + keyId: ..., + path: 'age', + bsonType: 'int', + queries: [ { queryType: 'range', contention: Long('8'), min: 0, max: 150 } ] + }, + { + keyId: ..., + path: 'address.sign', + bsonType: 'long', + queries: [ { queryType: 'range', contention: Long('2'), min: Long('-10'), max: Long('10') } ] + } + ] + } + } +} +---- +==== +====== + +[NOTE] +==== +- It is not possible to use both QE and CSFLE within the same collection. +- It is not possible to query a `range` indexed field with an `equality` operator. +- It is not possible to query an `equality` indexed field with a `range` operator. +- It is not possible to set `bypassAutoEncrytion(true)`. +- It is not possible to use self maintained encryption keys via `@Encrypted` in combination with Queryable Encryption. +- Contention is only optional on the server side, the clients requires you to set the value (Default us `8`). +- Additional options for eg. `min` and `max` need to match the actual field type. Make sure to use `$numberLong` etc. to ensure target types when parsing bson String. +- Queryable Encryption will an extra field `__safeContent__` to each of your documents. +Unless explicitly excluded the field will be loaded into memory when retrieving results. +==== + +[[mongo.encryption.queryable.automatic]] +=== Automatic Encryption (QE) + +MongoDB supports Queryable Encryption out of the box using the MongoDB driver with its Automatic Encryption feature. +Automatic Encryption requires a xref:mongodb/mapping/mapping-schema.adoc[JSON Schema] that allows to perform encrypted read and write operations without the need to provide an explicit en-/decryption step. + +All you need to do is create the collection according to the MongoDB documentation. +You may utilize techniques to create the required configuration outlined in the section above. + +[[mongo.encryption.queryable.manual]] +=== Explicit Encryption (QE) + +Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:mongodb-crypt`) to perform encryption and decryption tasks based on the meta information provided by annotation within the domain model. + +[NOTE] +==== +There is no official support for using Explicit Queryable Encryption. +The audacious user may combine `@Encyrpted` and `@Queryable` with `@ValueConverter(MongoEncryptionConverter.class)` at their own risk. +==== + [[mongo.encryption.explicit-setup]] -=== MongoEncryptionConverter Setup +[[mongo.encryption.converter-setup]] +== MongoEncryptionConverter Setup The converter setup for `MongoEncryptionConverter` requires a few steps as several components are involved. The bean setup consists of the following: @@ -124,7 +268,6 @@ The bean setup consists of the following: 2. A `MongoEncryptionConverter` instance configured with `ClientEncryption` and a `EncryptionKeyResolver`. 3. A `PropertyValueConverterFactory` that uses the registered `MongoEncryptionConverter` bean. -A side effect of using annotated key resolution is that the `@ExplicitEncrypted` annotation does not need to specify an alt key name. The `EncryptionKeyResolver` uses an `EncryptionContext` providing access to the property allowing for dynamic DEK resolution. .Sample MongoEncryptionConverter Configuration From e89548a84668a6339d90e146f79b8c71544a8db9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Apr 2025 14:38:40 +0200 Subject: [PATCH 03/74] Polishing. Original Pull Request: #4885 --- .../data/mongodb/core/CollectionOptions.java | 52 ++++++++++--------- .../data/mongodb/core/EntityOperations.java | 17 +++--- .../core/MappingMongoJsonSchemaCreator.java | 45 ++++++++-------- .../core/convert/MongoConversionContext.java | 39 ++++---------- .../mongodb/core/convert/QueryMapper.java | 3 +- .../encryption/MongoEncryptionConverter.java | 27 +++++----- .../mongodb/core/encryption/Encryption.java | 8 +-- .../data/mongodb/core/mapping/Queryable.java | 5 +- .../mongodb/core/mapping/RangeEncrypted.java | 11 ++-- .../core/schema/QueryCharacteristics.java | 20 +++++-- .../core/encryption/RangeEncryptionTests.java | 10 ++-- .../core/schema/MongoJsonSchemaUnitTests.java | 37 ++++++++----- 12 files changed, 145 insertions(+), 129 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 28215dc645..1def3e845d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -24,12 +24,12 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; -import java.util.stream.Collectors; import org.bson.BsonBinary; import org.bson.BsonBinarySubType; import org.bson.BsonNull; import org.bson.Document; + import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; @@ -43,7 +43,6 @@ import org.springframework.data.util.Optionals; import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -241,11 +240,11 @@ public CollectionOptions collation(@Nullable Collation collation) { * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given * {@link MongoJsonSchema}. * - * @param schema can be {@literal null}. + * @param schema must not be {@literal null}. * @return new {@link CollectionOptions}. * @since 2.1 */ - public CollectionOptions schema(@Nullable MongoJsonSchema schema) { + public CollectionOptions schema(MongoJsonSchema schema) { return validator(Validator.schema(schema)); } @@ -473,7 +472,7 @@ public Optional getChangeStreamOptions() { * Get the {@code encryptedFields} if available. * * @return {@link Optional#empty()} if not specified. - * @since 4.5.0 + * @since 4.5 */ public Optional getEncryptedFieldsOptions() { return Optional.ofNullable(encryptedFieldsOptions); @@ -552,7 +551,8 @@ public static class ValidationOptions { private final @Nullable ValidationLevel validationLevel; private final @Nullable ValidationAction validationAction; - public ValidationOptions(Validator validator, ValidationLevel validationLevel, ValidationAction validationAction) { + public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel, + @Nullable ValidationAction validationAction) { this.validator = validator; this.validationLevel = validationLevel; @@ -669,7 +669,7 @@ public int hashCode() { /** * Encapsulation of Encryption options for collections. - * + * * @author Christoph Strobl * @since 4.5 */ @@ -677,8 +677,19 @@ public static class EncryptedFieldsOptions { private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions(); - private @Nullable MongoJsonSchema schema; - private List queryableProperties; + private final @Nullable MongoJsonSchema schema; + private final List queryableProperties; + + EncryptedFieldsOptions() { + this(null, List.of()); + } + + private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema, + List queryableProperties) { + + this.schema = schema; + this.queryableProperties = queryableProperties; + } /** * @return {@link EncryptedFieldsOptions#NONE} @@ -701,17 +712,6 @@ public static EncryptedFieldsOptions fromProperties(List queryableProperties) { - - this.schema = schema; - this.queryableProperties = queryableProperties; - } - /** * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property. *

@@ -739,7 +739,6 @@ public Document toDocument() { return new Document("fields", selectPaths()); } - @NonNull private List selectPaths() { Map fields = new LinkedHashMap<>(); @@ -760,10 +759,13 @@ private List fromProperties() { List converted = new ArrayList<>(queryableProperties.size()); for (QueryableJsonSchemaProperty property : queryableProperties) { + Document field = new Document("path", property.getIdentifier()); + if (!property.getTypes().isEmpty()) { field.append("bsonType", property.getTypes().iterator().next().toBsonType().value()); } + if (property .getTargetProperty() instanceof IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty encrypted) { if (encrypted.getKeyId() != null) { @@ -775,11 +777,13 @@ private List fromProperties() { } } } - field.append("queries", property.getCharacteristics().getCharacteristics().stream() - .map(QueryCharacteristic::toDocument).collect(Collectors.toList())); + + field.append("queries", property.getCharacteristics().map(QueryCharacteristic::toDocument).toList()); + if (!field.containsKey("keyId")) { field.append("keyId", BsonNull.VALUE); } + converted.add(field); } return converted; @@ -813,7 +817,7 @@ private List fromSchema() { } } - private static void collectPaths(Document document, String currentPath, Map paths) { + private static void collectPaths(Document document, @Nullable String currentPath, Map paths) { if (document.containsKey("type") && document.get("type").equals("object")) { Object o = document.get("properties"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 24977c5af1..38269787cb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -22,6 +22,7 @@ import java.util.Map; import java.util.Optional; import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; import org.bson.BsonNull; import org.bson.Document; @@ -377,14 +378,16 @@ public CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Collec result.timeSeriesOptions(options); }); - collectionOptions.getChangeStreamOptions().ifPresent(it -> result - .changeStreamPreAndPostImagesOptions(new ChangeStreamPreAndPostImagesOptions(it.getPreAndPostImages()))); + collectionOptions.getChangeStreamOptions() // + .map(CollectionOptions.CollectionChangeStreamOptions::getPreAndPostImages) // + .map(ChangeStreamPreAndPostImagesOptions::new) // + .ifPresent(result::changeStreamPreAndPostImagesOptions); + + collectionOptions.getEncryptedFieldsOptions() // + .map(EncryptedFieldsOptions::toDocument) // + .filter(Predicate.not(Document::isEmpty)) // + .ifPresent(result::encryptedFields); - collectionOptions.getEncryptedFieldsOptions().map(EncryptedFieldsOptions::toDocument).ifPresent(encryptedFields -> { - if (!encryptedFields.isEmpty()) { - result.encryptedFields(encryptedFields); - } - }); return result; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index 790fa94293..bc26dfb68c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -24,6 +24,7 @@ import java.util.stream.Collectors; import org.bson.Document; + import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -32,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Queryable; -import org.springframework.data.mongodb.core.mapping.RangeEncrypted; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ObjectJsonSchemaProperty; @@ -126,29 +126,31 @@ public MongoJsonSchema createSchemaFor(Class type) { MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(type); MongoJsonSchemaBuilder schemaBuilder = MongoJsonSchema.builder(); - { - Encrypted encrypted = entity.findAnnotation(Encrypted.class); - if (encrypted != null) { + Encrypted encrypted = entity.findAnnotation(Encrypted.class); + if (encrypted != null) { + schemaBuilder.encryptionMetadata(getEncryptionMetadata(entity, encrypted)); + } - Document encryptionMetadata = new Document(); + List schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity); + schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0])); - Collection encryptionKeyIds = entity.getEncryptionKeyIds(); - if (!CollectionUtils.isEmpty(encryptionKeyIds)) { - encryptionMetadata.append("keyId", encryptionKeyIds); - } + return schemaBuilder.build(); + } - if (StringUtils.hasText(encrypted.algorithm())) { - encryptionMetadata.append("algorithm", encrypted.algorithm()); - } + private static Document getEncryptionMetadata(MongoPersistentEntity entity, Encrypted encrypted) { - schemaBuilder.encryptionMetadata(encryptionMetadata); - } + Document encryptionMetadata = new Document(); + + Collection encryptionKeyIds = entity.getEncryptionKeyIds(); + if (!CollectionUtils.isEmpty(encryptionKeyIds)) { + encryptionMetadata.append("keyId", encryptionKeyIds); } - List schemaProperties = computePropertiesForEntity(Collections.emptyList(), entity); - schemaBuilder.properties(schemaProperties.toArray(new JsonSchemaProperty[0])); + if (StringUtils.hasText(encrypted.algorithm())) { + encryptionMetadata.append("algorithm", encrypted.algorithm()); + } - return schemaBuilder.build(); + return encryptionMetadata; } private List computePropertiesForEntity(List path, @@ -190,8 +192,8 @@ private JsonSchemaProperty computeSchemaForProperty(List rawTargetType = computeTargetType(property); // target type before conversion Class targetType = converter.getTypeMapper().getWriteTargetTypeFor(rawTargetType); // conversion target type - - if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class || ClassUtils.isAssignable(targetType, rawTargetType) ) { + if ((rawTargetType.isPrimitive() || ClassUtils.isPrimitiveArray(rawTargetType)) && targetType == Object.class + || ClassUtils.isAssignable(targetType, rawTargetType)) { targetType = rawTargetType; } @@ -317,14 +319,15 @@ public Document toDocument() { if (queryable.contentionFactor() >= 0) { options.put("contention", queryable.contentionFactor()); } - if (!queryable.queryAttributes().isEmpty()) { + + if (StringUtils.hasText(queryable.queryAttributes())) { options.putAll(Document.parse(queryable.queryAttributes())); } return options; } }; - return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(List.of(characteristic))); + return new QueryableJsonSchemaProperty(enc, QueryCharacteristics.of(characteristic)); } private JsonSchemaProperty createObjectSchemaPropertyForEntity(List path, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index 9d672ea929..da106715d4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -79,7 +79,6 @@ public MongoPersistentProperty getProperty() { } /** - * * @param operatorContext * @return new instance of {@link MongoConversionContext}. * @since 4.5 @@ -119,71 +118,53 @@ public OperatorContext getOperatorContext() { /** * The {@link OperatorContext} provides access to the actual conversion intent like a write operation or a query * operator such as {@literal $gte}. - * + * * @since 4.5 */ public interface OperatorContext { /** * The operator the conversion is used in. + * * @return {@literal write} for simple write operations during save, or a query operator. */ - String getOperator(); + String operator(); /** * The context path the operator is used in. + * * @return never {@literal null}. */ - String getPath(); + String path(); boolean isWriteOperation(); - } - - public static class WriteOperatorContext implements OperatorContext { - private final String path; + } - public WriteOperatorContext(String path) { - this.path = path; - } + record WriteOperatorContext(String path) implements OperatorContext { @Override - public String getOperator() { + public String operator() { return "write"; } - @Override - public String getPath() { - return path; - } - @Override public boolean isWriteOperation() { return true; } } - public static class QueryOperatorContext implements OperatorContext { - - private final String operator; - private final String path; + record QueryOperatorContext(String operator, String path) implements OperatorContext { public QueryOperatorContext(@Nullable String operator, String path) { this.operator = operator != null ? operator : "$eq"; this.path = path; } - public String getOperator() { - return operator; - } - - public String getPath() { - return path; - } - @Override public boolean isWriteOperation() { return false; } } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index a0af734d69..debaf2f127 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -711,7 +711,8 @@ protected Object convertValueWithConversionContext(Field documentField, Object s return BsonUtils.mapValues(document, (key, val) -> { if (isKeyword(key)) { - return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext.forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().getPath()))); + return convertValueWithConversionContext(documentField, val, val, valueConverter, conversionContext + .forOperator(new QueryOperatorContext(key, conversionContext.getOperatorContext().path()))); } return val; }); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index c69653d2da..8d29847aae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core.convert.encryption; -import static java.util.Arrays.asList; -import static java.util.Collections.singletonList; -import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.QueryableEncryptionOptions; +import static java.util.Arrays.*; +import static java.util.Collections.*; +import static org.springframework.data.mongodb.core.encryption.EncryptionOptions.*; import java.util.Collection; import java.util.LinkedHashMap; @@ -32,6 +32,7 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.types.Binary; + import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; @@ -59,8 +60,8 @@ public class MongoEncryptionConverter implements EncryptingConverter { private static final Log LOGGER = LogFactory.getLog(MongoEncryptionConverter.class); - private static final String EQUALITY_OPERATOR = "$eq"; private static final List RANGE_OPERATORS = asList("$gt", "$gte", "$lt", "$lte"); + public static final String AND_OPERATOR = "$and"; private final Encryption encryption; private final EncryptionKeyResolver keyResolver; @@ -189,7 +190,7 @@ public Object encrypt(Object value, EncryptionContext context) { } private static @Nullable QueryableEncryptionOptions getEQOptions(MongoPersistentProperty persistentProperty, - OperatorContext operatorContext) { + @Nullable OperatorContext operatorContext) { Queryable queryableAnnotation = persistentProperty.findAnnotation(Queryable.class); if (queryableAnnotation == null || !StringUtils.hasText(queryableAnnotation.queryType())) { @@ -248,29 +249,29 @@ private BsonBinary encryptValue(Object value, EncryptionContext context, MongoPe * The mongodb-crypt {@code encryptExpression} has strict formatting requirements so this method ensures these * requirements are met and then picks out and returns just the value for use with a range query. * - * @param fieldNameAndQueryOperator field name and query operator - * @param value the value of the expression to be encrypted - * @param encryptionOptions the options - * @return the encrypted range value for use in a range query + * @param operatorContext field name and query operator. + * @param value the value of the expression to be encrypted. + * @param encryptionOptions the options. + * @return the encrypted range value for use in a range query. */ private BsonValue encryptExpression(OperatorContext operatorContext, Object value, EncryptionOptions encryptionOptions) { BsonValue doc = BsonUtils.simpleToBsonValue(value); - String fieldName = operatorContext.getPath(); - String queryOperator = operatorContext.getOperator(); + String fieldName = operatorContext.path(); + String queryOperator = operatorContext.operator(); if (!RANGE_OPERATORS.contains(queryOperator)) { throw new AssertionError(String.format("Not a valid range query. Querying a range encrypted field but the " + "query operator '%s' for field path '%s' is not a range query.", queryOperator, fieldName)); } - BsonDocument encryptExpression = new BsonDocument("$and", + BsonDocument encryptExpression = new BsonDocument(AND_OPERATOR, new BsonArray(singletonList(new BsonDocument(fieldName, new BsonDocument(queryOperator, doc))))); BsonDocument result = encryption.encryptExpression(encryptExpression, encryptionOptions); - return result.getArray("$and").get(0).asDocument().getDocument(fieldName).getBinary(queryOperator); + return result.getArray(AND_OPERATOR).get(0).asDocument().getDocument(fieldName).getBinary(queryOperator); } private BsonValue collectionLikeToBsonValue(Object value, MongoPersistentProperty property, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java index 16202598f5..a80a72ed1f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/Encryption.java @@ -20,11 +20,13 @@ /** * Component responsible for encrypting and decrypting values. * + * @param

plaintext type. + * @param ciphertext type. * @author Christoph Strobl * @author Ross Lawley * @since 4.1 */ -public interface Encryption { +public interface Encryption { /** * Encrypt the given value. @@ -33,7 +35,7 @@ public interface Encryption { * @param options must not be {@literal null}. * @return the encrypted value. */ - T encrypt(S value, EncryptionOptions options); + C encrypt(P value, EncryptionOptions options); /** * Decrypt the given value. @@ -41,7 +43,7 @@ public interface Encryption { * @param value must not be {@literal null}. * @return the decrypted value. */ - S decrypt(T value); + P decrypt(C value); /** * Encrypt the given expression. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java index abebc11a5a..a0c67f7187 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/Queryable.java @@ -1,5 +1,5 @@ /* - * Copyright 2025. the original author or authors. + * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,13 +30,11 @@ /** * @return empty {@link String} if not set. - * @since 4.5 */ String queryType() default ""; /** * @return empty {@link String} if not set. - * @since 4.5 */ String queryAttributes() default ""; @@ -46,4 +44,5 @@ * @return the contention factor */ long contentionFactor() default -1; + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java index 5710c081fa..8b2eccb6ca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/RangeEncrypted.java @@ -34,7 +34,7 @@ public @interface RangeEncrypted { /** - * Set the contention factor + * Set the contention factor. * * @return the contention factor */ @@ -42,15 +42,16 @@ long contentionFactor() default -1; /** - * Set the {@literal range} options + * Set the {@literal range} options. *

- * Should be valid extended json representing the range options and including the following values: {@code min}, - * {@code max}, {@code trimFactor} and {@code sparsity}. + * Should be valid extended {@link org.bson.Document#parse(String) JSON} representing the range options and including + * the following values: {@code min}, {@code max}, {@code trimFactor} and {@code sparsity}. *

* Please note that values are data type sensitive and may require proper identification via eg. {@code $numberLong}. * - * @return the json representation of range options + * @return the {@link org.bson.Document#parse(String) JSON} representation of range options. */ @AliasFor(annotation = Queryable.class, value = "queryAttributes") String rangeOptions() default ""; + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java index ad64e85ea1..8d9ea80fea 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java @@ -16,21 +16,25 @@ package org.springframework.data.mongodb.core.schema; import java.util.Arrays; +import java.util.Collections; +import java.util.Iterator; import java.util.List; import org.bson.BsonNull; import org.bson.Document; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; /** * @author Christoph Strobl * @since 4.5 */ -public class QueryCharacteristics { +public class QueryCharacteristics implements Streamable { - private static final QueryCharacteristics NONE = new QueryCharacteristics(List.of()); + private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList()); private final List characteristics; @@ -46,14 +50,19 @@ public static QueryCharacteristics of(List characteristics) return new QueryCharacteristics(List.copyOf(characteristics)); } - QueryCharacteristics(QueryCharacteristic... characteristics) { - this.characteristics = Arrays.asList(characteristics); + public static QueryCharacteristics of(QueryCharacteristic... characteristics) { + return new QueryCharacteristics(Arrays.asList(characteristics)); } public List getCharacteristics() { return characteristics; } + @Override + public Iterator iterator() { + return this.characteristics.iterator(); + } + public static RangeQuery range() { return new RangeQuery<>(); } @@ -96,7 +105,8 @@ private RangeQuery() { this(Range.unbounded(), null, null, null); } - public RangeQuery(Range valueRange, Integer trimFactor, Long sparsity, Long contention) { + public RangeQuery(@Nullable Range valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity, + @Nullable Long contention) { this.valueRange = valueRange; this.trimFactor = trimFactor; this.sparsity = sparsity; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java index 2ea0b5b218..e4e760cc91 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/RangeEncryptionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2023-2024 the original author or authors. + * Copyright 2024-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,9 +15,8 @@ */ package org.springframework.data.mongodb.core.encryption; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.core.query.Criteria.*; import java.security.SecureRandom; import java.util.LinkedHashMap; @@ -39,6 +38,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; @@ -369,7 +369,7 @@ MongoEncryptionConverter encryptingConverter(MongoClientEncryption mongoClientEn path = StringUtils.arrayToDelimitedString(ctx.getProperty().getMongoField().getName().parts(), "."); } if (ctx.getOperatorContext() != null) { - path = ctx.getOperatorContext().getPath(); + path = ctx.getOperatorContext().path(); } return EncryptionKey.keyId(keyHolder.getEncryptionKey(path)); })); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java index e2c385464c..1691305617 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/schema/MongoJsonSchemaUnitTests.java @@ -15,13 +15,10 @@ */ package org.springframework.data.mongodb.core.schema; -import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.rangeEncrypted; +import static org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty.*; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.number; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.string; -import static org.springframework.data.mongodb.test.util.Assertions.assertThat; -import static org.springframework.data.mongodb.test.util.Assertions.assertThatIllegalArgumentException; +import static org.springframework.data.mongodb.test.util.Assertions.*; import java.util.Arrays; import java.util.Collections; @@ -119,13 +116,27 @@ void rendersQueryablePropertyCorrectly() { List.of(QueryCharacteristics.range().contention(0).trimFactor(1).sparsity(1).min(0).max(200)))) .build(); - assertThat(schema.toDocument()).isEqualTo(new Document("$jsonSchema", - new Document("type", "object").append("properties", - new Document("ssn", - new Document("encrypt", - new Document("bsonType", "long").append("algorithm", "Range").append("queries", - List.of(new Document("contention", 0L).append("trimFactor", 1).append("sparsity", 1L) - .append("queryType", "range").append("min", 0).append("max", 200)))))))); + assertThat(schema.toDocument().get("$jsonSchema", Document.class)).isEqualTo(""" + { + "type": "object", + "properties": { + "ssn": { + "encrypt": { + "bsonType": "long", + "algorithm": "Range", + "queries": [{ + "queryType": "range", + "contention": {$numberLong: "0"}, + "trimFactor": 1, + "sparsity": {$numberLong: "1"}, + "min": 0, + "max": 200 + }] + } + } + } + } + """); } @Test // DATAMONGO-1835 From 272a06247396b70fc36b91b0e3b387d924177b7b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 14 Apr 2025 09:40:45 +0200 Subject: [PATCH 04/74] Update Javadoc. Original Pull Request: #4885 --- .../data/mongodb/core/CollectionOptions.java | 5 +- .../IdentifiableJsonSchemaProperty.java | 4 +- .../core/schema/QueryCharacteristic.java | 6 + .../core/schema/QueryCharacteristics.java | 144 +++++++++++++++--- .../util/MongoCompatibilityAdapter.java | 2 +- 5 files changed, 137 insertions(+), 24 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 1def3e845d..5df30e0b92 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -24,12 +24,12 @@ import java.util.Map.Entry; import java.util.Optional; import java.util.function.Function; +import java.util.stream.StreamSupport; import org.bson.BsonBinary; import org.bson.BsonBinarySubType; import org.bson.BsonNull; import org.bson.Document; - import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; @@ -778,7 +778,8 @@ private List fromProperties() { } } - field.append("queries", property.getCharacteristics().map(QueryCharacteristic::toDocument).toList()); + field.append("queries", StreamSupport.stream(property.getCharacteristics().spliterator(), false) + .map(QueryCharacteristic::toDocument).toList()); if (!field.containsKey("keyId")) { field.append("keyId", BsonNull.VALUE); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java index c95bd4c734..26dbd7dffb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -1205,8 +1205,8 @@ public Object getKeyId() { } /** - * {@link JsonSchemaProperty} implementation typically wrapping {@link EncryptedJsonSchemaProperty encrypted - * properties} to mark them as queryable. + * {@link JsonSchemaProperty} implementation typically wrapping an {@link EncryptedJsonSchemaProperty encrypted + * property} to mark it as queryable. * * @author Christoph Strobl * @since 4.5 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java index 2b405c56ce..8604ba9d6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristic.java @@ -18,6 +18,9 @@ import org.bson.Document; /** + * Defines the specific character of a query that can be executed. Mainly used to define the characteristic of queryable + * encrypted fields. + * * @author Christoph Strobl * @since 4.5 */ @@ -28,6 +31,9 @@ public interface QueryCharacteristic { */ String queryType(); + /** + * @return the raw {@link Document} representation of the instance. + */ default Document toDocument() { return new Document("queryType", queryType()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java index 8d9ea80fea..4ec775c5e7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java @@ -22,18 +22,22 @@ import org.bson.BsonNull; import org.bson.Document; - import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; -import org.springframework.data.util.Streamable; import org.springframework.lang.Nullable; /** + * Encapsulation of individual {@link QueryCharacteristic query characteristics} used to define queries that can be + * executed when using queryable encryption. + * * @author Christoph Strobl * @since 4.5 */ -public class QueryCharacteristics implements Streamable { +public class QueryCharacteristics implements Iterable { + /** + * instance indicating none + */ private static final QueryCharacteristics NONE = new QueryCharacteristics(Collections.emptyList()); private final List characteristics; @@ -42,18 +46,36 @@ public class QueryCharacteristics implements Streamable { this.characteristics = characteristics; } + /** + * @return marker instance indicating no characteristics have been defined. + */ public static QueryCharacteristics none() { return NONE; } + /** + * Create new {@link QueryCharacteristics} from given list of {@link QueryCharacteristic characteristics}. + * + * @param characteristics must not be {@literal null}. + * @return new instance of {@link QueryCharacteristics}. + */ public static QueryCharacteristics of(List characteristics) { return new QueryCharacteristics(List.copyOf(characteristics)); } + /** + * Create new {@link QueryCharacteristics} from given {@link QueryCharacteristic characteristics}. + * + * @param characteristics must not be {@literal null}. + * @return new instance of {@link QueryCharacteristics}. + */ public static QueryCharacteristics of(QueryCharacteristic... characteristics) { return new QueryCharacteristics(Arrays.asList(characteristics)); } + /** + * @return the list of {@link QueryCharacteristic characteristics}. + */ public List getCharacteristics() { return characteristics; } @@ -63,22 +85,50 @@ public Iterator iterator() { return this.characteristics.iterator(); } + /** + * Create a new {@link RangeQuery range query characteristic} used to define range queries against an encrypted field. + * + * @param targeted field type + * @return new instance of {@link RangeQuery}. + */ public static RangeQuery range() { return new RangeQuery<>(); } + /** + * Create a new {@link EqualityQuery equality query characteristic} used to define equality queries against an + * encrypted field. + * + * @param targeted field type + * @return new instance of {@link EqualityQuery}. + */ public static EqualityQuery equality() { return new EqualityQuery<>(null); } + /** + * {@link QueryCharacteristic} for equality comparison. + * + * @param + * @since 4.5 + */ public static class EqualityQuery implements QueryCharacteristic { private final @Nullable Long contention; + /** + * Create new instance of {@link EqualityQuery}. + * + * @param contention can be {@literal null}. + */ public EqualityQuery(@Nullable Long contention) { this.contention = contention; } + /** + * @param contention concurrent counter partition factor. + * @return new instance of {@link EqualityQuery}. + */ public EqualityQuery contention(long contention) { return new EqualityQuery<>(contention); } @@ -94,64 +144,120 @@ public Document toDocument() { } } + /** + * {@link QueryCharacteristic} for range comparison. + * + * @param + * @since 4.5 + */ public static class RangeQuery implements QueryCharacteristic { private final @Nullable Range valueRange; private final @Nullable Integer trimFactor; private final @Nullable Long sparsity; + private final @Nullable Long precision; private final @Nullable Long contention; private RangeQuery() { - this(Range.unbounded(), null, null, null); + this(Range.unbounded(), null, null, null, null); } + /** + * Create new instance of {@link RangeQuery}. + * + * @param valueRange + * @param trimFactor + * @param sparsity + * @param contention + */ public RangeQuery(@Nullable Range valueRange, @Nullable Integer trimFactor, @Nullable Long sparsity, - @Nullable Long contention) { + @Nullable Long precision, @Nullable Long contention) { this.valueRange = valueRange; this.trimFactor = trimFactor; this.sparsity = sparsity; + this.precision = precision; this.contention = contention; } - @Override - public String queryType() { - return "range"; - } - + /** + * @param lower the lower value range boundary for the queryable field. + * @return new instance of {@link RangeQuery}. + */ public RangeQuery min(T lower) { Range range = Range.of(Bound.inclusive(lower), valueRange != null ? valueRange.getUpperBound() : Bound.unbounded()); - return new RangeQuery<>(range, trimFactor, sparsity, contention); + return new RangeQuery<>(range, trimFactor, sparsity, precision, contention); } + /** + * @param upper the upper value range boundary for the queryable field. + * @return new instance of {@link RangeQuery}. + */ public RangeQuery max(T upper) { Range range = Range.of(valueRange != null ? valueRange.getLowerBound() : Bound.unbounded(), Bound.inclusive(upper)); - return new RangeQuery<>(range, trimFactor, sparsity, contention); + return new RangeQuery<>(range, trimFactor, sparsity, precision, contention); } + /** + * @param trimFactor value to control the throughput of concurrent inserts and updates. + * @return new instance of {@link RangeQuery}. + */ public RangeQuery trimFactor(int trimFactor) { - return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); } + /** + * @param sparsity value to control the value density within the index. + * @return new instance of {@link RangeQuery}. + */ public RangeQuery sparsity(long sparsity) { - return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); } + /** + * @param contention concurrent counter partition factor. + * @return new instance of {@link RangeQuery}. + */ public RangeQuery contention(long contention) { - return new RangeQuery<>(valueRange, trimFactor, sparsity, contention); + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + /** + * @param precision digits considered comparing floating point numbers. + * @return new instance of {@link RangeQuery}. + */ + public RangeQuery precision(long precision) { + return new RangeQuery<>(valueRange, trimFactor, sparsity, precision, contention); + } + + @Override + public String queryType() { + return "range"; } @Override @SuppressWarnings("unchecked") public Document toDocument() { - return QueryCharacteristic.super.toDocument().append("contention", contention).append("trimFactor", trimFactor) - .append("sparsity", sparsity).append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)) - .append("max", valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE)); + Document target = QueryCharacteristic.super.toDocument(); + if (contention != null) { + target.append("contention", contention); + } + if (trimFactor != null) { + target.append("trimFactor", trimFactor); + } + if (valueRange != null) { + target.append("min", valueRange.getLowerBound().getValue().orElse((T) BsonNull.VALUE)).append("max", + valueRange.getUpperBound().getValue().orElse((T) BsonNull.VALUE)); + } + if (sparsity != null) { + target.append("sparsity", sparsity); + } + + return target; } } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java index a61fe0eca8..8bd422c493 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -61,7 +61,7 @@ public class MongoCompatibilityAdapter { static { - // method name changed in between + // method name changed in between Method trimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor", Integer.class); if (trimFactor != null) { setTrimFactor = trimFactor; From 34c49e0a061779a2004bd756535e3eb39f3be057 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 11 Apr 2025 11:25:32 +0200 Subject: [PATCH 05/74] Fix `@MongoId` mapping for `insertAll`. This commit fixes an issue where id properties annotated with MongoId had not been converted into the desired target type when inserting a collection of objects instead a single one. Resolves: #4944 Original pull request: #4945 --- .../data/mongodb/core/MongoTemplate.java | 5 ++++- .../data/mongodb/core/MongoTemplateTests.java | 13 +++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 67ef3a3081..fd547c61a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1433,7 +1433,10 @@ protected Collection doInsertBatch(String collectionName, Collection(initialized, document, collectionName)); initialized = maybeCallBeforeSave(initialized, document, collectionName); - documentList.add(document); + MappedDocument mappedDocument = queryOperations.createInsertContext(MappedDocument.of(document)) + .prepareId(uninitialized.getClass()); + + documentList.add(mappedDocument.getDocument()); initializedBatchToSave.add(initialized); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 83d4e30cc5..6aaec4011e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -34,6 +34,7 @@ import java.util.stream.IntStream; import java.util.stream.Stream; +import org.bson.Document; import org.bson.types.ObjectId; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -3110,6 +3111,18 @@ public void generatesIdForInsertAll() { assertThat(jesse.getId()).isNotNull(); } + @Test // GH-4944 + public void insertAllShouldConvertIdToTargetTypeBeforeSave() { + + RawStringId walter = new RawStringId(); + walter.value = "walter"; + + RawStringId returned = template.insertAll(List.of(walter)).iterator().next(); + org.bson.Document document = template.execute(RawStringId.class, collection -> collection.find().first()); + + assertThat(returned.id).isEqualTo(document.get("_id")); + } + @Test // DATAMONGO-1208 public void takesSortIntoAccountWhenStreaming() { From 985bb8bfa5a782e11afdab12b9ffce33156cbf81 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 15:30:56 +0200 Subject: [PATCH 06/74] Polishing. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Initialize MongoId also for reactive insertAll(…). See: #4944 Original pull request: #4945 --- .../data/mongodb/core/ReactiveMongoTemplate.java | 11 ++++++++--- .../mongodb/core/ReactiveMongoTemplateTests.java | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index ea427a3e1f..b74ec6aa1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -1434,11 +1434,16 @@ protected Flux doInsertBatch(String collectionName, Collection(initialized, dbDoc, collectionName)); + maybeEmitEvent(new BeforeSaveEvent<>(initialized, mapped.getDocument(), collectionName)); + return maybeCallBeforeSave(initialized, mapped.getDocument(), collectionName).map(toSave -> { - return maybeCallBeforeSave(initialized, dbDoc, collectionName).thenReturn(Tuples.of(entity, dbDoc)); + MappedDocument mappedDocument = queryOperations.createInsertContext(mapped) + .prepareId(uninitialized.getClass()); + + return Tuples.of(entity, mappedDocument.getDocument()); + }); }); }).collectList(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java index 80dd584b9e..f87227cdde 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateTests.java @@ -48,6 +48,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; import org.springframework.dao.DataIntegrityViolationException; @@ -84,6 +85,7 @@ import com.mongodb.WriteConcern; import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoCollection; /** * Integration test for {@link MongoTemplate}. @@ -165,6 +167,19 @@ void insertCollectionSetsId() { assertThat(person.getId()).isNotNull(); } + @Test // GH-4944 + void insertAllShouldConvertIdToTargetTypeBeforeSave() { + + RawStringId walter = new RawStringId(); + walter.value = "walter"; + + RawStringId returned = template.insertAll(List.of(walter)).blockLast(); + template.execute(RawStringId.class, MongoCollection::find) // + .as(StepVerifier::create) // + .consumeNextWith(actual -> assertThat(returned.id).isEqualTo(actual.get("_id"))) // + .verifyComplete(); + } + @Test // DATAMONGO-1444 void saveSetsId() { From 5eaeb926fd70633b08f47e6cd2131b846f4c3bbe Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 14 Apr 2025 15:45:04 +0200 Subject: [PATCH 07/74] Polishing. Guard tests for encryption functionality. Original Pull Request: #4885 --- ...QueryableEncryptionCollectionCreationTests.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java index d88bde68a2..dd9e459e78 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/encryption/MongoQueryableEncryptionCollectionCreationTests.java @@ -15,12 +15,9 @@ */ package org.springframework.data.mongodb.core.encryption; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.encrypted; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int32; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.int64; -import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.queryable; -import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.range; -import static org.springframework.data.mongodb.test.util.Assertions.assertThat; +import static org.springframework.data.mongodb.core.schema.JsonSchemaProperty.*; +import static org.springframework.data.mongodb.core.schema.QueryCharacteristics.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import java.util.List; import java.util.UUID; @@ -34,6 +31,7 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; @@ -42,6 +40,7 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -49,9 +48,12 @@ import com.mongodb.client.MongoClient; /** + * Integration tests for creating collections with encrypted fields. + * * @author Christoph Strobl */ @ExtendWith({ MongoClientExtension.class, SpringExtension.class }) +@EnableIfMongoServerVersion(isGreaterThanEqual = "8.0") @ContextConfiguration public class MongoQueryableEncryptionCollectionCreationTests { From d2b7b80dd7fb7ef7a29cbc1a281cd62963cb5b83 Mon Sep 17 00:00:00 2001 From: Jeff Yemin Date: Mon, 14 Apr 2025 08:24:23 -0400 Subject: [PATCH 08/74] Update mongo-encryption.adoc Closes: #4947 Signed-off-by: Jeff Yemin --- .../antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc index a4aae23748..14e866cf14 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-encryption.adoc @@ -254,7 +254,7 @@ Explicit encryption uses the MongoDB driver's encryption library (`org.mongodb:m [NOTE] ==== There is no official support for using Explicit Queryable Encryption. -The audacious user may combine `@Encyrpted` and `@Queryable` with `@ValueConverter(MongoEncryptionConverter.class)` at their own risk. +The audacious user may combine `@Encrypted` and `@Queryable` with `@ValueConverter(MongoEncryptionConverter.class)` at their own risk. ==== [[mongo.encryption.explicit-setup]] From c8d2d83b0aaafac47b0e7e9bb2228cc913da2ca9 Mon Sep 17 00:00:00 2001 From: Alex Bevilacqua Date: Fri, 28 Mar 2025 16:05:12 -0400 Subject: [PATCH 09/74] Update mongo-search-indexes.adoc. Remove quantization from VectorSearch docs. Closes: #4931 Signed-off-by: Alex Bevilacqua --- .../modules/ROOT/pages/mongodb/mongo-search-indexes.adoc | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc index 9b6bfcf095..345b5dbb6c 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc @@ -84,7 +84,6 @@ VectorSearchOperation search = VectorSearchOperation.search("vector_index") <1> .vector( ... ) .numCandidates(150) .limit(10) - .quantization(SCALAR) .withSearchScore("score"); <3> AggregationResults results = mongoTemplate @@ -107,8 +106,7 @@ db.embedded_movies.aggregate([ "path": "plot_embedding", <1> "queryVector": [ ... ], "numCandidates": 150, - "limit": 10, - "quantization": "scalar" + "limit": 10 } }, { From 4bd5a1f113dae6a658053c7435e38edd526093a9 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 17 Apr 2025 09:43:15 +0200 Subject: [PATCH 10/74] Use `awaitility` version property. Closes #4948 --- spring-data-mongodb/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 096fd48022..b842a2def3 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -135,7 +135,7 @@ org.awaitility awaitility - 4.2.2 + ${awaitility} test From 98f871b012f8989d9f418af091cde60683685772 Mon Sep 17 00:00:00 2001 From: Ranzy Blessings Date: Thu, 10 Apr 2025 16:14:00 +0200 Subject: [PATCH 11/74] Add support for sorting simple arrays (integers/strings) with SortArray () - Added methods `byValueAscending()` and `byValueDescending()` to the SortArray class to support sorting simple array types (e.g., integers, strings) in ascending and descending order. - Updated tests to verify the correct functionality of sorting arrays by value. - Refactored SortArray to handle sorting of simple types without requiring a property for sorting. For more details, refer to: https://www.mongodb.com/docs/manual/reference/operator/aggregation/sortArray/ Resolves: #4929 Original Pull Request: #4943 Signed-off-by: Ranzy Blessings --- .../core/aggregation/ArrayOperators.java | 22 +++++++++++++++++ .../aggregation/ArrayOperatorsUnitTests.java | 24 +++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index 41688bfc62..93d7b5bfa9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -2059,6 +2059,28 @@ public SortArray by(Sort sort) { return new SortArray(append("sortBy", sort)); } + /** + * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers, + * strings). + * + * @return new instance of {@link SortArray}. + * @since 4.x (TBD) + */ + public SortArray byValueAscending() { + return new SortArray(append("sortBy", 1)); + } + + /** + * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers, + * strings). + * + * @return new instance of {@link SortArray}. + * @since 4.x (TBD) + */ + public SortArray byValueDescending() { + return new SortArray(append("sortBy", -1)); + } + /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java index 007fdbb28c..d023d54058 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject; +import org.springframework.data.mongodb.core.aggregation.ArrayOperators.SortArray; /** * Unit tests for {@link ArrayOperators} @@ -179,4 +180,27 @@ void sortByWithFieldRef() { assertThat(ArrayOperators.arrayOf("team").sort(Sort.by("name")).toDocument(Aggregation.DEFAULT_CONTEXT)) .isEqualTo("{ $sortArray: { input: \"$team\", sortBy: { name: 1 } } }"); } + + @Test // GH-4929 + public void sortArrayByValueAscending() { + Document result = SortArray.sortArrayOf("numbers").byValueAscending().toDocument(Aggregation.DEFAULT_CONTEXT); + Document expected = new Document("$sortArray", new Document("input", "$numbers").append("sortBy", 1)); + assertThat(result).isEqualTo(expected); + } + + @Test // GH-4929 + public void sortArrayByValueDescending() { + Document result = SortArray.sortArrayOf("numbers").byValueDescending().toDocument(Aggregation.DEFAULT_CONTEXT); + Document expected = new Document("$sortArray", new Document("input", "$numbers").append("sortBy", -1)); + assertThat(result).isEqualTo(expected); + } + + @Test // GH-4929 + public void sortArrayByPropertyUnchanged() { + Document result = SortArray.sortArrayOf("items").by(Sort.by(Sort.Direction.ASC, "price")) + .toDocument(Aggregation.DEFAULT_CONTEXT); + Document expected = new Document("$sortArray", + new Document("input", "$items").append("sortBy", new Document("price", 1))); + assertThat(result).isEqualTo(expected); + } } From 5db56b2d4d61c2bfdd76435bd9d2af0d61a66f7a Mon Sep 17 00:00:00 2001 From: Nathan McDonald Date: Wed, 2 Apr 2025 15:46:51 +0100 Subject: [PATCH 12/74] Add support for sorting simple arrays by direction. Add method to provide sorting direction to sort array aggregation. Related to: #4929 Original Pull Request: #4935 Signed-off-by: Nathan McDonald --- .../core/aggregation/ArrayOperators.java | 33 +++++++++++++++++-- .../aggregation/ArrayOperatorsUnitTests.java | 9 +++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index 93d7b5bfa9..ae2a32ffa0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -24,6 +24,7 @@ import org.bson.Document; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; @@ -336,6 +337,22 @@ public SortArray sort(Sort sort) { return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(sort); } + /** + * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given {@link Sort + * order}. + * + * @return new instance of {@link SortArray}. + * @since 4.0 + */ + public SortArray sort(Direction direction) { + + if (usesFieldRef()) { + return SortArray.sortArrayOf(fieldReference).by(direction); + } + + return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(direction); + } + /** * Creates new {@link AggregationExpression} that transposes an array of input arrays so that the first element of * the output array would be an array containing, the first element of the first input array, the first element of @@ -2081,10 +2098,20 @@ public SortArray byValueDescending() { return new SortArray(append("sortBy", -1)); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() + /** + * Set the order to put elements in. + * + * @param direction must not be {@literal null}. + * @return new instance of {@link SortArray}. */ + public SortArray by(Direction direction) { + return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() + */ @Override protected String getMongoMethod() { return "$sortArray"; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java index d023d54058..93ade364a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java @@ -24,6 +24,7 @@ import org.bson.Document; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.SortArray; @@ -203,4 +204,12 @@ public void sortArrayByPropertyUnchanged() { new Document("input", "$items").append("sortBy", new Document("price", 1))); assertThat(result).isEqualTo(expected); } + + @Test // GH-4929 + void sortByWithDirection() { + + assertThat(ArrayOperators.arrayOf(List.of("a", "b", "d", "c")).sort(Direction.DESC) + .toDocument(Aggregation.DEFAULT_CONTEXT)) + .isEqualTo("{ $sortArray: { input: [\"a\", \"b\", \"d\", \"c\"], sortBy: -1 } }"); + } } From 9dee60af151ceb1a46c3d665b959b48ea3a0947b Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 11 Apr 2025 14:03:37 +0200 Subject: [PATCH 13/74] Polishing. Update test, since tags, rename method. Original Pull Requests: #4935 & #4943 --- .../core/aggregation/ArrayOperators.java | 49 +++++++------------ .../aggregation/ArrayOperatorsUnitTests.java | 22 +++------ 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index ae2a32ffa0..a8cb58d17c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -338,19 +338,19 @@ public SortArray sort(Sort sort) { } /** - * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given {@link Sort - * order}. + * Creates new {@link AggregationExpression} that takes the associated array and sorts it by the given + * {@link Direction order}. * * @return new instance of {@link SortArray}. - * @since 4.0 + * @since 4.5 */ public SortArray sort(Direction direction) { if (usesFieldRef()) { - return SortArray.sortArrayOf(fieldReference).by(direction); + return SortArray.sortArrayOf(fieldReference).direction(direction); } - return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).by(direction); + return (usesExpression() ? SortArray.sortArrayOf(expression) : SortArray.sortArray(values)).direction(direction); } /** @@ -1960,10 +1960,6 @@ public static First firstOf(AggregationExpression expression) { return new First(expression); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() - */ @Override protected String getMongoMethod() { return "$first"; @@ -2014,10 +2010,6 @@ public static Last lastOf(AggregationExpression expression) { return new Last(expression); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() - */ @Override protected String getMongoMethod() { return "$last"; @@ -2077,41 +2069,38 @@ public SortArray by(Sort sort) { } /** - * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers, - * strings). + * Order the values for the array in the given direction. * + * @param direction must not be {@literal null}. * @return new instance of {@link SortArray}. - * @since 4.x (TBD) + * @since 4.5 */ - public SortArray byValueAscending() { - return new SortArray(append("sortBy", 1)); + public SortArray direction(Direction direction) { + return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1)); } /** - * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers, + * Sort the array elements by their values in ascending order. Suitable for arrays of simple types (e.g., integers, * strings). * * @return new instance of {@link SortArray}. - * @since 4.x (TBD) + * @since 4.5 */ - public SortArray byValueDescending() { - return new SortArray(append("sortBy", -1)); + public SortArray byValueAscending() { + return direction(Direction.ASC); } /** - * Set the order to put elements in. + * Sort the array elements by their values in descending order. Suitable for arrays of simple types (e.g., integers, + * strings). * - * @param direction must not be {@literal null}. * @return new instance of {@link SortArray}. + * @since 4.5 */ - public SortArray by(Direction direction) { - return new SortArray(append("sortBy", direction.isAscending() ? 1 : -1)); + public SortArray byValueDescending() { + return direction(Direction.DESC); } - /* - * (non-Javadoc) - * @see org.springframework.data.mongodb.core.aggregation.AbstractAggregationExpression#getMongoMethod() - */ @Override protected String getMongoMethod() { return "$sortArray"; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java index 93ade364a1..0ab5545f23 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ArrayOperatorsUnitTests.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.springframework.data.mongodb.test.util.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import java.util.ArrayList; import java.util.Arrays; @@ -26,7 +26,6 @@ import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.ArrayToObject; -import org.springframework.data.mongodb.core.aggregation.ArrayOperators.SortArray; /** * Unit tests for {@link ArrayOperators} @@ -184,25 +183,16 @@ void sortByWithFieldRef() { @Test // GH-4929 public void sortArrayByValueAscending() { - Document result = SortArray.sortArrayOf("numbers").byValueAscending().toDocument(Aggregation.DEFAULT_CONTEXT); - Document expected = new Document("$sortArray", new Document("input", "$numbers").append("sortBy", 1)); - assertThat(result).isEqualTo(expected); + + Document result = ArrayOperators.arrayOf("numbers").sort(Direction.ASC).toDocument(Aggregation.DEFAULT_CONTEXT); + assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: 1 } }"); } @Test // GH-4929 public void sortArrayByValueDescending() { - Document result = SortArray.sortArrayOf("numbers").byValueDescending().toDocument(Aggregation.DEFAULT_CONTEXT); - Document expected = new Document("$sortArray", new Document("input", "$numbers").append("sortBy", -1)); - assertThat(result).isEqualTo(expected); - } - @Test // GH-4929 - public void sortArrayByPropertyUnchanged() { - Document result = SortArray.sortArrayOf("items").by(Sort.by(Sort.Direction.ASC, "price")) - .toDocument(Aggregation.DEFAULT_CONTEXT); - Document expected = new Document("$sortArray", - new Document("input", "$items").append("sortBy", new Document("price", 1))); - assertThat(result).isEqualTo(expected); + Document result = ArrayOperators.arrayOf("numbers").sort(Direction.DESC).toDocument(Aggregation.DEFAULT_CONTEXT); + assertThat(result).isEqualTo("{ $sortArray: { input: '$numbers', sortBy: -1 } }"); } @Test // GH-4929 From ff0fd94bcdbfc00ada661600f3da7f445385d1b0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 08:52:02 +0200 Subject: [PATCH 14/74] Clarify QueryDSL usage in reference documentation. Closes #4951 --- .../modules/ROOT/pages/repositories/core-extensions.adoc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc index 3bc7648154..75dcea1e4f 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-extensions.adoc @@ -169,7 +169,6 @@ Maven:: com.querydsl querydsl-mongodb ${querydslVersion} - jakarta @@ -216,7 +215,7 @@ Gradle:: [source,groovy,indent=0,subs="verbatim,quotes",role="secondary"] ---- dependencies { - implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}:jakarta' + implementation 'com.querydsl:querydsl-mongodb:${querydslVersion}' annotationProcessor 'com.querydsl:querydsl-apt:${querydslVersion}:jakarta' annotationProcessor 'org.springframework.data:spring-data-mongodb' @@ -235,6 +234,8 @@ tasks.withType(JavaCompile).configureEach { ====== Note that the setup above shows the simplest usage omitting any other options or dependencies that your project might require. +This way of configuring annotation processing disables Java's annotation processor scanning because MongoDB requires specifying `-processor` by class name. +If you're using other annotation processors, you need to add them to the list of `-processor`/`annotationProcessors` as well. include::{commons}@data-commons::page$repositories/core-extensions-web.adoc[leveloffset=1] From 792f13b6d287c39674aa5fa70f9f6fe1eb87554b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 11:23:51 +0200 Subject: [PATCH 15/74] Prepare 4.5 RC1 (2025.0.0). See #4924 --- pom.xml | 14 ++------------ src/main/resources/notice.txt | 3 ++- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/pom.xml b/pom.xml index 9f4b6bc897..0b9efedb84 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 3.5.0-RC1 @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.5.0-SNAPSHOT + 3.5.0-RC1 5.4.0 1.19 @@ -157,16 +157,6 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - spring-milestone https://repo.spring.io/milestone diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 61b472b23b..52ee00c4f5 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 4.5 M2 (2025.0.0) +Spring Data MongoDB 4.5 RC1 (2025.0.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -59,5 +59,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file. + From 706a8ee3ff9b114a959674195ea2e800cd325921 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 11:24:11 +0200 Subject: [PATCH 16/74] Release version 4.5 RC1 (2025.0.0). See #4924 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 0b9efedb84..987bb869e8 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-RC1 pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..c7c925620b 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-RC1 ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b842a2def3..21487dcbdc 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0-RC1 ../pom.xml From dbf8f2d264910451b5be575b22f14bbabb7fe405 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 11:26:47 +0200 Subject: [PATCH 17/74] Prepare next development iteration. See #4924 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 987bb869e8..0b9efedb84 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-RC1 + 4.5.0-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index c7c925620b..58c63dfc97 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-RC1 + 4.5.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 21487dcbdc..b842a2def3 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-RC1 + 4.5.0-SNAPSHOT ../pom.xml From 9496d1bf65fba8b3695bbb64b463e5281d14c863 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 11:26:48 +0200 Subject: [PATCH 18/74] After release cleanups. See #4924 --- pom.xml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 0b9efedb84..9f4b6bc897 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 3.5.0-RC1 + 3.5.0-SNAPSHOT @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.5.0-RC1 + 3.5.0-SNAPSHOT 5.4.0 1.19 @@ -157,6 +157,16 @@ + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + spring-milestone https://repo.spring.io/milestone From f3bd67e12bf12b4821f601e87c1bbdee94d529f7 Mon Sep 17 00:00:00 2001 From: kssumin <201566@jnu.ac.kr> Date: Fri, 2 May 2025 10:02:31 +0900 Subject: [PATCH 19/74] Fix AddFieldsOperationBuilder to treat String value as Field reference This commit modifies the AddFieldsOperationBuilder to correctly treat String values as field references. When a String value is passed, it is now interpreted as a reference to another field, following MongoDB's field reference syntax. Resolves: #4933 Original Pull Request: #4959 Signed-off-by: kssumin <201566@jnu.ac.kr> --- .../core/aggregation/AddFieldsOperation.java | 3 ++- .../AddFieldsOperationUnitTests.java | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java index 0dc1588bf8..b79d978b8b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java @@ -31,6 +31,7 @@ * * * @author Christoph Strobl + * @author Kim Sumin * @since 3.0 * @see MongoDB Aggregation * Framework: $addFields @@ -148,7 +149,7 @@ public AddFieldsOperationBuilder withValue(Object value) { @Override public AddFieldsOperationBuilder withValueOf(Object value) { - valueMap.put(field, value instanceof String stringValue ? Fields.fields(stringValue) : value); + valueMap.put(field, value instanceof String stringValue ? Fields.field(stringValue) : value); return AddFieldsOperationBuilder.this; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java index 32c6d43220..39437fc7a2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java @@ -33,6 +33,7 @@ * * @author Christoph Strobl * @author Mark Paluch + * @author Kim Sumin */ class AddFieldsOperationUnitTests { @@ -127,6 +128,22 @@ void exposesFieldsCorrectly() { assertThat(fields.getField("does-not-exist")).isNull(); } + @Test // DATAMONGO-4933 + void rendersStringValueAsFieldReferenceCorrectly() { + + AddFieldsOperation operation = AddFieldsOperation.builder().addField("name").withValueOf("value").build(); + + assertThat(operation.toPipelineStages(contextFor(Scores.class))) + .containsExactly(Document.parse("{\"$addFields\" : {\"name\":\"$value\"}}")); + + AddFieldsOperation mappedOperation = AddFieldsOperation.builder().addField("totalHomework").withValueOf("homework") + .build(); + + assertThat(mappedOperation.toPipelineStages(contextFor(ScoresWithMappedField.class))) + .containsExactly(Document.parse("{\"$addFields\" : {\"totalHomework\":\"$home_work\"}}")); + } + + private static AggregationOperationContext contextFor(@Nullable Class type) { if (type == null) { From ee1a94f0091340f1e5af1ea4be09d5dff92b76ae Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 5 May 2025 09:34:00 +0200 Subject: [PATCH 20/74] Fix SetOperation.toValueOf rendering for field references. Use single field reference instead of invalid multi field. See: #4933 --- .../mongodb/core/aggregation/SetOperation.java | 2 +- .../AddFieldsOperationUnitTests.java | 3 +-- .../core/aggregation/SetOperationUnitTests.java | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 4 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java index 7f5c1c7722..b188b16b5f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java @@ -140,7 +140,7 @@ public SetOperation toValue(Object value) { @Override public SetOperation toValueOf(Object value) { - valueMap.put(field, value instanceof String stringValue ? Fields.fields(stringValue) : value); + valueMap.put(field, value instanceof String stringValue ? Fields.field(stringValue) : value); return FieldAppender.this.build(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java index 39437fc7a2..8dcf96231c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java @@ -128,7 +128,7 @@ void exposesFieldsCorrectly() { assertThat(fields.getField("does-not-exist")).isNull(); } - @Test // DATAMONGO-4933 + @Test // GH-4933 void rendersStringValueAsFieldReferenceCorrectly() { AddFieldsOperation operation = AddFieldsOperation.builder().addField("name").withValueOf("value").build(); @@ -143,7 +143,6 @@ void rendersStringValueAsFieldReferenceCorrectly() { .containsExactly(Document.parse("{\"$addFields\" : {\"totalHomework\":\"$home_work\"}}")); } - private static AggregationOperationContext contextFor(@Nullable Class type) { if (type == null) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java index 093d4af7a0..d6f95216a5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java @@ -21,7 +21,6 @@ import org.bson.Document; import org.junit.jupiter.api.Test; - import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; @@ -106,6 +105,22 @@ void rendersTargetValueExpressionCorrectly() { .containsExactly(Document.parse("{\"$set\" : {\"totalHomework\": { \"$sum\" : \"$homework\" }}}")); } + @Test // GH-4933 + void rendersTargetFieldReferenceCorrectly() { + + assertThat( + SetOperation.builder().set("totalHomework").toValueOf("homework").toPipelineStages(contextFor(Scores.class))) + .containsExactly(Document.parse("{\"$set\" : {\"totalHomework\": \"$homework\" }}")); + } + + @Test // GH-4933 + void rendersMappedTargetFieldReferenceCorrectly() { + + assertThat(SetOperation.builder().set("totalHomework").toValueOf("homework") + .toPipelineStages(contextFor(ScoresWithMappedField.class))) + .containsExactly(Document.parse("{\"$set\" : {\"totalHomework\": \"$home_work\" }}")); + } + @Test // DATAMONGO-2331 void exposesFieldsCorrectly() { From e419764023493d988d01d3c6e2be31576b0524e5 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 May 2025 11:22:54 +0200 Subject: [PATCH 21/74] Skip setting vector search limit if unlimited. Skip setting limit if unlimited and rely on server side command validation. Closes: #4963 --- .../core/aggregation/VectorSearchOperation.java | 5 ++++- .../VectorSearchOperationUnitTests.java | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java index bcc5fbd7bc..dd14ef20c9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java @@ -236,7 +236,10 @@ public Document toDocument(AggregationOperationContext context) { } $vectorSearch.append("index", indexName); - $vectorSearch.append("limit", limit.max()); + + if(limit.isLimited()) { + $vectorSearch.append("limit", limit.max()); + } if (numCandidates != null) { $vectorSearch.append("numCandidates", numCandidates); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java index 4ce045fe6f..c4628eda99 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperationUnitTests.java @@ -15,14 +15,14 @@ */ package org.springframework.data.mongodb.core.aggregation; -import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import java.util.List; import org.bson.Document; import org.junit.jupiter.api.Test; - import org.springframework.data.annotation.Id; +import org.springframework.data.domain.Limit; import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Criteria; @@ -103,6 +103,16 @@ void mapsCriteriaToDomainType() { .containsExactly(new Document("$vectorSearch", new Document($VECTOR_SEARCH).append("filter", filter))); } + @Test // GH-4963 + void shouldSkipLimitIfUnlimited() { + + VectorSearchOperation $search = VectorSearchOperation.search("vector_index").path("plot_embedding") + .vector(-0.0016261312, -0.028070757, -0.011342932).limit(Limit.unlimited()); + + List stages = $search.toPipelineStages(TestAggregationContext.contextFor(Movie.class)); + assertThat(stages.get(0)).doesNotContainKey("$vectorSearch.limit"); + } + static class Movie { @Id String id; From 2b8b59aec25ee1d70c2a9f0049afe2849494e002 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 7 May 2025 14:16:06 +0200 Subject: [PATCH 22/74] Upgrade to MongoDB driver 5.5.0 Closes: #4962 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 9f4b6bc897..962ae73ffe 100644 --- a/pom.xml +++ b/pom.xml @@ -27,7 +27,7 @@ multi spring-data-mongodb 3.5.0-SNAPSHOT - 5.4.0 + 5.5.0 1.19 From b947e9dc1ccba9d2d9ca962cad457afc76add875 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 9 May 2025 10:24:58 +0200 Subject: [PATCH 23/74] Adopt to documentation feature-flags in Commons. See #4954 --- .../modules/ROOT/pages/repositories/query-methods-details.adoc | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc index dfe4814955..614da0b059 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/query-methods-details.adoc @@ -1 +1,2 @@ +:feature-scroll: include::{commons}@data-commons::page$repositories/query-methods-details.adoc[] From 9f44d8bfb68282bfe7407cf6e553ce65911511c3 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 08:55:53 +0200 Subject: [PATCH 24/74] Update CI Properties. See #4954 --- .mvn/jvm.config | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.mvn/jvm.config b/.mvn/jvm.config index 32599cefea..e27f6e8f5e 100644 --- a/.mvn/jvm.config +++ b/.mvn/jvm.config @@ -8,3 +8,7 @@ --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED --add-opens jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED +--add-opens=java.base/java.util=ALL-UNNAMED +--add-opens=java.base/java.lang.reflect=ALL-UNNAMED +--add-opens=java.base/java.text=ALL-UNNAMED +--add-opens=java.desktop/java.awt.font=ALL-UNNAMED From cfbb83a80b3041fb9b3787f7597280c11158008e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 08:56:21 +0200 Subject: [PATCH 25/74] Update CI Properties. See #4954 --- ci/pipeline.properties | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 9eb163fde7..34eef52b6f 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,15 +1,15 @@ # Java versions java.main.tag=17.0.13_11-jdk-focal -java.next.tag=23.0.1_11-jdk-noble +java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard docker.java.main.image=library/eclipse-temurin:${java.main.tag} docker.java.next.image=library/eclipse-temurin:${java.next.tag} # Supported versions of MongoDB -docker.mongodb.6.0.version=6.0.10 -docker.mongodb.7.0.version=7.0.2 -docker.mongodb.8.0.version=8.0.0 +docker.mongodb.6.0.version=6.0.23 +docker.mongodb.7.0.version=7.0.20 +docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis docker.redis.6.version=6.2.13 From 28fdc1c9d938ae335ab936085b3a9cc08c9f39c2 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 09:00:22 +0200 Subject: [PATCH 26/74] Update CI Properties. See #4954 --- ci/pipeline.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 34eef52b6f..cb3670dee1 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,5 +1,5 @@ # Java versions -java.main.tag=17.0.13_11-jdk-focal +java.main.tag=17.0.15_6-jdk-focal java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard From d37fa9e94d3b696da32bce7ad5a4e6e4442d6f2e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 09:33:04 +0200 Subject: [PATCH 27/74] Update CI Properties. See #4954 --- ci/pipeline.properties | 1 + 1 file changed, 1 insertion(+) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index cb3670dee1..8dd2295acc 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -14,6 +14,7 @@ docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis docker.redis.6.version=6.2.13 docker.redis.7.version=7.2.4 +docker.valkey.8.version=8.1.1 # Docker environment settings docker.java.inside.basic=-v $HOME:/tmp/jenkins-home From df3abef71727cc4468f681e9036071f8af986753 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 15 May 2025 09:27:58 +0200 Subject: [PATCH 28/74] Polishing. Refine MongoVector factory methods for a more natural adoption and terminology when creating vectors. See #4706 --- .../mongodb/core/mapping/MongoVector.java | 88 ++++++++++++++++--- .../MongoConvertersIntegrationTests.java | 6 +- .../core/mapping/MongoVectorUnitTests.java | 79 +++++++++++++++++ 3 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoVectorUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoVector.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoVector.java index 3b2e0a45f1..f7e0d1ee3f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoVector.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoVector.java @@ -24,9 +24,9 @@ import org.springframework.util.ObjectUtils; /** - * MongoDB-specific extension to {@link Vector} based on Mongo's {@link BinaryVector}. Note that only float32 and int8 - * variants can be represented as floating-point numbers. int1 returns an all-zero array for {@link #toFloatArray()} and - * {@link #toDoubleArray()}. + * MongoDB-specific extension to {@link Vector} based on Mongo's {@link BinaryVector}. Note that only {@code float32} + * and {@code int8} variants can be represented as floating-point numbers. {@code int1} throws + * {@link UnsupportedOperationException} when calling {@link #toFloatArray()} and {@link #toDoubleArray()}. * * @author Mark Paluch * @since 4.5 @@ -40,15 +40,65 @@ public class MongoVector implements Vector { } /** - * Creates a new {@link MongoVector} from the given {@link BinaryVector}. + * Creates a new binary {@link MongoVector} using the given {@link BinaryVector}. * * @param v binary vector representation. - * @return the {@link MongoVector} for the given vector values. + * @return the {@link MongoVector} wrapping {@link BinaryVector}. */ public static MongoVector of(BinaryVector v) { return new MongoVector(v); } + /** + * Creates a new binary {@link MongoVector} using the given {@code data}. + *

+ * A {@link BinaryVector.DataType#INT8} vector is a vector of 8-bit signed integers where each byte in the vector + * represents an element of a vector, with values in the range {@code [-128, 127]}. + *

+ * NOTE: The byte array is not copied; changes to the provided array will be referenced in the created + * {@code MongoVector} instance. + * + * @param data the byte array representing the {@link BinaryVector.DataType#INT8} vector data. + * @return the {@link MongoVector} containing the given vector values to be represented as binary {@code int8}. + */ + public static MongoVector ofInt8(byte[] data) { + return of(BinaryVector.int8Vector(data)); + } + + /** + * Creates a new binary {@link MongoVector} using the given {@code data}. + *

+ * A {@link BinaryVector.DataType#FLOAT32} vector is a vector of floating-point numbers, where each element in the + * vector is a {@code float}. + *

+ * NOTE: The float array is not copied; changes to the provided array will be referenced in the created + * {@code MongoVector} instance. + * + * @param data the float array representing the {@link BinaryVector.DataType#FLOAT32} vector data. + * @return the {@link MongoVector} containing the given vector values to be represented as binary {@code float32}. + */ + public static MongoVector ofFloat(float... data) { + return of(BinaryVector.floatVector(data)); + } + + /** + * Creates a new binary {@link MongoVector} from the given {@link Vector}. + *

+ * A {@link BinaryVector.DataType#FLOAT32} vector is a vector of floating-point numbers, where each element in the + * vector is a {@code float}. The given {@link Vector} must be able to return a {@link Vector#toFloatArray() float} + * array. + *

+ * NOTE: The float array is not copied; changes to the provided array will be referenced in the created + * {@code MongoVector} instance. + * + * @param v the + * @return the {@link MongoVector} using vector values from the given {@link Vector} to be represented as binary + * float32. + */ + public static MongoVector fromFloat(Vector v) { + return of(BinaryVector.floatVector(v.toFloatArray())); + } + @Override public Class getType() { @@ -90,6 +140,11 @@ public int size() { return 0; } + /** + * {@inheritDoc} + * + * @throws UnsupportedOperationException if the underlying data type is {@code int1} {@link PackedBitBinaryVector}. + */ @Override public float[] toFloatArray() { @@ -102,14 +157,22 @@ public float[] toFloatArray() { if (v instanceof Int8BinaryVector i) { - float[] result = new float[i.getData().length]; - System.arraycopy(i.getData(), 0, result, 0, result.length); + byte[] data = i.getData(); + float[] result = new float[data.length]; + for (int j = 0; j < data.length; j++) { + result[j] = data[j]; + } return result; } - return new float[size()]; + throw new UnsupportedOperationException("Cannot return float array for " + v.getClass()); } + /** + * {@inheritDoc} + * + * @throws UnsupportedOperationException if the underlying data type is {@code int1} {@link PackedBitBinaryVector}. + */ @Override public double[] toDoubleArray() { @@ -126,12 +189,15 @@ public double[] toDoubleArray() { if (v instanceof Int8BinaryVector i) { - double[] result = new double[i.getData().length]; - System.arraycopy(i.getData(), 0, result, 0, result.length); + byte[] data = i.getData(); + double[] result = new double[data.length]; + for (int j = 0; j < data.length; j++) { + result[j] = data[j]; + } return result; } - return new double[size()]; + throw new UnsupportedOperationException("Cannot return double array for " + v.getClass()); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersIntegrationTests.java index b57ab35ea1..a1c2fc0897 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MongoConvertersIntegrationTests.java @@ -138,7 +138,7 @@ public void shouldReadAndWriteBinFloat32Vectors() { WithVectors source = new WithVectors(); source.binVector = BinaryVector.floatVector(new float[] { 1.1f, 2.2f, 3.3f }); - source.vector = MongoVector.of(source.binVector); + source.vector = MongoVector.ofFloat(new float[] { 1.1f, 2.2f, 3.3f }); template.save(source); @@ -146,6 +146,7 @@ public void shouldReadAndWriteBinFloat32Vectors() { assertThat(loaded.vector).isEqualTo(source.vector); assertThat(loaded.binVector).isEqualTo(source.binVector); + assertThat(loaded.binVector).isEqualTo(source.vector.getSource()); } @Test // GH-4706 @@ -153,7 +154,7 @@ public void shouldReadAndWriteBinInt8Vectors() { WithVectors source = new WithVectors(); source.binVector = BinaryVector.int8Vector(new byte[] { 1, 2, 3 }); - source.vector = MongoVector.of(source.binVector); + source.vector = MongoVector.ofInt8(new byte[] { 1, 2, 3 }); template.save(source); @@ -161,6 +162,7 @@ public void shouldReadAndWriteBinInt8Vectors() { assertThat(loaded.vector).isEqualTo(source.vector); assertThat(loaded.binVector).isEqualTo(source.binVector); + assertThat(loaded.binVector).isEqualTo(source.vector.getSource()); } @Test // GH-4706 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoVectorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoVectorUnitTests.java new file mode 100644 index 0000000000..31eeebdb83 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/MongoVectorUnitTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core.mapping; + +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import org.bson.BinaryVector; +import org.bson.Float32BinaryVector; +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Vector; + +/** + * Unit tests for {@link MongoVector}. + * + * @author Mark Paluch + */ +class MongoVectorUnitTests { + + @Test // GH-4706 + void shouldReturnInt8AsFloatingPoints() { + + MongoVector vector = MongoVector.ofInt8(new byte[] { 1, 2, 3 }); + + assertThat(vector.toDoubleArray()).contains(1, 2, 3); + assertThat(vector.toFloatArray()).contains(1, 2, 3); + } + + @Test // GH-4706 + void shouldReturnFloatAsFloatingPoints() { + + MongoVector vector = MongoVector.ofFloat(1f, 2f, 3f); + + assertThat(vector.toDoubleArray()).contains(1, 2, 3); + assertThat(vector.toFloatArray()).contains(1, 2, 3); + } + + @Test // GH-4706 + void ofFloatIsNotEqualToVectorOf() { + + MongoVector mv = MongoVector.ofFloat(1f, 2f, 3f); + Vector v = Vector.of(1f, 2f, 3f); + + assertThat(v).isNotEqualTo(mv); + } + + @Test // GH-4706 + void mongoVectorCanAdaptToFloatVector() { + + Vector v = Vector.of(1f, 2f, 3f); + MongoVector mv = MongoVector.fromFloat(v); + + assertThat(mv.toFloatArray()).isEqualTo(v.toFloatArray()); + assertThat(mv.getSource()).isInstanceOf(Float32BinaryVector.class); + } + + @Test // GH-4706 + void shouldNotReturnFloatsForPackedBit() { + + MongoVector vector = MongoVector.of(BinaryVector.packedBitVector(new byte[] { 1, 2, 3 }, (byte) 1)); + + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(vector::toFloatArray); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(vector::toDoubleArray); + } + +} From b229fa5be13737b9eae173a31d517b2465dd747f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:28:09 +0200 Subject: [PATCH 29/74] Prepare 4.5 GA (2025.0.0). See #4954 --- pom.xml | 20 ++++---------------- src/main/resources/notice.txt | 3 ++- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 962ae73ffe..193b9102ad 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 3.5.0-SNAPSHOT + 3.5.0 @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.5.0-SNAPSHOT + 3.5.0 5.5.0 1.19 @@ -157,20 +157,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 52ee00c4f5..3b6fc5c998 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 4.5 RC1 (2025.0.0) +Spring Data MongoDB 4.5 GA (2025.0.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -60,5 +60,6 @@ conditions of the subcomponent's license, as noted in the LICENSE file. + From 5fcf2ca3a75f312bc485aec54b7e9e9f6030b2a7 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:28:30 +0200 Subject: [PATCH 30/74] Release version 4.5 GA (2025.0.0). See #4954 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 193b9102ad..9f10d7846f 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0 pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 58c63dfc97..c0bdd90d4a 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0 ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index b842a2def3..ad6132cb01 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0-SNAPSHOT + 4.5.0 ../pom.xml From 4341dcee25fd985ad2bc2b289964fd23f44a1d48 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:31:15 +0200 Subject: [PATCH 31/74] Prepare next development iteration. See #4954 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 9f10d7846f..e931d39472 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0 + 4.6.0-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index c0bdd90d4a..ee8d4817eb 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0 + 4.6.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ad6132cb01..9eba5bf52d 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.5.0 + 4.6.0-SNAPSHOT ../pom.xml From 8b924f42e113eee83b58218e08102b671192061d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 11:31:16 +0200 Subject: [PATCH 32/74] After release cleanups. See #4954 --- pom.xml | 22 +++++++++++++++++----- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/pom.xml b/pom.xml index e931d39472..95fc8379d9 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.6.0-SNAPSHOT + 5.0.0-SNAPSHOT pom Spring Data MongoDB @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 3.5.0 + 4.0.0-SNAPSHOT @@ -26,7 +26,7 @@ multi spring-data-mongodb - 3.5.0 + 4.0.0-SNAPSHOT 5.5.0 1.19 @@ -157,8 +157,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index ee8d4817eb..fc88571622 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 4.6.0-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 9eba5bf52d..6109e29130 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 4.6.0-SNAPSHOT + 5.0.0-SNAPSHOT ../pom.xml From 59491d8f36d66b51e108d3dc219df0f60079dc9e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 19 Nov 2024 15:30:17 +0100 Subject: [PATCH 33/74] Adopt to deprecation removals in Commons. Closes #4837 --- .../core/convert/MappingMongoConverter.java | 3 +- .../UnwrappedMongoPersistentEntity.java | 6 -- .../repository/query/AbstractMongoQuery.java | 61 ++-------------- .../query/AbstractReactiveMongoQuery.java | 66 ------------------ .../query/DefaultSpELExpressionEvaluator.java | 69 ------------------- .../repository/query/PartTreeMongoQuery.java | 25 ------- .../query/ReactivePartTreeMongoQuery.java | 23 ------- .../query/ReactiveStringBasedAggregation.java | 20 ------ .../query/ReactiveStringBasedMongoQuery.java | 67 +----------------- .../query/StringBasedAggregation.java | 27 -------- .../query/StringBasedMongoQuery.java | 44 ------------ .../support/MongoRepositoryFactory.java | 2 - .../ReactiveMongoRepositoryFactory.java | 10 ++- .../ReactiveMongoRepositoryFactoryBean.java | 10 --- .../util/json/ParameterBindingContext.java | 25 ------- .../json/ParameterBindingDocumentCodec.java | 2 +- .../data/mongodb/core/MongoTemplateTests.java | 6 +- .../data/mongodb/core/Venue.java | 4 +- .../DbRefMappingMongoConverterUnitTests.java | 6 +- .../MappingMongoConverterUnitTests.java | 19 +++-- .../core/convert/ObjectPathUnitTests.java | 8 +-- .../core/convert/UpdateMapperUnitTests.java | 2 +- .../data/mongodb/core/geo/GeoJsonTests.java | 4 +- ...ersistentEntityIndexResolverUnitTests.java | 6 +- ...BasicMongoPersistentPropertyUnitTests.java | 12 ++-- .../data/mongodb/core/mapping/Person.java | 4 +- .../core/mapping/PersonCustomIdName.java | 4 +- .../mongodb/performance/PerformanceTests.java | 6 +- .../performance/ReactivePerformanceTests.java | 6 +- .../mongodb/repository/PersonAggregate.java | 4 +- .../ReactiveMongoRepositoryTests.java | 2 - .../SimpleReactiveMongoRepositoryTests.java | 2 - ...sitoryConfigurationExtensionUnitTests.java | 6 +- ...sitoryConfigurationExtensionUnitTests.java | 6 +- .../query/MongoQueryExecutionUnitTests.java | 15 ++-- .../query/PartTreeMongoQueryUnitTests.java | 6 +- .../ReactiveMongoQueryExecutionUnitTests.java | 6 +- ...activeStringBasedAggregationUnitTests.java | 20 +++--- ...eactiveStringBasedMongoQueryUnitTests.java | 14 ++-- .../StringBasedAggregationUnitTests.java | 20 +++--- .../ROOT/pages/mongodb/mapping/mapping.adoc | 8 +-- .../mongodb/template-query-operations.adoc | 2 +- 42 files changed, 101 insertions(+), 557 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 864cc1c3e3..1d40573b81 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -71,7 +71,6 @@ import org.springframework.data.mapping.model.PersistentEntityParameterValueProvider; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; -import org.springframework.data.mapping.model.SpELExpressionParameterValueProvider; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionParameterValueProvider; import org.springframework.data.mongodb.CodecRegistryProvider; @@ -2059,7 +2058,7 @@ public T getPropertyValue(MongoPersistentProperty property) { } /** - * Extension of {@link SpELExpressionParameterValueProvider} to recursively trigger value conversion on the raw + * Extension of {@link ValueExpressionParameterValueProvider} to recursively trigger value conversion on the raw * resolved SpEL value. * * @author Oliver Gierke diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java index fed08815b8..6b032a1558 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentEntity.java @@ -98,13 +98,7 @@ public String getName() { return delegate.getName(); } - @Override @Nullable - @Deprecated - public PreferredConstructor getPersistenceConstructor() { - return delegate.getPersistenceConstructor(); - } - @Override public InstanceCreatorMetadata getInstanceCreatorMetadata() { return delegate.getInstanceCreatorMetadata(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 4d0d604a27..910665253d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -21,11 +21,8 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; -import org.springframework.data.expression.ValueExpressionParser; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; @@ -47,16 +44,10 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -84,36 +75,6 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final ValueExpressionDelegate valueExpressionDelegate; private final ValueEvaluationContextProvider valueEvaluationContextProvider; - /** - * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "4.4.0") - public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, ExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(operations, "MongoOperations must not be null"); - Assert.notNull(method, "MongoQueryMethod must not be null"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "QueryMethodEvaluationContextProvider must not be null"); - - this.method = method; - this.operations = operations; - - MongoEntityMetadata metadata = method.getEntityInformation(); - Class type = metadata.getCollectionEntity().getType(); - - this.executableFind = operations.query(type); - this.executableUpdate = operations.update(type); - this.valueExpressionDelegate = new ValueExpressionDelegate(new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), evaluationContextProvider.getEvaluationContextProvider()), ValueExpressionParser.create(() -> expressionParser)); - this.valueEvaluationContextProvider = valueExpressionDelegate.createValueContextProvider(method.getParameters()); - } - /** * Creates a new {@link AbstractMongoQuery} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * @@ -185,7 +146,8 @@ protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, C } /** - * If present apply the {@link com.mongodb.ReadPreference} from the {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. + * If present apply the {@link com.mongodb.ReadPreference} from the + * {@link org.springframework.data.mongodb.repository.ReadPreference} annotation. * * @param query must not be {@literal null}. * @return never {@literal null}. @@ -396,20 +358,6 @@ protected ParameterBindingDocumentCodec getParameterBindingCodec() { return codec.get(); } - /** - * Obtain a the {@link EvaluationContext} suitable to evaluate expressions backed by the given dependencies. - * - * @param dependencies must not be {@literal null}. - * @param accessor must not be {@literal null}. - * @return the {@link SpELExpressionEvaluator}. - * @since 2.4 - */ - protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDependencies dependencies, - ConvertingParameterAccessor accessor) { - - return new DefaultSpELExpressionEvaluator(new SpelExpressionParser(), valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), dependencies).getEvaluationContext()); - } - /** * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions. * @@ -418,8 +366,9 @@ protected SpELExpressionEvaluator getSpELExpressionEvaluatorFor(ExpressionDepend * @since 4.4.0 */ protected ValueExpressionEvaluator getExpressionEvaluatorFor(MongoParameterAccessor accessor) { - return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, (ValueExpression expression) -> - valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies())); + return new ValueExpressionDelegateValueExpressionEvaluator(valueExpressionDelegate, + (ValueExpression expression) -> valueEvaluationContextProvider.getEvaluationContext(accessor.getValues(), + expression.getExpressionDependencies())); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index a5754a4e46..76b4b2e088 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -27,14 +27,11 @@ import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; -import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ReactiveValueEvaluationContextProvider; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.model.EntityInstantiators; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveFindOperation.FindWithProjection; @@ -56,14 +53,10 @@ import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -89,45 +82,6 @@ public abstract class AbstractReactiveMongoQuery implements RepositoryQuery { private final ValueExpressionDelegate valueExpressionDelegate; private final ReactiveValueEvaluationContextProvider valueEvaluationContextProvider; - /** - * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and - * {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param operations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated use the constructor version with {@link ValueExpressionDelegate} - */ - @Deprecated(since = "4.4.0") - public AbstractReactiveMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations operations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - - Assert.notNull(method, "MongoQueryMethod must not be null"); - Assert.notNull(operations, "ReactiveMongoOperations must not be null"); - Assert.notNull(expressionParser, "SpelExpressionParser must not be null"); - Assert.notNull(evaluationContextProvider, "ReactiveEvaluationContextExtension must not be null"); - - this.method = method; - this.operations = operations; - this.instantiators = new EntityInstantiators(); - this.valueExpressionDelegate = new ValueExpressionDelegate( - new QueryMethodValueEvaluationContextAccessor(new StandardEnvironment(), - evaluationContextProvider.getEvaluationContextProvider()), - ValueExpressionParser.create(() -> expressionParser)); - - MongoEntityMetadata metadata = method.getEntityInformation(); - Class type = metadata.getCollectionEntity().getType(); - - this.findOperationWithProjection = operations.query(type); - this.updateOps = operations.update(type); - ValueEvaluationContextProvider valueContextProvider = valueExpressionDelegate - .createValueContextProvider(method.getParameters()); - Assert.isInstanceOf(ReactiveValueEvaluationContextProvider.class, valueContextProvider, - "ValueEvaluationContextProvider must be reactive"); - this.valueEvaluationContextProvider = (ReactiveValueEvaluationContextProvider) valueContextProvider; - } - /** * Creates a new {@link AbstractReactiveMongoQuery} from the given {@link MongoQueryMethod} and * {@link MongoOperations}. @@ -460,26 +414,6 @@ protected Mono getParameterBindingCodec() { return getCodecRegistry().map(ParameterBindingDocumentCodec::new); } - /** - * Obtain a {@link Mono publisher} emitting the {@link SpELExpressionEvaluator} suitable to evaluate expressions - * backed by the given dependencies. - * - * @param dependencies must not be {@literal null}. - * @param accessor must not be {@literal null}. - * @return a {@link Mono} emitting the {@link SpELExpressionEvaluator} when ready. - * @since 3.4 - * @deprecated since 4.4.0, use - * {@link #getValueExpressionEvaluatorLater(ExpressionDependencies, MongoParameterAccessor)} instead - */ - @Deprecated(since = "4.4.0") - protected Mono getSpelEvaluatorFor(ExpressionDependencies dependencies, - MongoParameterAccessor accessor) { - return valueEvaluationContextProvider.getEvaluationContextLater(accessor.getValues(), dependencies) - .map(evaluationContext -> (SpELExpressionEvaluator) new DefaultSpELExpressionEvaluator( - new SpelExpressionParser(), evaluationContext.getEvaluationContext())) - .defaultIfEmpty(DefaultSpELExpressionEvaluator.unsupported()); - } - /** * Obtain a {@link ValueExpressionEvaluator} suitable to evaluate expressions. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java deleted file mode 100644 index 16a1e55226..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/DefaultSpELExpressionEvaluator.java +++ /dev/null @@ -1,69 +0,0 @@ -/* - * Copyright 2020-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.repository.query; - -import org.springframework.data.mapping.model.SpELExpressionEvaluator; -import org.springframework.expression.EvaluationContext; -import org.springframework.expression.ExpressionParser; - -/** - * Simple {@link SpELExpressionEvaluator} implementation using {@link ExpressionParser} and {@link EvaluationContext}. - * - * @author Mark Paluch - * @since 3.1 - */ -class DefaultSpELExpressionEvaluator implements SpELExpressionEvaluator { - - private final ExpressionParser parser; - private final EvaluationContext context; - - DefaultSpELExpressionEvaluator(ExpressionParser parser, EvaluationContext context) { - this.parser = parser; - this.context = context; - } - - /** - * Return a {@link SpELExpressionEvaluator} that does not support expression evaluation. - * - * @return a {@link SpELExpressionEvaluator} that does not support expression evaluation. - * @since 3.1 - */ - public static SpELExpressionEvaluator unsupported() { - return NoOpExpressionEvaluator.INSTANCE; - } - - @Override - @SuppressWarnings("unchecked") - public T evaluate(String expression) { - return (T) parser.parseExpression(expression).getValue(context, Object.class); - } - - /** - * {@link SpELExpressionEvaluator} that does not support SpEL evaluation. - * - * @author Mark Paluch - * @since 3.1 - */ - enum NoOpExpressionEvaluator implements SpELExpressionEvaluator { - - INSTANCE; - - @Override - public T evaluate(String expression) { - throw new UnsupportedOperationException("Expression evaluation not supported"); - } - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index afabf9c37e..fdf08ba9d8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -18,8 +18,6 @@ import org.bson.Document; import org.bson.json.JsonParseException; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; @@ -29,14 +27,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.expression.ExpressionParser; import org.springframework.util.StringUtils; /** @@ -54,26 +49,6 @@ public class PartTreeMongoQuery extends AbstractMongoQuery { private final MappingContext context; private final ResultProcessor processor; - /** - * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public PartTreeMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ExpressionParser expressionParser, - QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - this.processor = method.getResultProcessor(); - this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); - this.isGeoNearQuery = method.isGeoNearQuery(); - this.context = mongoOperations.getConverter().getMappingContext(); - } - /** * Creates a new {@link PartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index 5787cca5a5..b27adfab93 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -28,14 +28,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.expression.ExpressionParser; import org.springframework.util.StringUtils; /** @@ -52,26 +49,6 @@ public class ReactivePartTreeMongoQuery extends AbstractReactiveMongoQuery { private final MappingContext context; private final ResultProcessor processor; - /** - * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public ReactivePartTreeMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - this.processor = method.getResultProcessor(); - this.tree = new PartTree(method.getName(), processor.getReturnedType().getDomainType()); - this.isGeoNearQuery = method.isGeoNearQuery(); - this.context = mongoOperations.getConverter().getMappingContext(); - } - /** * Creates a new {@link ReactivePartTreeMongoQuery} from the given {@link QueryMethod} and {@link MongoTemplate}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index ff01d8f8a3..cf6e7231f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -29,11 +29,9 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.expression.ExpressionParser; import org.springframework.lang.Nullable; /** @@ -49,24 +47,6 @@ public class ReactiveStringBasedAggregation extends AbstractReactiveMongoQuery { private final ReactiveMongoOperations reactiveMongoOperations; private final MongoConverter mongoConverter; - /** - * @param method must not be {@literal null}. - * @param reactiveMongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedAggregation(ReactiveMongoQueryMethod method, - ReactiveMongoOperations reactiveMongoOperations, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - - super(method, reactiveMongoOperations, expressionParser, evaluationContextProvider); - - this.reactiveMongoOperations = reactiveMongoOperations; - this.mongoConverter = reactiveMongoOperations.getConverter(); - } - /** * @param method must not be {@literal null}. * @param reactiveMongoOperations must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index 0e980fcfaf..562ee026fc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -28,12 +28,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.NonNull; import org.springframework.util.Assert; @@ -59,67 +55,8 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery { private final boolean isDeleteQuery; /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod} and - * {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, - ExpressionParser expressionParser, ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - this(method.getAnnotatedQuery(), method, mongoOperations, expressionParser, evaluationContextProvider); - } - - /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link String}, {@link MongoQueryMethod}, - * {@link MongoOperations}, {@link SpelExpressionParser} and - * {@link ReactiveExtensionAwareQueryMethodEvaluationContextProvider}. - * - * @param query must not be {@literal null}. - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method, - ReactiveMongoOperations mongoOperations, ExpressionParser expressionParser, - ReactiveQueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - Assert.notNull(query, "Query must not be null"); - - this.query = query; - this.expressionParser = ValueExpressionParser.create(() -> expressionParser); - this.fieldSpec = method.getFieldSpecification(); - - if (method.hasAnnotatedQuery()) { - - org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); - - this.isCountQuery = queryAnnotation.count(); - this.isExistsQuery = queryAnnotation.exists(); - this.isDeleteQuery = queryAnnotation.delete(); - - if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { - throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); - } - - } else { - - this.isCountQuery = false; - this.isExistsQuery = false; - this.isDeleteQuery = false; - } - } - - /** - * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod}, - * {@link MongoOperations} and {@link ValueExpressionDelegate}. + * Creates a new {@link ReactiveStringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations} + * and {@link ValueExpressionDelegate}. * * @param method must not be {@literal null}. * @param mongoOperations must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 7ad5d78fa6..5596435eb0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -29,12 +29,9 @@ import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.expression.ExpressionParser; import org.springframework.lang.Nullable; /** @@ -51,30 +48,6 @@ public class StringBasedAggregation extends AbstractMongoQuery { private final MongoOperations mongoOperations; private final MongoConverter mongoConverter; - /** - * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link QueryMethodValueEvaluationContextAccessor} instead. - */ - @Deprecated(since = "4.4.0") - public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOperations, - ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - if (method.isPageQuery()) { - throw new InvalidMongoDbApiUsageException(String.format( - "Repository aggregation method '%s' does not support '%s' return type; Please use 'Slice' or 'List' instead", - method.getName(), method.getReturnType().getType().getSimpleName())); - } - - this.mongoOperations = mongoOperations; - this.mongoConverter = mongoOperations.getConverter(); - } - /** * Creates a new {@link StringBasedAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index abc158f88a..5e2fba381b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -22,11 +22,8 @@ import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.expression.ExpressionParser; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.util.Assert; /** @@ -49,47 +46,6 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { private final boolean isExistsQuery; private final boolean isDeleteQuery; - /** - * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations}, - * {@link SpelExpressionParser} and {@link QueryMethodEvaluationContextProvider}. - * - * @param method must not be {@literal null}. - * @param mongoOperations must not be {@literal null}. - * @param expressionParser must not be {@literal null}. - * @param evaluationContextProvider must not be {@literal null}. - * @deprecated since 4.4.0, use the constructors accepting {@link ValueExpressionDelegate} instead. - */ - @Deprecated(since = "4.4.0") - public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, - ExpressionParser expressionParser, QueryMethodEvaluationContextProvider evaluationContextProvider) { - super(method, mongoOperations, expressionParser, evaluationContextProvider); - - String query = method.getAnnotatedQuery(); - Assert.notNull(query, "Query must not be null"); - - this.query = query; - this.fieldSpec = method.getFieldSpecification(); - - if (method.hasAnnotatedQuery()) { - - org.springframework.data.mongodb.repository.Query queryAnnotation = method.getQueryAnnotation(); - - this.isCountQuery = queryAnnotation.count(); - this.isExistsQuery = queryAnnotation.exists(); - this.isDeleteQuery = queryAnnotation.delete(); - - if (hasAmbiguousProjectionFlags(this.isCountQuery, this.isExistsQuery, this.isDeleteQuery)) { - throw new IllegalArgumentException(String.format(COUNT_EXISTS_AND_DELETE, method)); - } - - } else { - - this.isCountQuery = false; - this.isExistsQuery = false; - this.isDeleteQuery = false; - } - } - /** * Creates a new {@link StringBasedMongoQuery} for the given {@link MongoQueryMethod}, {@link MongoOperations}, * {@link ValueExpressionDelegate}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index baf069c3a4..91a3e39cbe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -42,7 +42,6 @@ import org.springframework.data.repository.core.support.RepositoryFactorySupport; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; -import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; @@ -61,7 +60,6 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final MongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; - @Nullable private QueryMethodValueEvaluationContextAccessor accessor; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 3edfcdd2db..fe18fda758 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -21,7 +21,6 @@ import java.lang.reflect.Method; import java.util.Optional; -import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -44,7 +43,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; @@ -77,7 +75,6 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { this.operations = mongoOperations; this.mappingContext = mongoOperations.getConverter().getMappingContext(); - setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } @@ -132,7 +129,8 @@ protected Object getTargetRepository(RepositoryInformation information) { return targetRepository; } - @Override protected Optional getQueryLookupStrategy(Key key, + @Override + protected Optional getQueryLookupStrategy(Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } @@ -159,8 +157,8 @@ private MongoEntityInformation getEntityInformation(Class doma * @author Christoph Strobl */ private record MongoQueryLookupStrategy(ReactiveMongoOperations operations, - MappingContext, MongoPersistentProperty> mappingContext, - ValueExpressionDelegate delegate) implements QueryLookupStrategy { + MappingContext, MongoPersistentProperty> mappingContext, + ValueExpressionDelegate delegate) implements QueryLookupStrategy { @Override public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, ProjectionFactory factory, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index 4f9c0d945c..dfb7c00fe6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -16,17 +16,13 @@ package org.springframework.data.mongodb.repository.support; import java.io.Serializable; -import java.util.Optional; -import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.index.IndexOperationsAdapter; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -94,12 +90,6 @@ protected RepositoryFactorySupport createRepositoryFactory() { return factory; } - @Override - protected Optional createDefaultQueryMethodEvaluationContextProvider( - ListableBeanFactory beanFactory) { - return Optional.of(new ReactiveExtensionAwareQueryMethodEvaluationContextProvider(beanFactory)); - } - /** * Creates and initializes a {@link RepositoryFactorySupport} instance. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java index b4fd13b3af..5678d959ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingContext.java @@ -19,7 +19,6 @@ import java.util.function.Function; import java.util.function.Supplier; -import org.springframework.data.mapping.model.SpELExpressionEvaluator; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; @@ -28,7 +27,6 @@ import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; /** @@ -44,29 +42,6 @@ public class ParameterBindingContext { private final ValueProvider valueProvider; private final ValueExpressionEvaluator expressionEvaluator; - /** - * @param valueProvider - * @param expressionParser - * @param evaluationContext - * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ExpressionParser, Supplier)} instead. - */ - @Deprecated(since = "4.4.0") - public ParameterBindingContext(ValueProvider valueProvider, SpelExpressionParser expressionParser, - EvaluationContext evaluationContext) { - this(valueProvider, expressionParser, () -> evaluationContext); - } - - /** - * @param valueProvider - * @param expressionEvaluator - * @since 3.1 - * @deprecated since 4.4.0, use {@link #ParameterBindingContext(ValueProvider, ValueExpressionEvaluator)} instead. - */ - @Deprecated(since = "4.4.0") - public ParameterBindingContext(ValueProvider valueProvider, SpELExpressionEvaluator expressionEvaluator) { - this(valueProvider, (ValueExpressionEvaluator) expressionEvaluator); - } - /** * @param valueProvider * @param expressionParser diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index ffa226ab69..adce99c904 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -170,7 +170,7 @@ public void encode(final BsonWriter writer, final Document document, final Encod public Document decode(@Nullable String json, Object[] values) { return decode(json, new ParameterBindingContext((index) -> values[index], new SpelExpressionParser(), - EvaluationContextProvider.DEFAULT.getEvaluationContext(values))); + () -> EvaluationContextProvider.DEFAULT.getEvaluationContext(values))); } public Document decode(@Nullable String json, ParameterBindingContext bindingContext) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index 6aaec4011e..f3aeec6de8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -49,7 +49,7 @@ import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.annotation.Id; import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Version; import org.springframework.data.auditing.IsNewAwareAuditingHandler; import org.springframework.data.domain.PageRequest; @@ -4482,7 +4482,7 @@ static class TestClass { LocalDateTime myDate; - @PersistenceConstructor + @PersistenceCreator TestClass(LocalDateTime myDate) { this.myDate = myDate; } @@ -4739,7 +4739,7 @@ static class DocumentWithLazyDBrefUsedInPresistenceConstructor { @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocUsedInCtor; @org.springframework.data.mongodb.core.mapping.DBRef(lazy = true) Document refToDocNotUsedInCtor; - @PersistenceConstructor + @PersistenceCreator public DocumentWithLazyDBrefUsedInPresistenceConstructor(Document refToDocUsedInCtor) { this.refToDocUsedInCtor = refToDocUsedInCtor; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java index 09a0605ed7..13aba70afa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Venue.java @@ -19,7 +19,7 @@ import java.util.Date; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.mapping.Document; @Document("newyork") @@ -30,7 +30,7 @@ public class Venue { private double[] location; private Date openingDate; - @PersistenceConstructor + @PersistenceCreator Venue(String name, double[] location) { super(); this.name = name; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index b53531f301..787e8d6746 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -46,7 +46,7 @@ import org.springframework.data.annotation.AccessType; import org.springframework.data.annotation.AccessType.Type; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mongodb.MongoDatabaseFactory; @@ -774,7 +774,7 @@ static class LazyDbRefTargetWithPeristenceConstructor extends LazyDbRefTarget { public LazyDbRefTargetWithPeristenceConstructor() {} - @PersistenceConstructor + @PersistenceCreator LazyDbRefTargetWithPeristenceConstructor(String id, String value) { super(id, value); this.persistenceConstructorCalled = true; @@ -790,7 +790,7 @@ static class LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor e boolean persistenceConstructorCalled; - @PersistenceConstructor + @PersistenceCreator LazyDbRefTargetWithPeristenceConstructorWithoutDefaultConstructor(String id, String value) { super(id, value); this.persistenceConstructorCalled = true; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index cf6d69c6c3..a343d15c7e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -63,7 +63,7 @@ import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Transient; import org.springframework.data.annotation.TypeAlias; import org.springframework.data.convert.ConverterBuilder; @@ -103,7 +103,6 @@ import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; -import org.springframework.data.util.ClassTypeInformation; import org.springframework.data.util.TypeInformation; import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; @@ -1061,7 +1060,7 @@ void convertsSetToBasicDBList() { address.city = "London"; address.street = "Foo"; - Object result = converter.convertToMongoType(Collections.singleton(address), ClassTypeInformation.OBJECT); + Object result = converter.convertToMongoType(Collections.singleton(address), TypeInformation.OBJECT); assertThat(result).isInstanceOf(List.class); Set readResult = converter.read(Set.class, (org.bson.Document) result); @@ -1393,7 +1392,7 @@ void convertsListToBasicDBListAndRetainsTypeInformationForComplexObjects() { address.street = "Foo"; Object result = converter.convertToMongoType(Collections.singletonList(address), - ClassTypeInformation.from(InterfaceType.class)); + TypeInformation.of(InterfaceType.class)); assertThat(result).isInstanceOf(List.class); @@ -1421,7 +1420,7 @@ void convertsArrayToBasicDBListAndRetainsTypeInformationForComplexObjects() { address.city = "London"; address.street = "Foo"; - Object result = converter.convertToMongoType(new Address[] { address }, ClassTypeInformation.OBJECT); + Object result = converter.convertToMongoType(new Address[] { address }, TypeInformation.OBJECT); assertThat(result).isInstanceOf(List.class); @@ -1712,7 +1711,7 @@ void shouldIncludeTextScorePropertyWhenReading() { } @Test // DATAMONGO-1001, DATAMONGO-1509 - void shouldWriteCglibProxiedClassTypeInformationCorrectly() { + void shouldWriteCglibProxiedTypeInformationCorrectly() { ProxyFactory factory = new ProxyFactory(); factory.setTargetClass(GenericType.class); @@ -2318,7 +2317,7 @@ void readAndConvertDBRefNestedByMapCorrectly() { Mockito.doReturn(cluster).when(spyConverter).readRef(dbRef); Map result = spyConverter.readMap(spyConverter.getConversionContext(ObjectPath.ROOT), data, - ClassTypeInformation.MAP); + TypeInformation.MAP); assertThat(((Map) result.get("cluster")).get("_id")).isEqualTo(100L); } @@ -3522,7 +3521,7 @@ static class Person implements Contact { } - @PersistenceConstructor + @PersistenceCreator public Person(Set

addresses) { this.addresses = addresses; } @@ -3802,7 +3801,7 @@ static class PrimitiveContainer { @Field("property") private int m_property; - @PersistenceConstructor + @PersistenceCreator public PrimitiveContainer(@Value("#root.property") int a_property) { m_property = a_property; } @@ -3817,7 +3816,7 @@ static class ObjectContainer { @Field("property") private PrimitiveContainer m_property; - @PersistenceConstructor + @PersistenceCreator public ObjectContainer(@Value("#root.property") PrimitiveContainer a_property) { m_property = a_property; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java index b772772444..9e58693faa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ObjectPathUnitTests.java @@ -23,7 +23,7 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; /** * Unit tests for {@link ObjectPath}. @@ -39,9 +39,9 @@ public class ObjectPathUnitTests { @BeforeEach public void setUp() { - one = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityOne.class)); - two = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityTwo.class)); - three = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(EntityThree.class)); + one = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityOne.class)); + two = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityTwo.class)); + three = new BasicMongoPersistentEntity<>(TypeInformation.of(EntityThree.class)); } @Test // DATAMONGO-1703 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java index d8e36c8f67..c646af5539 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/UpdateMapperUnitTests.java @@ -544,7 +544,7 @@ void doesNotConvertRawDocuments() { } @Test // DATAMONG0-471 - void testUpdateShouldRetainClassTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() { + void testUpdateShouldRetainTypeInformationWhenUsing$addToSetWith$eachForCustomTypes() { Update update = new Update().addToSet("models").each(new ModelImpl(2014), new ModelImpl(1), new ModelImpl(28)); Document mappedObject = mapper.getMappedObject(update.getUpdateObject(), diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java index b81b51abd5..96c685275f 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoJsonTests.java @@ -33,7 +33,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.dao.DataAccessException; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -536,7 +536,7 @@ static class Venue2DSphere { private String name; private @GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE) double[] location; - @PersistenceConstructor + @PersistenceCreator public Venue2DSphere(String name, double[] location) { this.name = name; this.location = location; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index aa26445f2d..d1fa2b1b9a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -53,7 +53,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Unwrapped; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; /** * Tests for {@link MongoPersistentEntityIndexResolver}. @@ -1186,7 +1186,7 @@ public void shouldNotDetectCycleWhenTypeIsUsedMoreThanOnce() { @SuppressWarnings({ "rawtypes", "unchecked" }) public void shouldCatchCyclicReferenceExceptionOnRoot() { - MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Object.class)); + MongoPersistentEntity entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Object.class)); MongoPersistentProperty propertyMock = mock(MongoPersistentProperty.class); when(propertyMock.isEntity()).thenReturn(true); @@ -1195,7 +1195,7 @@ public void shouldCatchCyclicReferenceExceptionOnRoot() { new MongoPersistentEntityIndexResolver.CyclicPropertyReferenceException("foo", Object.class, "bar")); MongoPersistentEntity selfCyclingEntity = new BasicMongoPersistentEntity<>( - ClassTypeInformation.from(SelfCyclingViaCollectionType.class)); + TypeInformation.of(SelfCyclingViaCollectionType.class)); new MongoPersistentEntityIndexResolver(prepareMappingContext(SelfCyclingViaCollectionType.class)) .resolveIndexForEntity(selfCyclingEntity); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java index 116505143e..1037ba4f19 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentPropertyUnitTests.java @@ -39,7 +39,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.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.ReflectionUtils; /** @@ -56,7 +56,7 @@ public class BasicMongoPersistentPropertyUnitTests { @BeforeEach void setup() { - entity = new BasicMongoPersistentEntity<>(ClassTypeInformation.from(Person.class)); + entity = new BasicMongoPersistentEntity<>(TypeInformation.of(Person.class)); } @Test @@ -90,7 +90,7 @@ void preventsNegativeOrder() { void usesPropertyAccessForThrowableCause() { BasicMongoPersistentEntity entity = new BasicMongoPersistentEntity<>( - ClassTypeInformation.from(Throwable.class)); + TypeInformation.of(Throwable.class)); MongoPersistentProperty property = getPropertyFor(entity, "cause"); assertThat(property.usePropertyAccess()).isTrue(); @@ -99,7 +99,7 @@ void usesPropertyAccessForThrowableCause() { @Test // DATAMONGO-607 void usesCustomFieldNamingStrategyByDefault() throws Exception { - ClassTypeInformation type = ClassTypeInformation.from(Person.class); + TypeInformation type = TypeInformation.of(Person.class); Field field = ReflectionUtils.findField(Person.class, "lastname"); MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity, @@ -116,7 +116,7 @@ void usesCustomFieldNamingStrategyByDefault() throws Exception { @Test // DATAMONGO-607 void rejectsInvalidValueReturnedByFieldNamingStrategy() { - ClassTypeInformation type = ClassTypeInformation.from(Person.class); + TypeInformation type = TypeInformation.of(Person.class); Field field = ReflectionUtils.findField(Person.class, "lastname"); MongoPersistentProperty property = new BasicMongoPersistentProperty(Property.of(type, field), entity, @@ -255,7 +255,7 @@ private MongoPersistentProperty getPropertyFor(Field field) { } private static MongoPersistentProperty getPropertyFor(Class type, String fieldname) { - return getPropertyFor(new BasicMongoPersistentEntity<>(ClassTypeInformation.from(type)), fieldname); + return getPropertyFor(new BasicMongoPersistentEntity<>(TypeInformation.of(type)), fieldname); } private static MongoPersistentProperty getPropertyFor(MongoPersistentEntity entity, String fieldname) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java index 06f0db6c35..eaed01fc3b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/Person.java @@ -18,7 +18,7 @@ import java.util.List; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.annotation.Transient; import org.springframework.data.mongodb.core.index.CompoundIndex; import org.springframework.data.mongodb.core.index.CompoundIndexes; @@ -44,7 +44,7 @@ public Person(Integer ssn) { this.ssn = ssn; } - @PersistenceConstructor + @PersistenceCreator public Person(Integer ssn, String firstName, String lastName, Integer age, T address) { this.ssn = ssn; this.firstName = firstName; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java index a68fe0d531..4a4f7fb126 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/PersonCustomIdName.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; /** * @author Jon Brisbin @@ -30,7 +30,7 @@ public PersonCustomIdName(Integer ssn, String firstName) { this.firstName = firstName; } - @PersistenceConstructor + @PersistenceCreator public PersonCustomIdName(Integer ssn, String firstName, String lastName) { this.ssn = ssn; this.firstName = firstName; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java index e815cc6e7c..fb8cedd9b1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/PerformanceTests.java @@ -28,7 +28,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.Constants; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; @@ -454,7 +454,7 @@ public Address(String zipCode, String city) { this(zipCode, city, new HashSet(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values())))); } - @PersistenceConstructor + @PersistenceCreator public Address(String zipCode, String city, Set types) { this.zipCode = zipCode; this.city = city; @@ -512,7 +512,7 @@ public Order(List lineItems, Date createdAt) { this.status = Status.ORDERED; } - @PersistenceConstructor + @PersistenceCreator public Order(List lineItems, Date createdAt, Status status) { this.lineItems = lineItems; this.createdAt = createdAt; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java index edda1aad01..3e5f416caf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java @@ -32,7 +32,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.core.Constants; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; @@ -513,7 +513,7 @@ public Address(String zipCode, String city) { this(zipCode, city, new HashSet(pickRandomNumerOfItemsFrom(Arrays.asList(AddressType.values())))); } - @PersistenceConstructor + @PersistenceCreator public Address(String zipCode, String city, Set types) { this.zipCode = zipCode; this.city = city; @@ -571,7 +571,7 @@ public Order(List lineItems, Date createdAt) { this.status = Status.ORDERED; } - @PersistenceConstructor + @PersistenceCreator public Order(List lineItems, Date createdAt, Status status) { this.lineItems = lineItems; this.createdAt = createdAt; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java index 16b2157bc8..da22801ba6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonAggregate.java @@ -22,7 +22,7 @@ import java.util.Set; import org.springframework.data.annotation.Id; -import org.springframework.data.annotation.PersistenceConstructor; +import org.springframework.data.annotation.PersistenceCreator; /** * @author Christoph Strobl @@ -37,7 +37,7 @@ public PersonAggregate(String lastname, String name) { this(lastname, Collections.singletonList(name)); } - @PersistenceConstructor + @PersistenceCreator public PersonAggregate(String lastname, Collection names) { this.lastname = lastname; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index e89dec21bd..1a481b49ed 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -72,7 +72,6 @@ import org.springframework.data.mongodb.test.util.ReactiveMongoClientClosingTestConfiguration; import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.Repository; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.test.context.junit.jupiter.SpringExtension; /** @@ -111,7 +110,6 @@ ReactiveMongoRepositoryFactory factory(ReactiveMongoOperations template, BeanFac factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class); factory.setBeanClassLoader(beanFactory.getClass().getClassLoader()); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); return factory; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 44235c54ef..9198e002c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -48,7 +48,6 @@ import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; @@ -96,7 +95,6 @@ void setUp() { factory.setRepositoryBaseClass(SimpleReactiveMongoRepository.class); factory.setBeanClassLoader(classLoader); factory.setBeanFactory(beanFactory); - factory.setEvaluationContextProvider(ReactiveQueryMethodEvaluationContextProvider.DEFAULT); repository = factory.getRepository(ReactivePersonRepository.class); immutableRepository = factory.getRepository(ReactiveImmutablePersonRepository.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java index f613beb6d5..a222deca39 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtensionUnitTests.java @@ -27,7 +27,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.Repository; @@ -43,13 +43,13 @@ */ public class MongoRepositoryConfigurationExtensionUnitTests { - StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); ResourceLoader loader = new PathMatchingResourcePatternResolver(); Environment environment = new StandardEnvironment(); BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableMongoRepositories.class, loader, environment, registry); + EnableMongoRepositories.class, loader, environment, registry, null); @Test // DATAMONGO-1009 public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java index 45ecba992f..2b52204f74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtensionUnitTests.java @@ -27,7 +27,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.core.type.StandardAnnotationMetadata; +import org.springframework.core.type.AnnotationMetadata; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; @@ -43,13 +43,13 @@ */ public class ReactiveMongoRepositoryConfigurationExtensionUnitTests { - StandardAnnotationMetadata metadata = new StandardAnnotationMetadata(Config.class, true); + AnnotationMetadata metadata = AnnotationMetadata.introspect(Config.class); ResourceLoader loader = new PathMatchingResourcePatternResolver(); Environment environment = new StandardEnvironment(); BeanDefinitionRegistry registry = new DefaultListableBeanFactory(); RepositoryConfigurationSource configurationSource = new AnnotationRepositoryConfigurationSource(metadata, - EnableReactiveMongoRepositories.class, loader, environment, registry); + EnableReactiveMongoRepositories.class, loader, environment, registry, null); @Test // DATAMONGO-1444 public void isStrictMatchIfDomainTypeIsAnnotatedWithDocument() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java index 74ff20b148..326ccf5f3a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java @@ -56,8 +56,7 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.util.ReflectionUtils; import com.mongodb.client.result.DeleteResult; @@ -81,15 +80,13 @@ class MongoQueryExecutionUnitTests { @Mock TerminatingFindNear terminatingGeoMock; @Mock DbRefResolver dbRefResolver; - private SpelExpressionParser EXPRESSION_PARSER = new SpelExpressionParser(); private Point POINT = new Point(10, 20); private Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); private MongoMappingContext context = new MongoMappingContext(); private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); private Method method = ReflectionUtils.findMethod(PersonRepository.class, "findByLocationNear", Point.class, - Distance.class, - Pageable.class); + Distance.class, Pageable.class); private MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); private MappingMongoConverter converter; @@ -152,8 +149,8 @@ void pagingGeoExecutionShouldUseCountFromResultWithOffsetAndResultsWithinPageSiz ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(0, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, - QueryMethodEvaluationContextProvider.DEFAULT); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, + ValueExpressionDelegate.create()); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); @@ -173,8 +170,8 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() { ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(converter, new MongoParametersParameterAccessor(queryMethod, new Object[] { POINT, DISTANCE, PageRequest.of(2, 10) })); - PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, EXPRESSION_PARSER, - QueryMethodEvaluationContextProvider.DEFAULT); + PartTreeMongoQuery query = new PartTreeMongoQuery(queryMethod, mongoOperationsMock, + ValueExpressionDelegate.create()); PagingGeoNearExecution execution = new PagingGeoNearExecution(findOperationMock, queryMethod, accessor, query); execution.execute(new Query()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java index e0b9b77099..07c10592d9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQueryUnitTests.java @@ -45,8 +45,7 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; /** * Unit tests for {@link PartTreeMongoQuery}. @@ -206,8 +205,7 @@ private PartTreeMongoQuery createQueryForMethod(String methodName, Class... p MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(Repo.class), factory, mappingContext); - return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, new SpelExpressionParser(), - QueryMethodEvaluationContextProvider.DEFAULT); + return new PartTreeMongoQuery(queryMethod, mongoOperationsMock, ValueExpressionDelegate.create()); } catch (Exception e) { throw new IllegalArgumentException(e.getMessage(), e); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java index 21d5dc71fb..d7a3430048 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java @@ -46,7 +46,7 @@ import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.DeleteExecution; import org.springframework.data.mongodb.repository.query.ReactiveMongoQueryExecution.GeoNearExecution; -import org.springframework.data.util.ClassTypeInformation; +import org.springframework.data.util.TypeInformation; import org.springframework.util.ClassUtils; import com.mongodb.client.result.DeleteResult; @@ -74,7 +74,7 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception { .thenReturn(Range.from(Bound.inclusive(new Distance(10))).to(Bound.inclusive(new Distance(15)))); when(parameterAccessor.getPageable()).thenReturn(PageRequest.of(1, 10)); - new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query, + new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query, Person.class, "person"); ArgumentCaptor queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class); @@ -96,7 +96,7 @@ public void geoNearExecutionShouldApplyMinimalSettings() throws Exception { when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2)); when(parameterAccessor.getDistanceRange()).thenReturn(Range.unbounded()); - new GeoNearExecution(operations, parameterAccessor, ClassTypeInformation.fromReturnTypeOf(geoNear)).execute(query, + new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query, Person.class, "person"); ArgumentCaptor queryArgumentCaptor = ArgumentCaptor.forClass(NearQuery.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index c6047ce30d..b4bc48cadf 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -34,6 +34,7 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.reactivestreams.Publisher; + import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -54,9 +55,8 @@ import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.reactive.ReactiveCrudRepository; -import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -71,8 +71,6 @@ @ExtendWith(MockitoExtension.class) public class ReactiveStringBasedAggregationUnitTests { - SpelExpressionParser PARSER = new SpelExpressionParser(); - @Mock ReactiveMongoOperations operations; @Mock DbRefResolver dbRefResolver; MongoConverter converter; @@ -226,8 +224,7 @@ private ReactiveStringBasedAggregation createAggregationForMethod(String name, C ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - return new ReactiveStringBasedAggregation(queryMethod, operations, PARSER, - ReactiveQueryMethodEvaluationContextProvider.DEFAULT); + return new ReactiveStringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create()); } private List pipelineOf(AggregationInvocation invocation) { @@ -250,19 +247,18 @@ private Collation collationOf(AggregationInvocation invocation) { @Nullable private Object hintOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + return invocation.aggregation.getOptions() != null + ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; } private Boolean skipResultsOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() - : false; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false; } @Nullable private ReadPreference readPreferenceOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() - : null; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null; } private Class targetTypeOf(AggregationInvocation invocation) { @@ -284,7 +280,7 @@ private interface SampleRepository extends ReactiveCrudRepository @Aggregation(GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER) Mono spelParameterReplacementAggregation(String arg0); - @Aggregation(pipeline = {RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER}) + @Aggregation(pipeline = { RAW_GROUP_BY_LASTNAME_STRING, GROUP_BY_LASTNAME_STRING_WITH_SPEL_PARAMETER_PLACEHOLDER }) Mono multiOperationPipeline(String arg0); @Aggregation(pipeline = RAW_GROUP_BY_LASTNAME_STRING, collation = "de_AT") diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java index 72f9626a57..7358bf4ce6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQueryUnitTests.java @@ -56,8 +56,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; -import org.springframework.data.repository.query.ReactiveExtensionAwareQueryMethodEvaluationContextProvider; -import org.springframework.data.repository.query.ReactiveQueryMethodEvaluationContextProvider; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.spi.EvaluationContextExtension; import org.springframework.data.spel.spi.ReactiveEvaluationContextExtension; @@ -248,8 +246,8 @@ public void shouldSupportNonQuotedBinaryDataReplacement() throws Exception { ReactiveStringBasedMongoQuery mongoQuery = createQueryForMethod("findByLastnameAsBinary", byte[].class); org.springframework.data.mongodb.core.query.Query query = mongoQuery.createQuery(accesor).block(); - org.springframework.data.mongodb.core.query.Query reference = new BasicQuery( - "{'lastname' : { '$binary' : '" + Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}"); + org.springframework.data.mongodb.core.query.Query reference = new BasicQuery("{'lastname' : { '$binary' : '" + + Base64.getEncoder().encodeToString(binaryData) + "', '$type' : '" + 0 + "'}}"); assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson()); } @@ -266,16 +264,14 @@ void shouldConsiderReactiveSpelExtension() throws Exception { assertThat(query.getQueryObject().toJson()).isEqualTo(reference.getQueryObject().toJson()); } - private ReactiveStringBasedMongoQuery createQueryForMethod( - String name, Class... parameters) - throws Exception { + private ReactiveStringBasedMongoQuery createQueryForMethod(String name, Class... parameters) throws Exception { Method method = SampleRepository.class.getMethod(name, parameters); ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); ReactiveMongoQueryMethod queryMethod = new ReactiveMongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor( - environment, Collections.singletonList(ReactiveSpelExtension.INSTANCE)); + QueryMethodValueEvaluationContextAccessor accessor = new QueryMethodValueEvaluationContextAccessor(environment, + Collections.singletonList(ReactiveSpelExtension.INSTANCE)); return new ReactiveStringBasedMongoQuery(queryMethod, operations, new ValueExpressionDelegate(accessor, PARSER)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 85a8650b26..463bb2a22a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -35,6 +35,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; + import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -62,8 +63,7 @@ import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; -import org.springframework.data.repository.query.QueryMethodEvaluationContextProvider; -import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -81,8 +81,6 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class StringBasedAggregationUnitTests { - private SpelExpressionParser PARSER = new SpelExpressionParser(); - @Mock MongoOperations operations; @Mock DbRefResolver dbRefResolver; @Mock AggregationResults aggregationResults; @@ -254,8 +252,7 @@ void aggregateRaisesErrorOnInvalidReturnType() { factory, converter.getMappingContext()); assertThatExceptionOfType(InvalidMongoDbApiUsageException.class) // - .isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, PARSER, - QueryMethodEvaluationContextProvider.DEFAULT)) // + .isThrownBy(() -> new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create())) // .withMessageContaining("pageIsUnsupported") // .withMessageContaining("Page"); } @@ -311,7 +308,7 @@ private StringBasedAggregation createAggregationForMethod(String name, Class. ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(SampleRepository.class), factory, converter.getMappingContext()); - return new StringBasedAggregation(queryMethod, operations, PARSER, QueryMethodEvaluationContextProvider.DEFAULT); + return new StringBasedAggregation(queryMethod, operations, ValueExpressionDelegate.create()); } private List pipelineOf(AggregationInvocation invocation) { @@ -334,19 +331,18 @@ private Collation collationOf(AggregationInvocation invocation) { @Nullable private Object hintOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) + return invocation.aggregation.getOptions() != null + ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; } private Boolean skipResultsOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() - : false; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().isSkipResults() : false; } @Nullable private ReadPreference readPreferenceOf(AggregationInvocation invocation) { - return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() - : null; + return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getReadPreference() : null; } private Class targetTypeOf(AggregationInvocation invocation) { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc index d76266c36a..3b5b4e49fe 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mapping/mapping.adoc @@ -465,7 +465,7 @@ This can be a single value (the _id_ by default), or a `Document` provided via a * `@Transient`: By default, all fields are mapped to the document. This annotation excludes the field where it is applied from being stored in the database. Transient properties cannot be used within a persistence constructor as the converter cannot materialize a value for the constructor argument. -* `@PersistenceConstructor`: Marks a given constructor - even a package protected one - to use when instantiating the object from the database. +* `@PersistenceCreator`: Marks a given constructor or a `static` factory method - even a package protected one - to use when instantiating the object from the database. Constructor arguments are mapped by name to the key values in the retrieved Document. * `@Value`: This annotation is part of the Spring Framework . Within the mapping framework it can be applied to constructor arguments. This lets you use a Spring Expression Language statement to transform a key's value retrieved in the database before it is used to construct a domain object. @@ -513,7 +513,7 @@ public class Person { this.ssn = ssn; } - @PersistenceConstructor + @PersistenceCreator public Person(Integer ssn, String firstName, String lastName, Integer age, T address) { this.ssn = ssn; this.firstName = firstName; @@ -673,7 +673,7 @@ Increased levels of nesting increase the complexity of the aggregation expressio [[mapping-custom-object-construction]] === Customized Object Construction -The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceConstructor` annotation. +The mapping subsystem allows the customization of the object construction by annotating a constructor with the `@PersistenceCreator` annotation. The values to be used for the constructor parameters are resolved in the following way: * If a parameter is annotated with the `@Value` annotation, the given expression is evaluated and the result is used as the parameter value. @@ -706,7 +706,7 @@ OrderItem item = converter.read(OrderItem.class, input); NOTE: The SpEL expression in the `@Value` annotation of the `quantity` parameter falls back to the value `0` if the given property path cannot be resolved. -Additional examples for using the `@PersistenceConstructor` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite. +Additional examples for using the `@PersistenceCreator` annotation can be found in the https://github.com/spring-projects/spring-data-mongodb/blob/master/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java[MappingMongoConverterUnitTests] test suite. [[mapping-usage-events]] === Mapping Framework Events diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc index a424748205..697af23a9e 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-query-operations.adoc @@ -342,7 +342,7 @@ public class Venue { private String name; private double[] location; - @PersistenceConstructor + @PersistenceCreator Venue(String name, double[] location) { super(); this.name = name; From 30f4d0f632ad0acea190cec5d589b83bef02ae55 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 8 Jan 2025 15:05:23 +0100 Subject: [PATCH 34/74] Remove commons-logging exclusion. Closes #4870 --- spring-data-mongodb/pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 6109e29130..36dcb50ade 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -67,12 +67,6 @@ org.springframework spring-core - - - commons-logging - commons-logging - - org.springframework From 94006ab67411bee0bbd691952cbd929634398bc1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:47:59 +0100 Subject: [PATCH 35/74] Prepare 5.0 M1 (2025.1.0). See #4836 --- pom.xml | 20 +++--------- src/main/resources/notice.txt | 57 +---------------------------------- 2 files changed, 5 insertions(+), 72 deletions(-) diff --git a/pom.xml b/pom.xml index 95fc8379d9..af61c7d12b 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M1 @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-SNAPSHOT + 4.0.0-M1 5.5.0 1.19 @@ -157,20 +157,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 3b6fc5c998..84a338e8d6 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 4.5 GA (2025.0.0) +Spring Data MongoDB 5.0 M1 (2025.1.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -8,58 +8,3 @@ This product may include a number of subcomponents with separate copyright notices and license terms. Your use of the source code for the these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - From 7348d8e80c8d4b9932392c429719d4d92611cc65 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:48:55 +0100 Subject: [PATCH 36/74] Release version 5.0 M1 (2025.1.0). See #4836 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index af61c7d12b..e9acadfe82 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M1 pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..b401fe9fbc 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M1 ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 36dcb50ade..23ad8dffab 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M1 ../pom.xml From 937ea9491dc24eca48d02a31329c8536dacee7ce Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:09 +0100 Subject: [PATCH 37/74] Prepare next development iteration. See #4836 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index e9acadfe82..af61c7d12b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M1 + 5.0.0-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index b401fe9fbc..fc88571622 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M1 + 5.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 23ad8dffab..36dcb50ade 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M1 + 5.0.0-SNAPSHOT ../pom.xml From 8418f65f3e5bd864c158fec14e1cf812fe51465d Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 24 Jan 2025 10:53:11 +0100 Subject: [PATCH 38/74] After release cleanups. See #4836 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index af61c7d12b..6c55b87172 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M1 + 4.0.0-SNAPSHOT @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-M1 + 4.0.0-SNAPSHOT 5.5.0 1.19 @@ -157,8 +157,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 0c5a908cf4b03d1c30309932e3890ee363070484 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 9 Apr 2025 11:08:54 +0200 Subject: [PATCH 39/74] Remove JMX support. Closes #4940 --- .../data/mongodb/config/MongoJmxParser.java | 78 -- .../mongodb/config/MongoNamespaceHandler.java | 1 - .../data/mongodb/monitor/AbstractMonitor.java | 66 -- .../data/mongodb/monitor/AssertMetrics.java | 74 -- .../monitor/BackgroundFlushingMetrics.java | 82 -- .../mongodb/monitor/BtreeIndexCounters.java | 81 -- .../mongodb/monitor/ConnectionMetrics.java | 60 -- .../mongodb/monitor/GlobalLockMetrics.java | 85 -- .../data/mongodb/monitor/MemoryMetrics.java | 75 -- .../mongodb/monitor/OperationCounters.java | 78 -- .../data/mongodb/monitor/ServerInfo.java | 83 -- .../data/mongodb/monitor/package-info.java | 7 - .../main/resources/META-INF/spring.schemas | 6 +- .../data/mongodb/config/spring-mongo-5.0.xsd | 935 ++++++++++++++++++ .../data/mongodb/core/JmxServer.java | 38 - .../monitor/MongoMonitorIntegrationTests.java | 68 -- .../data/mongodb/monitor/Resumeable.java | 27 - .../src/test/resources/server-jmx.xml | 23 - 18 files changed, 939 insertions(+), 928 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java create mode 100644 spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java delete mode 100644 spring-data-mongodb/src/test/resources/server-jmx.xml diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java deleted file mode 100644 index af1ffbbb02..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2011-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.config; - -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.parsing.BeanComponentDefinition; -import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.xml.BeanDefinitionParser; -import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.data.mongodb.core.MongoAdmin; -import org.springframework.data.mongodb.monitor.*; -import org.springframework.util.StringUtils; -import org.w3c.dom.Element; - -/** - * @author Mark Pollack - * @author Thomas Risberg - * @author John Brisbin - * @author Oliver Gierke - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -public class MongoJmxParser implements BeanDefinitionParser { - - public BeanDefinition parse(Element element, ParserContext parserContext) { - String name = element.getAttribute("mongo-ref"); - if (!StringUtils.hasText(name)) { - name = BeanNames.MONGO_BEAN_NAME; - } - registerJmxComponents(name, element, parserContext); - return null; - } - - protected void registerJmxComponents(String mongoRefName, Element element, ParserContext parserContext) { - Object eleSource = parserContext.extractSource(element); - - CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource); - - createBeanDefEntry(AssertMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(BackgroundFlushingMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(BtreeIndexCounters.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(ConnectionMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(GlobalLockMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(MemoryMetrics.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(OperationCounters.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(ServerInfo.class, compositeDef, mongoRefName, eleSource, parserContext); - createBeanDefEntry(MongoAdmin.class, compositeDef, mongoRefName, eleSource, parserContext); - - parserContext.registerComponent(compositeDef); - - } - - protected void createBeanDefEntry(Class clazz, CompositeComponentDefinition compositeDef, String mongoRefName, - Object eleSource, ParserContext parserContext) { - BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz); - builder.getRawBeanDefinition().setSource(eleSource); - builder.addConstructorArgReference(mongoRefName); - BeanDefinition assertDef = builder.getBeanDefinition(); - String assertName = parserContext.getReaderContext().registerWithGeneratedName(assertDef); - compositeDef.addNestedComponent(new BeanComponentDefinition(assertDef, assertName)); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java index 47519ca615..62a4a1082d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java @@ -31,7 +31,6 @@ public void init() { registerBeanDefinitionParser("mapping-converter", new MappingMongoConverterParser()); registerBeanDefinitionParser("mongo-client", new MongoClientParser()); registerBeanDefinitionParser("db-factory", new MongoDbFactoryParser()); - registerBeanDefinitionParser("jmx", new MongoJmxParser()); registerBeanDefinitionParser("auditing", new MongoAuditingBeanDefinitionParser()); registerBeanDefinitionParser("template", new MongoTemplateParser()); registerBeanDefinitionParser("gridFsTemplate", new GridFsTemplateParser()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java deleted file mode 100644 index 5ffe37a4a7..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AbstractMonitor.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import java.util.List; -import java.util.stream.Collectors; - -import org.bson.Document; - -import com.mongodb.ServerAddress; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoDatabase; -import com.mongodb.connection.ServerDescription; - -/** - * Base class to encapsulate common configuration settings when connecting to a database - * - * @author Mark Pollack - * @author Oliver Gierke - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -public abstract class AbstractMonitor { - - private final MongoClient mongoClient; - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - protected AbstractMonitor(MongoClient mongoClient) { - this.mongoClient = mongoClient; - } - - public Document getServerStatus() { - return getDb("admin").runCommand(new Document("serverStatus", 1).append("rangeDeleter", 1).append("repl", 1)); - } - - public MongoDatabase getDb(String databaseName) { - return mongoClient.getDatabase(databaseName); - } - - protected MongoClient getMongoClient() { - return mongoClient; - } - - protected List hosts() { - - return mongoClient.getClusterDescription().getServerDescriptions().stream().map(ServerDescription::getAddress) - .collect(Collectors.toList()); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java deleted file mode 100644 index 15666fa4d0..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/AssertMetrics.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for assertions - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Assertion Metrics") -public class AssertMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public AssertMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Regular") - public int getRegular() { - return getBtree("regular"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Warning") - public int getWarning() { - return getBtree("warning"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Msg") - public int getMsg() { - return getBtree("msg"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "User") - public int getUser() { - return getBtree("user"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Rollovers") - public int getRollovers() { - return getBtree("rollovers"); - } - - private int getBtree(String key) { - Document asserts = (Document) getServerStatus().get("asserts"); - // Class c = btree.get(key).getClass(); - return (Integer) asserts.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java deleted file mode 100644 index 2ceb75a4f8..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BackgroundFlushingMetrics.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import java.util.Date; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Background Flushing - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Background Flushing Metrics") -public class BackgroundFlushingMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public BackgroundFlushingMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Flushes") - public int getFlushes() { - return getFlushingData("flushes", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total ms", unit = "ms") - public int getTotalMs() { - return getFlushingData("total_ms", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Average ms", unit = "ms") - public double getAverageMs() { - return getFlushingData("average_ms", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last Ms", unit = "ms") - public int getLastMs() { - return getFlushingData("last_ms", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Last finished") - public Date getLastFinished() { - return getLast(); - } - - @SuppressWarnings("unchecked") - private T getFlushingData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("backgroundFlushing"); - return (T) mem.get(key); - } - - private Date getLast() { - Document bgFlush = (Document) getServerStatus().get("backgroundFlushing"); - Date lastFinished = (Date) bgFlush.get("last_finished"); - return lastFinished; - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java deleted file mode 100644 index 671d017e05..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/BtreeIndexCounters.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for B-tree index counters - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Btree Metrics") -public class BtreeIndexCounters extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public BtreeIndexCounters(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Accesses") - public int getAccesses() { - return getBtree("accesses"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Hits") - public int getHits() { - return getBtree("hits"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Misses") - public int getMisses() { - return getBtree("misses"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resets") - public int getResets() { - return getBtree("resets"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Miss Ratio") - public int getMissRatio() { - return getBtree("missRatio"); - } - - private int getBtree(String key) { - Document indexCounters = (Document) getServerStatus().get("indexCounters"); - if (indexCounters.get("note") != null) { - String message = (String) indexCounters.get("note"); - if (message.contains("not supported")) { - return -1; - } - } - Document btree = (Document) indexCounters.get("btree"); - // Class c = btree.get(key).getClass(); - return (Integer) btree.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java deleted file mode 100644 index 0d0eb84b35..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ConnectionMetrics.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Connections - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Connection metrics") -public class ConnectionMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public ConnectionMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Connections") - public int getCurrent() { - return getConnectionData("current", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Available Connections") - public int getAvailable() { - return getConnectionData("available", java.lang.Integer.class); - } - - @SuppressWarnings("unchecked") - private T getConnectionData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("connections"); - // Class c = mem.get(key).getClass(); - return (T) mem.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java deleted file mode 100644 index 6997f5fba8..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/GlobalLockMetrics.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.DBObject; -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Global Locks - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Global Lock Metrics") -public class GlobalLockMetrics extends AbstractMonitor { - - /** - * @param mongoClient must not be {@literal null}. - * @since 2.2 - */ - public GlobalLockMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Total time") - public double getTotalTime() { - return getGlobalLockData("totalTime", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Lock time", unit = "s") - public double getLockTime() { - return getGlobalLockData("lockTime", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Lock time") - public double getLockTimeRatio() { - return getGlobalLockData("ratio", java.lang.Double.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Current Queue") - public int getCurrentQueueTotal() { - return getCurrentQueue("total"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Reader Queue") - public int getCurrentQueueReaders() { - return getCurrentQueue("readers"); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Writer Queue") - public int getCurrentQueueWriters() { - return getCurrentQueue("writers"); - } - - @SuppressWarnings("unchecked") - private T getGlobalLockData(String key, Class targetClass) { - DBObject globalLock = (DBObject) getServerStatus().get("globalLock"); - return (T) globalLock.get(key); - } - - private int getCurrentQueue(String key) { - Document globalLock = (Document) getServerStatus().get("globalLock"); - Document currentQueue = (Document) globalLock.get("currentQueue"); - return (Integer) currentQueue.get(key); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java deleted file mode 100644 index 4dbdebb26f..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/MemoryMetrics.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Memory - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Memory Metrics") -public class MemoryMetrics extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - public MemoryMetrics(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Memory address size") - public int getBits() { - return getMemData("bits", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Resident in Physical Memory", unit = "MB") - public int getResidentSpace() { - return getMemData("resident", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Virtual Address Space", unit = "MB") - public int getVirtualAddressSpace() { - return getMemData("virtual", java.lang.Integer.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Is memory info supported on this platform") - public boolean getMemoryInfoSupported() { - return getMemData("supported", java.lang.Boolean.class); - } - - @ManagedMetric(metricType = MetricType.GAUGE, displayName = "Memory Mapped Space", unit = "MB") - public int getMemoryMappedSpace() { - return getMemData("mapped", java.lang.Integer.class); - } - - @SuppressWarnings("unchecked") - private T getMemData(String key, Class targetClass) { - Document mem = (Document) getServerStatus().get("mem"); - // Class c = mem.get(key).getClass(); - return (T) mem.get(key); - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java deleted file mode 100644 index 1624501490..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/OperationCounters.java +++ /dev/null @@ -1,78 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import org.bson.Document; -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; -import org.springframework.util.NumberUtils; - -import com.mongodb.client.MongoClient; - -/** - * JMX Metrics for Operation counters - * - * @author Mark Pollack - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Operation Counters") -public class OperationCounters extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - public OperationCounters(MongoClient mongoClient) { - super(mongoClient); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Insert operation count") - public int getInsertCount() { - return getOpCounter("insert"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Query operation count") - public int getQueryCount() { - return getOpCounter("query"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Update operation count") - public int getUpdateCount() { - return getOpCounter("update"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Delete operation count") - public int getDeleteCount() { - return getOpCounter("delete"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "GetMore operation count") - public int getGetMoreCount() { - return getOpCounter("getmore"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Command operation count") - public int getCommandCount() { - return getOpCounter("command"); - } - - private int getOpCounter(String key) { - Document opCounters = (Document) getServerStatus().get("opcounters"); - return NumberUtils.convertNumberToTargetClass((Number) opCounters.get(key), Integer.class); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java deleted file mode 100644 index 3aedf3f29f..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/ServerInfo.java +++ /dev/null @@ -1,83 +0,0 @@ -/* - * Copyright 2012-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import java.net.UnknownHostException; - -import org.springframework.jmx.export.annotation.ManagedMetric; -import org.springframework.jmx.export.annotation.ManagedOperation; -import org.springframework.jmx.export.annotation.ManagedResource; -import org.springframework.jmx.support.MetricType; -import org.springframework.util.StringUtils; - -import com.mongodb.client.MongoClient; - -/** - * Expose basic server information via JMX - * - * @author Mark Pollack - * @author Thomas Darimont - * @author Christoph Strobl - * @deprecated since 4.5 - */ -@Deprecated(since = "4.5", forRemoval = true) -@ManagedResource(description = "Server Information") -public class ServerInfo extends AbstractMonitor { - - /** - * @param mongoClient - * @since 2.2 - */ - protected ServerInfo(MongoClient mongoClient) { - super(mongoClient); - } - - /** - * Returns the hostname of the used server reported by MongoDB. - * - * @return the reported hostname can also be an IP address. - * @throws UnknownHostException - */ - @ManagedOperation(description = "Server host name") - public String getHostName() throws UnknownHostException { - - /* - * UnknownHostException is not necessary anymore, but clients could have - * called this method in a try..catch(UnknownHostException) already - */ - return StringUtils.collectionToDelimitedString(hosts(), ","); - } - - @ManagedMetric(displayName = "Uptime Estimate") - public double getUptimeEstimate() { - return (Double) getServerStatus().get("uptimeEstimate"); - } - - @ManagedOperation(description = "MongoDB Server Version") - public String getVersion() { - return (String) getServerStatus().get("version"); - } - - @ManagedOperation(description = "Local Time") - public String getLocalTime() { - return (String) getServerStatus().get("localTime"); - } - - @ManagedMetric(metricType = MetricType.COUNTER, displayName = "Server uptime in seconds", unit = "seconds") - public double getUptime() { - return (Double) getServerStatus().get("uptime"); - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java deleted file mode 100644 index 1e1c221b64..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java +++ /dev/null @@ -1,7 +0,0 @@ -/** - * MongoDB specific JMX monitoring support. - */ -@Deprecated(since = "4.5", forRemoval = true) -@org.springframework.lang.NonNullApi -package org.springframework.data.mongodb.monitor; - diff --git a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas index 57920f7449..c6c28dbab1 100644 --- a/spring-data-mongodb/src/main/resources/META-INF/spring.schemas +++ b/spring-data-mongodb/src/main/resources/META-INF/spring.schemas @@ -13,7 +13,8 @@ http\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/sprin http\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd http\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd -http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd +http\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.0.xsd=org/springframework/data/mongodb/config/spring-mongo-1.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.1.xsd=org/springframework/data/mongodb/config/spring-mongo-1.1.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-1.2.xsd=org/springframework/data/mongodb/config/spring-mongo-1.2.xsd @@ -29,4 +30,5 @@ https\://www.springframework.org/schema/data/mongo/spring-mongo-2.2.xsd=org/spri https\://www.springframework.org/schema/data/mongo/spring-mongo-3.0.xsd=org/springframework/data/mongodb/config/spring-mongo-3.0.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-3.3.xsd=org/springframework/data/mongodb/config/spring-mongo-3.3.xsd https\://www.springframework.org/schema/data/mongo/spring-mongo-4.0.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd -https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-4.0.xsd +https\://www.springframework.org/schema/data/mongo/spring-mongo-5.0.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd +https\://www.springframework.org/schema/data/mongo/spring-mongo.xsd=org/springframework/data/mongodb/config/spring-mongo-5.0.xsd diff --git a/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd new file mode 100644 index 0000000000..5fae630b6b --- /dev/null +++ b/spring-data-mongodb/src/main/resources/org/springframework/data/mongodb/config/spring-mongo-5.0.xsd @@ -0,0 +1,935 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The WriteConcern that will be the default value used when asking + the MongoDatabaseFactory for a DB object + + + + + + + + + + + + + + The reference to a MongoTemplate. Will default to 'mongoTemplate'. + + + + + + + Enables creation of indexes for queries that get derived from the + method name + and thus reference domain class properties. Defaults to false. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + The reference to a MongoTypeMapper to be used by this + MappingMongoConverter. + + + + + + + The reference to a MappingContext. Will default to + 'mappingContext'. + + + + + + + Disables JSR-303 validation on MongoDB documents before they are + saved. By default it is set to false. + + + + + + + + + + Enables abbreviating the field names for domain class properties + to the + first character of their camel case names, e.g. fooBar -> fb. + Defaults to false. + + + + + + + + + + The reference to a FieldNamingStrategy. + + + + + + + Enable/Disable index creation for annotated properties/entities. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + A reference to a custom converter. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + The WriteConcern that will be the default value used when asking + the MongoDatabaseFactory for a DB object + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + The reference to a MongoDatabaseFactory. + + + + + + + + + + + + + + + + diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java deleted file mode 100644 index 004bda1544..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/JmxServer.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.core; - -import org.springframework.context.support.ClassPathXmlApplicationContext; - -/** - * Server application than can be run as an app or unit test. - * - * @author Mark Pollack - * @author Oliver Gierke - * @deprecated since 4.5. - */ -@Deprecated(since = "4.5", forRemoval = true) -public class JmxServer { - - public static void main(String[] args) { - new JmxServer().run(); - } - - @SuppressWarnings("resource") - public void run() { - new ClassPathXmlApplicationContext(new String[] { "infrastructure.xml", "server-jmx.xml" }); - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java deleted file mode 100644 index e70b398f7f..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/MongoMonitorIntegrationTests.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2002-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import static org.assertj.core.api.Assertions.*; - -import java.net.UnknownHostException; - -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.data.mongodb.test.util.Client; -import org.springframework.data.mongodb.test.util.MongoClientExtension; - -import com.mongodb.client.MongoClient; - -/** - * This test class assumes that you are already running the MongoDB server. - * - * @author Mark Pollack - * @author Thomas Darimont - * @author Mark Paluch - */ -@ExtendWith(MongoClientExtension.class) -public class MongoMonitorIntegrationTests { - - static @Client MongoClient mongoClient; - - @Test - public void serverInfo() { - ServerInfo serverInfo = new ServerInfo(mongoClient); - serverInfo.getVersion(); - } - - @Test // DATAMONGO-685 - public void getHostNameShouldReturnServerNameReportedByMongo() throws UnknownHostException { - - ServerInfo serverInfo = new ServerInfo(mongoClient); - - String hostName = null; - try { - hostName = serverInfo.getHostName(); - } catch (UnknownHostException e) { - throw e; - } - - assertThat(hostName).isNotNull(); - assertThat(hostName).isEqualTo("127.0.0.1:27017"); - } - - @Test - public void operationCounters() { - OperationCounters operationCounters = new OperationCounters(mongoClient); - operationCounters.getInsertCount(); - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java deleted file mode 100644 index 1fdbb1f188..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/monitor/Resumeable.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2018-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.monitor; - -import java.util.function.Supplier; - -/** - * @author Christoph Strobl - * @since 2018/01 - */ -interface Resumeable { - - void resumeAt(Supplier token); -} diff --git a/spring-data-mongodb/src/test/resources/server-jmx.xml b/spring-data-mongodb/src/test/resources/server-jmx.xml deleted file mode 100644 index 54f985f4cb..0000000000 --- a/spring-data-mongodb/src/test/resources/server-jmx.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file From 6531bc792bb6518587c8e2dc37938887f60c6738 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 14 Apr 2025 11:32:34 +0200 Subject: [PATCH 40/74] Remove MongoDB driver 4 compatibility. Closes: #4886 --- .../data/mongodb/core/IndexConverters.java | 5 - .../core/MongoClientSettingsFactoryBean.java | 18 - .../data/mongodb/core/MongoTemplate.java | 10 +- .../mongodb/core/ReactiveMongoTemplate.java | 9 +- .../encryption/MongoClientEncryption.java | 4 +- .../mongodb/core/index/GeoSpatialIndexed.java | 10 - .../mongodb/core/index/GeospatialIndex.java | 18 - .../MongoPersistentEntityIndexResolver.java | 19 - .../core/mapreduce/MapReduceOptions.java | 19 - ...aultMongoHandlerObservationConvention.java | 13 - .../util/MongoCompatibilityAdapter.java | 417 ------------------ ...iveSessionBoundMongoTemplateUnitTests.java | 4 +- .../SessionBoundMongoTemplateUnitTests.java | 3 +- ...ersistentEntityIndexResolverUnitTests.java | 6 +- .../mongodb/core/query/IndexUnitTests.java | 4 +- .../mongodb/test/util/CleanMongoDBTests.java | 6 +- .../mongodb/test/util/MongoTestTemplate.java | 3 +- .../MongoCompatibilityAdapterUnitTests.java | 49 -- 18 files changed, 15 insertions(+), 602 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index f5856100d0..2008b85f60 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -18,11 +18,9 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -90,9 +88,6 @@ private static Converter getIndexDefinitionIndexO if (indexOptions.containsKey("bits")) { ops = ops.bits((Integer) indexOptions.get("bits")); } - if (indexOptions.containsKey("bucketSize")) { - MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); - } if (indexOptions.containsKey("default_language")) { ops = ops.defaultLanguage(indexOptions.get("default_language").toString()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 02913b4303..4400db1ea7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -25,9 +25,7 @@ import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; - import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -57,8 +55,6 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean { - for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { + for (String name : db.listCollectionNames()) { if (name.equals(collectionName)) { return true; } @@ -1980,11 +1979,6 @@ public List mapReduce(Query query, Class domainType, String inputColle mapReduce = mapReduce.jsMode(mapReduceOptions.getJavaScriptMode()); } - if (mapReduceOptions.getOutputSharded().isPresent()) { - MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce) - .sharded(mapReduceOptions.getOutputSharded().get()); - } - if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) { mapReduce = mapReduce.collectionName(mapReduceOptions.getOutputCollection()) @@ -2367,7 +2361,7 @@ protected String replaceWithResourceIfNecessary(String function) { public Set getCollectionNames() { return execute(db -> { Set result = new LinkedHashSet<>(); - for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { + for (String name : db.listCollectionNames()) { result.add(name); } return result; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index b74ec6aa1c..9b5af77f65 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -118,7 +118,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Optionals; import org.springframework.lang.Nullable; @@ -739,7 +738,7 @@ public Mono collectionExists(Class entityClass) { @Override public Mono collectionExists(String collectionName) { - return createMono(db -> Flux.from(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()) // + return createMono(db -> Flux.from(db.listCollectionNames()) // .filter(s -> s.equals(collectionName)) // .map(s -> true) // .single(false)); @@ -787,7 +786,7 @@ public ReactiveBulkOperations bulkOps(BulkMode mode, @Nullable Class entityTy @Override public Flux getCollectionNames() { - return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()); + return createFlux(db -> db.listCollectionNames()); } public Mono getMongoDatabase() { @@ -2176,10 +2175,6 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu publisher = publisher.jsMode(options.getJavaScriptMode()); } - if (options.getOutputSharded().isPresent()) { - MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get()); - } - if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) { publisher = publisher.collectionName(options.getOutputCollection()).action(options.getMapReduceAction()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java index f83f98d4ac..aee5dd7f8b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/MongoClientEncryption.java @@ -15,8 +15,6 @@ */ package org.springframework.data.mongodb.core.encryption; -import static org.springframework.data.mongodb.util.MongoCompatibilityAdapter.rangeOptionsAdapter; - import java.util.Map; import java.util.function.Supplier; @@ -124,7 +122,7 @@ protected RangeOptions rangeOptions(Map attributes) { Assert.isInstanceOf(Integer.class, trimFactor, () -> String .format("Expected to find a %s but it turned out to be %s.", Integer.class, trimFactor.getClass())); - rangeOptionsAdapter(encryptionRangeOptions).trimFactor((Integer) trimFactor); + encryptionRangeOptions.trimFactor((Integer) trimFactor); } if (attributes.containsKey("sparsity")) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java index 3fb797559b..a39da5c946 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeoSpatialIndexed.java @@ -114,16 +114,6 @@ */ GeoSpatialIndexType type() default GeoSpatialIndexType.GEO_2D; - /** - * The bucket size for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes, in coordinate units. - * - * @since 1.4 - * @return {@literal 1.0} by default. - * @deprecated since MongoDB server version 4.4 - */ - @Deprecated - double bucketSize() default 1.0; - /** * The name of the additional field to use for {@link GeoSpatialIndexType#GEO_HAYSTACK} indexes * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 0949506195..1c5a8ca6bc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -19,7 +19,6 @@ import org.bson.Document; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,7 +40,6 @@ public class GeospatialIndex implements IndexDefinition { private @Nullable Integer max; private @Nullable Integer bits; private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; - private Double bucketSize = MongoClientVersion.isVersion5orNewer() ? null : 1.0; private @Nullable String additionalField; private Optional filter = Optional.empty(); private Optional collation = Optional.empty(); @@ -107,17 +105,6 @@ public GeospatialIndex typed(GeoSpatialIndexType type) { return this; } - /** - * @param bucketSize - * @return this. - * @deprecated since MongoDB server version 4.4 - */ - @Deprecated - public GeospatialIndex withBucketSize(double bucketSize) { - this.bucketSize = bucketSize; - return this; - } - /** * @param fieldName * @return this. @@ -203,14 +190,9 @@ public Document getIndexOptions() { break; case GEO_2DSPHERE: - break; case GEO_HAYSTACK: - - if (bucketSize != null) { - document.put("bucketSize", bucketSize); - } break; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index a5988b8c1d..37008ad76e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -25,7 +25,6 @@ import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.Supplier; @@ -55,7 +54,6 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.mongodb.util.DurationUtil; -import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.util.TypeInformation; @@ -711,23 +709,6 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, .named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty)); } - if (MongoClientVersion.isVersion5orNewer()) { - - Optional defaultBucketSize = MergedAnnotation.of(GeoSpatialIndexed.class).getDefaultValue("bucketSize", - Double.class); - if (!defaultBucketSize.isPresent() || index.bucketSize() != defaultBucketSize.get()) { - indexDefinition.withBucketSize(index.bucketSize()); - } else { - if (LOGGER.isInfoEnabled()) { - LOGGER.info( - "GeoSpatialIndexed.bucketSize no longer supported by Mongo Client 5 or newer. Ignoring bucketSize for path %s." - .formatted(dotPath)); - } - } - } else { - indexDefinition.withBucketSize(index.bucketSize()); - } - indexDefinition.typed(index.type()).withAdditionalField(index.additionalField()); return new IndexDefinitionHolder(dotPath, indexDefinition, collection); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java index 9f34ec44e4..e9ee146be6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java @@ -45,7 +45,6 @@ public class MapReduceOptions { private Boolean verbose = Boolean.TRUE; private @Nullable Integer limit; - private Optional outputSharded = Optional.empty(); private Optional finalizeFunction = Optional.empty(); private Optional collation = Optional.empty(); @@ -152,19 +151,6 @@ public MapReduceOptions actionReplace() { return this; } - /** - * If true and combined with an output mode that writes to a collection, the output collection will be sharded using - * the _id field. For MongoDB 1.9+ - * - * @param outputShared if true, output will be sharded based on _id key. - * @return MapReduceOptions so that methods can be chained in a fluent API style - */ - public MapReduceOptions outputSharded(boolean outputShared) { - - this.outputSharded = Optional.of(outputShared); - return this; - } - /** * Sets the finalize function * @@ -245,10 +231,6 @@ public Optional getOutputDatabase() { return this.outputDatabase; } - public Optional getOutputSharded() { - return this.outputSharded; - } - public Map getScopeVariables() { return this.scopeVariables; } @@ -336,7 +318,6 @@ protected Document createOutObject() { } outputDatabase.ifPresent(val -> out.append("db", val)); - outputSharded.ifPresent(val -> out.append("sharded", val)); return out; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index b823ce223b..8c7c6a55c4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -17,10 +17,7 @@ import io.micrometer.common.KeyValues; -import java.net.InetSocketAddress; - import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -78,16 +75,6 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { keyValues = keyValues.and(LowCardinalityCommandKeyNames.NET_TRANSPORT.withValue("IP.TCP"), LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - - InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress) - .getSocketAddress(); - - if (socketAddress != null) { - - keyValues = keyValues.and( - LowCardinalityCommandKeyNames.NET_SOCK_PEER_ADDR.withValue(socketAddress.getHostName()), - LowCardinalityCommandKeyNames.NET_SOCK_PEER_PORT.withValue("" + socketAddress.getPort())); - } } ConnectionId connectionId = connectionDescription.getConnectionId(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java deleted file mode 100644 index 8bd422c493..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java +++ /dev/null @@ -1,417 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.util; - -import java.lang.reflect.Method; -import java.net.InetSocketAddress; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; - -import com.mongodb.MongoClientSettings; -import com.mongodb.MongoClientSettings.Builder; -import com.mongodb.ServerAddress; -import com.mongodb.client.ClientSession; -import com.mongodb.client.MapReduceIterable; -import com.mongodb.client.MongoDatabase; -import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.IndexOptions; -import com.mongodb.client.model.vault.RangeOptions; -import com.mongodb.reactivestreams.client.MapReducePublisher; - -/** - * Compatibility adapter to bridge functionality across different MongoDB driver versions. - *

- * This class is for internal use within the framework and should not be used by applications. - * - * @author Christoph Strobl - * @author Ross Lawley - * @since 4.3 - */ -public class MongoCompatibilityAdapter { - - private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5 or newer"; - private static final String NOT_SUPPORTED_ON_4 = "%s is not supported on Mongo Client 4"; - - private static final @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, - "getStreamFactoryFactory"); - - private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", - Double.class); - - private static final @Nullable Method setTrimFactor; - - static { - - // method name changed in between - Method trimFactor = ReflectionUtils.findMethod(RangeOptions.class, "setTrimFactor", Integer.class); - if (trimFactor != null) { - setTrimFactor = trimFactor; - } else { - setTrimFactor = ReflectionUtils.findMethod(RangeOptions.class, "trimFactor", Integer.class); - } - } - - /** - * Return a compatibility adapter for {@link MongoClientSettings.Builder}. - * - * @param builder - * @return - */ - public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) { - return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory; - } - - /** - * Return a compatibility adapter for {@link MongoClientSettings}. - * - * @param clientSettings - * @return - */ - public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) { - return new ClientSettingsAdapter() { - @Override - public T getStreamFactoryFactory() { - - if (MongoClientVersion.isVersion5orNewer() || getStreamFactoryFactory == null) { - return null; - } - - return (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings); - } - }; - } - - /** - * Return a compatibility adapter for {@link IndexOptions}. - * - * @param options - * @return - */ - public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { - return bucketSize -> { - - if (MongoClientVersion.isVersion5orNewer() || setBucketSize == null) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); - } - - ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); - }; - } - - /** - * Return a compatibility adapter for {@code MapReduceIterable}. - * - * @param iterable - * @return - */ - @SuppressWarnings("deprecation") - public static MapReduceIterableAdapter mapReduceIterableAdapter(Object iterable) { - return sharded -> { - - if (MongoClientVersion.isVersion5orNewer()) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); - } - - // Use MapReduceIterable to avoid package-protected access violations to - // com.mongodb.client.internal.MapReduceIterableImpl - Method shardedMethod = ReflectionUtils.findMethod(MapReduceIterable.class, "sharded", boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, iterable, sharded); - }; - } - - /** - * Return a compatibility adapter for {@link RangeOptions}. - * - * @param options - * @return - */ - public static RangeOptionsAdapter rangeOptionsAdapter(RangeOptions options) { - return trimFactor -> { - - if (!MongoClientVersion.isVersion5orNewer() || setTrimFactor == null) { - throw new UnsupportedOperationException(NOT_SUPPORTED_ON_4.formatted("RangeOptions.trimFactor")); - } - - ReflectionUtils.invokeMethod(setTrimFactor, options, trimFactor); - }; - } - - /** - * Return a compatibility adapter for {@code MapReducePublisher}. - * - * @param publisher - * @return - */ - @SuppressWarnings("deprecation") - public static MapReducePublisherAdapter mapReducePublisherAdapter(Object publisher) { - return sharded -> { - - if (MongoClientVersion.isVersion5orNewer()) { - throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); - } - - // Use MapReducePublisher to avoid package-protected access violations to MapReducePublisherImpl - Method shardedMethod = ReflectionUtils.findMethod(MapReducePublisher.class, "sharded", boolean.class); - ReflectionUtils.invokeMethod(shardedMethod, publisher, sharded); - }; - } - - /** - * Return a compatibility adapter for {@link ServerAddress}. - * - * @param serverAddress - * @return - */ - public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) { - return () -> { - - if (MongoClientVersion.isVersion5orNewer()) { - return null; - } - - Method serverAddressMethod = ReflectionUtils.findMethod(ServerAddress.class, "getSocketAddress"); - Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); - return value != null ? InetSocketAddress.class.cast(value) : null; - }; - } - - public static MongoDatabaseAdapterBuilder mongoDatabaseAdapter() { - return MongoDatabaseAdapter::new; - } - - public static ReactiveMongoDatabaseAdapterBuilder reactiveMongoDatabaseAdapter() { - return ReactiveMongoDatabaseAdapter::new; - } - - public interface IndexOptionsAdapter { - void setBucketSize(double bucketSize); - } - - public interface ClientSettingsAdapter { - @Nullable - T getStreamFactoryFactory(); - } - - public interface ClientSettingsBuilderAdapter { - void setStreamFactoryFactory(T streamFactory); - } - - public interface MapReduceIterableAdapter { - void sharded(boolean sharded); - } - - public interface MapReducePublisherAdapter { - void sharded(boolean sharded); - } - - public interface ServerAddressAdapter { - @Nullable - InetSocketAddress getSocketAddress(); - } - - public interface MongoDatabaseAdapterBuilder { - MongoDatabaseAdapter forDb(com.mongodb.client.MongoDatabase db); - } - - public interface RangeOptionsAdapter { - void trimFactor(Integer trimFactor); - } - - @SuppressWarnings({ "unchecked", "DataFlowIssue" }) - public static class MongoDatabaseAdapter { - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD; - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; - - private static final Class collectionNamesReturnType; - - private final MongoDatabase db; - - static { - - if (MongoClientVersion.isSyncClientPresent()) { - - LIST_COLLECTION_NAMES_METHOD = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames"); - LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod(MongoDatabase.class, "listCollectionNames", - ClientSession.class); - - if (MongoClientVersion.isVersion5orNewer()) { - try { - collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.ListCollectionNamesIterable", - MongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); - } - } else { - try { - collectionNamesReturnType = ClassUtils.forName("com.mongodb.client.MongoIterable", - MongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("Unable to load com.mongodb.client.ListCollectionNamesIterable", e); - } - } - } else { - LIST_COLLECTION_NAMES_METHOD = null; - LIST_COLLECTION_NAMES_METHOD_SESSION = null; - collectionNamesReturnType = Object.class; - } - } - - public MongoDatabaseAdapter(MongoDatabase db) { - this.db = db; - } - - public Class> collectionNameIterableType() { - return (Class>) collectionNamesReturnType; - } - - public MongoIterable listCollectionNames() { - - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); - return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); - } - - public MongoIterable listCollectionNames(ClientSession clientSession) { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, - "No method listCollectionNames(ClientSession) present for %s".formatted(db)); - return (MongoIterable) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, - clientSession); - } - } - - public interface ReactiveMongoDatabaseAdapterBuilder { - ReactiveMongoDatabaseAdapter forDb(com.mongodb.reactivestreams.client.MongoDatabase db); - } - - @SuppressWarnings({ "unchecked", "DataFlowIssue" }) - public static class ReactiveMongoDatabaseAdapter { - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD; - - @Nullable // - private static final Method LIST_COLLECTION_NAMES_METHOD_SESSION; - - private static final Class collectionNamesReturnType; - - private final com.mongodb.reactivestreams.client.MongoDatabase db; - - static { - - if (MongoClientVersion.isReactiveClientPresent()) { - - LIST_COLLECTION_NAMES_METHOD = ReflectionUtils - .findMethod(com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames"); - LIST_COLLECTION_NAMES_METHOD_SESSION = ReflectionUtils.findMethod( - com.mongodb.reactivestreams.client.MongoDatabase.class, "listCollectionNames", - com.mongodb.reactivestreams.client.ClientSession.class); - - if (MongoClientVersion.isVersion5orNewer()) { - try { - collectionNamesReturnType = ClassUtils.forName( - "com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", - ReactiveMongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("com.mongodb.reactivestreams.client.ListCollectionNamesPublisher", e); - } - } else { - try { - collectionNamesReturnType = ClassUtils.forName("org.reactivestreams.Publisher", - ReactiveMongoDatabaseAdapter.class.getClassLoader()); - } catch (ClassNotFoundException e) { - throw new IllegalStateException("org.reactivestreams.Publisher", e); - } - } - } else { - LIST_COLLECTION_NAMES_METHOD = null; - LIST_COLLECTION_NAMES_METHOD_SESSION = null; - collectionNamesReturnType = Object.class; - } - } - - ReactiveMongoDatabaseAdapter(com.mongodb.reactivestreams.client.MongoDatabase db) { - this.db = db; - } - - public Class> collectionNamePublisherType() { - return (Class>) collectionNamesReturnType; - - } - - public Publisher listCollectionNames() { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db)); - return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db); - } - - public Publisher listCollectionNames(com.mongodb.reactivestreams.client.ClientSession clientSession) { - Assert.state(LIST_COLLECTION_NAMES_METHOD != null, - "No method listCollectionNames(ClientSession) present for %s".formatted(db)); - return (Publisher) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD_SESSION, db, clientSession); - } - } - - static class MongoStreamFactoryFactorySettingsConfigurer { - - private static final Log logger = LogFactory.getLog(MongoStreamFactoryFactorySettingsConfigurer.class); - - private static final String STREAM_FACTORY_NAME = "com.mongodb.connection.StreamFactoryFactory"; - private static final boolean STREAM_FACTORY_PRESENT = ClassUtils.isPresent(STREAM_FACTORY_NAME, - MongoCompatibilityAdapter.class.getClassLoader()); - private final MongoClientSettings.Builder settingsBuilder; - - static boolean isStreamFactoryPresent() { - return STREAM_FACTORY_PRESENT; - } - - public MongoStreamFactoryFactorySettingsConfigurer(Builder settingsBuilder) { - this.settingsBuilder = settingsBuilder; - } - - void setStreamFactory(Object streamFactory) { - - if (MongoClientVersion.isVersion5orNewer() && isStreamFactoryPresent()) { - logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead."); - return; - } - - try { - Class streamFactoryType = ClassUtils.forName(STREAM_FACTORY_NAME, streamFactory.getClass().getClassLoader()); - - if (!ClassUtils.isAssignable(streamFactoryType, streamFactory.getClass())) { - throw new IllegalArgumentException("Expected %s but found %s".formatted(streamFactoryType, streamFactory)); - } - - Method setter = ReflectionUtils.findMethod(settingsBuilder.getClass(), "streamFactoryFactory", - streamFactoryType); - if (setter != null) { - ReflectionUtils.invokeMethod(setter, settingsBuilder, streamFactoryType.cast(streamFactory)); - } - } catch (ReflectiveOperationException e) { - throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); - } - } - } - -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 73970d2ad3..02637e9971 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Proxy; +import com.mongodb.reactivestreams.client.ListCollectionNamesPublisher; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -58,7 +59,6 @@ import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit tests for {@link ReactiveSessionBoundMongoTemplate}. @@ -94,7 +94,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Before public void setUp() { - mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType()); + mock(ListCollectionNamesPublisher.class); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index dfa4b00515..50ea579044 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -49,7 +49,6 @@ import com.mongodb.client.model.DeleteOptions; import com.mongodb.client.model.FindOneAndUpdateOptions; import com.mongodb.client.model.UpdateOptions; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and @@ -90,7 +89,7 @@ public class SessionBoundMongoTemplateUnitTests { @Before public void setUp() { - collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType()); + collectionNamesIterable = mock(ListCollectionNamesIterable.class); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java index d1fa2b1b9a..dda16f7849 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolverUnitTests.java @@ -31,7 +31,6 @@ import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; - import org.springframework.core.annotation.AliasFor; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.annotation.Id; @@ -506,7 +505,7 @@ public void resolvesComposedAnnotationIndexDefinitionOptionsCorrectly() { assertThat(indexDefinition.getIndexKeys()).containsEntry("location", "geoHaystack").containsEntry("What light?", 1); assertThat(indexDefinition.getIndexOptions()).containsEntry("name", "my_geo_index_name") - .containsEntry("bucketSize", 2.0); + .doesNotContainKey("bucketSize"); } @Test // DATAMONGO-2112 @@ -558,9 +557,6 @@ class GeoSpatialIndexedDocumentWithComposedAnnotation { @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "additionalField") String theAdditionalFieldINeedToDefine() default "What light?"; - @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "bucketSize") - double size() default 2; - @AliasFor(annotation = GeoSpatialIndexed.class, attribute = "type") GeoSpatialIndexType indexType() default GeoSpatialIndexType.GEO_HAYSTACK; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java index 156b5b23c6..2ec31e49bc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/IndexUnitTests.java @@ -80,9 +80,9 @@ public void testGeospatialIndex2DSphere() { public void testGeospatialIndexGeoHaystack() { GeospatialIndex i = new GeospatialIndex("location").typed(GeoSpatialIndexType.GEO_HAYSTACK) - .withAdditionalField("name").withBucketSize(40); + .withAdditionalField("name"); assertThat(i.getIndexKeys()).isEqualTo(Document.parse("{ \"location\" : \"geoHaystack\" , \"name\" : 1}")); - assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ \"bucketSize\" : 40.0}")); + assertThat(i.getIndexOptions()).isEqualTo(Document.parse("{ }")); } @Test diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java index f2fd993ef8..17a045b7a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java @@ -32,8 +32,8 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; import org.springframework.data.mongodb.test.util.CleanMongoDB.Struct; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; +import com.mongodb.client.ListCollectionNamesIterable; import com.mongodb.client.ListDatabasesIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; @@ -74,11 +74,11 @@ void setUp() throws ClassNotFoundException { when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock); // collections have to exist - MongoIterable collectionIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db1mock).collectionNameIterableType()); + MongoIterable collectionIterable = mock(ListCollectionNamesIterable.class); when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2")); doReturn(collectionIterable).when(db1mock).listCollectionNames(); - MongoIterable collectionIterable2 = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db2mock).collectionNameIterableType()); + MongoIterable collectionIterable2 = mock(ListCollectionNamesIterable.class); when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1")); doReturn(collectionIterable2).when(db2mock).listCollectionNames(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java index 40948a0e22..53bd7c6ab7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java @@ -28,7 +28,6 @@ import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.PersistentEntities; import org.springframework.data.mongodb.core.MongoTemplate; -import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; import org.testcontainers.shaded.org.awaitility.Awaitility; import com.mongodb.MongoWriteException; @@ -96,7 +95,7 @@ public void flush() { } public void flushDatabase() { - flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames()); + flush(getDb().listCollectionNames()); } public void flush(Iterable collections) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java deleted file mode 100644 index ab8e17a469..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2024-2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.util; - -import static org.assertj.core.api.Assertions.*; - -import org.junit.jupiter.api.Test; -import org.springframework.data.mongodb.test.util.ExcludeReactiveClientFromClassPath; -import org.springframework.data.mongodb.test.util.ExcludeSyncClientFromClassPath; -import org.springframework.util.ClassUtils; - -/** - * @author Christoph Strobl - */ -class MongoCompatibilityAdapterUnitTests { - - @Test // GH-4578 - @ExcludeReactiveClientFromClassPath - void returnsListCollectionNameIterableTypeCorrectly() { - - String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesIterable" : "MongoIterable"; - assertThat(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(null).collectionNameIterableType()) - .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); - - } - - @Test // GH-4578 - @ExcludeSyncClientFromClassPath - void returnsListCollectionNamePublisherTypeCorrectly() { - - String expectedType = MongoClientVersion.isVersion5orNewer() ? "ListCollectionNamesPublisher" : "Publisher"; - assertThat(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(null).collectionNamePublisherType()) - .satisfies(type -> assertThat(ClassUtils.getShortName(type)).isEqualTo(expectedType)); - - } -} From 13ff2d602c6b155de8fe5b39541eb457cc81e44f Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 12 Mar 2025 11:06:41 +0100 Subject: [PATCH 41/74] Migrate to JSpecify annotations for nullability constraints. Closes: #4874 --- spring-data-mongodb/pom.xml | 70 ++++++- .../data/mongodb/BindableMongoExpression.java | 20 +- .../data/mongodb/BulkOperationException.java | 5 +- .../data/mongodb/ClientSessionException.java | 2 +- ...efaultMongoTransactionOptionsResolver.java | 5 +- .../data/mongodb/MongoDatabaseUtils.java | 17 +- .../data/mongodb/MongoResourceHolder.java | 5 +- .../mongodb/MongoTransactionException.java | 2 +- .../data/mongodb/MongoTransactionManager.java | 26 ++- .../data/mongodb/MongoTransactionOptions.java | 49 ++--- .../MongoTransactionOptionsResolver.java | 2 +- .../mongodb/ReactiveMongoDatabaseUtils.java | 24 ++- .../mongodb/ReactiveMongoResourceHolder.java | 8 +- .../ReactiveMongoTransactionManager.java | 19 +- .../SessionAwareMethodInterceptor.java | 9 +- .../SimpleMongoTransactionOptions.java | 38 ++-- .../data/mongodb/TransactionMetadata.java | 2 +- .../mongodb/TransactionOptionResolver.java | 2 +- .../UncategorizedMongoDbException.java | 2 +- .../data/mongodb/aot/MongoAotPredicates.java | 2 +- ...agedTypesBeanRegistrationAotProcessor.java | 2 +- .../data/mongodb/aot/MongoRuntimeHints.java | 4 +- .../ConnectionStringPropertyEditor.java | 2 +- .../config/MappingMongoConverterParser.java | 13 +- .../MongoAuditingBeanDefinitionParser.java | 4 +- .../config/MongoConfigurationSupport.java | 11 +- .../config/MongoCredentialPropertyEditor.java | 2 +- .../mongodb/config/MongoDbFactoryParser.java | 7 +- .../mongodb/config/MongoParsingUtils.java | 2 + .../mongodb/config/MongoTemplateParser.java | 2 + .../config/ReadConcernPropertyEditor.java | 2 +- .../config/ReadPreferencePropertyEditor.java | 4 +- .../config/ServerAddressPropertyEditor.java | 13 +- .../UUidRepresentationPropertyEditor.java | 2 +- .../config/WriteConcernPropertyEditor.java | 2 +- .../data/mongodb/config/package-info.java | 2 +- .../data/mongodb/core/AggregationUtil.java | 3 +- .../data/mongodb/core/ChangeStreamEvent.java | 33 ++-- .../mongodb/core/ChangeStreamOptions.java | 22 ++- .../data/mongodb/core/CollectionCallback.java | 2 +- .../data/mongodb/core/CollectionOptions.java | 8 +- .../core/CollectionPreparerSupport.java | 5 +- .../data/mongodb/core/CountQuery.java | 5 +- .../data/mongodb/core/CursorPreparer.java | 5 +- .../data/mongodb/core/DbCallback.java | 2 +- .../mongodb/core/DefaultBulkOperations.java | 19 +- .../mongodb/core/DefaultIndexOperations.java | 12 +- .../core/DefaultIndexOperationsProvider.java | 3 +- .../core/DefaultReactiveBulkOperations.java | 16 +- .../core/DefaultReactiveIndexOperations.java | 31 +-- .../mongodb/core/DefaultScriptOperations.java | 44 ++--- .../core/DefaultWriteConcernResolver.java | 4 +- .../core/EntityLifecycleEventDelegate.java | 3 +- .../data/mongodb/core/EntityOperations.java | 34 ++-- ...ExecutableAggregationOperationSupport.java | 17 +- .../mongodb/core/ExecutableFindOperation.java | 2 +- .../core/ExecutableFindOperationSupport.java | 38 ++-- .../ExecutableInsertOperationSupport.java | 13 +- .../ExecutableMapReduceOperationSupport.java | 5 +- .../ExecutableRemoveOperationSupport.java | 9 +- .../core/ExecutableUpdateOperation.java | 2 +- .../ExecutableUpdateOperationSupport.java | 23 ++- .../mongodb/core/FindAndModifyOptions.java | 7 +- .../mongodb/core/FindAndReplaceOptions.java | 4 + .../mongodb/core/FindPublisherPreparer.java | 5 +- .../data/mongodb/core/HintFunction.java | 2 +- .../data/mongodb/core/MappedDocument.java | 5 +- .../core/MappingMongoJsonSchemaCreator.java | 13 +- .../data/mongodb/core/MongoAction.java | 17 +- .../mongodb/core/MongoClientFactoryBean.java | 13 +- .../core/MongoDatabaseFactorySupport.java | 4 +- .../MongoEncryptionSettingsFactoryBean.java | 12 +- .../core/MongoExceptionTranslator.java | 7 +- .../mongodb/core/MongoJsonSchemaCreator.java | 4 +- .../data/mongodb/core/MongoOperations.java | 56 ++---- .../core/MongoServerApiFactoryBean.java | 12 +- .../data/mongodb/core/MongoTemplate.java | 181 +++++++++++------- .../data/mongodb/core/QueryOperations.java | 8 +- .../ReactiveAggregationOperationSupport.java | 12 +- .../ReactiveChangeStreamOperationSupport.java | 2 +- .../core/ReactiveFindOperationSupport.java | 6 +- .../core/ReactiveInsertOperationSupport.java | 5 +- .../ReactiveMapReduceOperationSupport.java | 7 +- .../core/ReactiveMongoClientFactoryBean.java | 8 +- .../mongodb/core/ReactiveMongoOperations.java | 2 +- .../mongodb/core/ReactiveMongoTemplate.java | 107 ++++++----- .../core/ReactiveRemoveOperationSupport.java | 5 +- .../core/ReactiveUpdateOperationSupport.java | 25 ++- .../data/mongodb/core/ReadConcernAware.java | 2 +- .../mongodb/core/ReadPreferenceAware.java | 2 +- .../data/mongodb/core/ReplaceOptions.java | 2 + .../data/mongodb/core/ScriptOperations.java | 2 +- .../data/mongodb/core/ScrollUtils.java | 8 + .../data/mongodb/core/SessionCallback.java | 2 +- .../data/mongodb/core/SessionScoped.java | 10 +- .../SimpleReactiveMongoDatabaseFactory.java | 2 +- .../data/mongodb/core/ViewOptions.java | 4 +- .../data/mongodb/core/WriteConcernAware.java | 2 +- .../mongodb/core/WriteConcernResolver.java | 2 +- .../AbstractAggregationExpression.java | 3 +- .../aggregation/AccumulatorOperators.java | 55 +++++- .../core/aggregation/AddFieldsOperation.java | 7 +- .../AggregationExpressionTransformer.java | 2 +- .../AggregationOperationContext.java | 2 +- .../AggregationOperationRenderer.java | 2 +- .../core/aggregation/AggregationOptions.java | 25 ++- .../core/aggregation/AggregationPipeline.java | 2 + .../core/aggregation/AggregationResults.java | 8 +- .../AggregationSpELExpression.java | 4 +- .../core/aggregation/AggregationUpdate.java | 11 +- .../core/aggregation/AggregationVariable.java | 2 +- .../core/aggregation/ArithmeticOperators.java | 147 +++++++++----- .../core/aggregation/ArrayOperators.java | 45 ++++- .../core/aggregation/BooleanOperators.java | 15 +- .../core/aggregation/BucketAutoOperation.java | 5 +- .../core/aggregation/BucketOperation.java | 8 +- .../aggregation/BucketOperationSupport.java | 18 +- .../core/aggregation/ComparisonOperators.java | 34 +++- .../aggregation/ConditionalOperators.java | 32 +++- .../core/aggregation/ConvertOperators.java | 4 +- .../core/aggregation/DateOperators.java | 70 ++++++- .../core/aggregation/DensifyOperation.java | 13 +- .../DocumentEnhancingOperation.java | 9 +- .../core/aggregation/DocumentOperators.java | 4 + .../core/aggregation/EvaluationOperators.java | 7 +- .../core/aggregation/ExposedFields.java | 5 +- ...osedFieldsAggregationOperationContext.java | 6 +- .../core/aggregation/FacetOperation.java | 1 + .../data/mongodb/core/aggregation/Fields.java | 11 +- .../core/aggregation/GeoNearOperation.java | 4 +- .../aggregation/GraphLookupOperation.java | 29 ++- .../core/aggregation/GroupOperation.java | 7 +- ...osedFieldsAggregationOperationContext.java | 2 +- .../core/aggregation/LookupOperation.java | 8 + .../core/aggregation/MatchOperation.java | 6 +- .../core/aggregation/MergeOperation.java | 21 +- ...ExpressionAggregationOperationContext.java | 3 +- .../core/aggregation/ObjectOperators.java | 11 +- .../core/aggregation/OutOperation.java | 10 +- ...DelegatingAggregationOperationContext.java | 2 +- .../core/aggregation/ProjectionOperation.java | 13 +- .../core/aggregation/RedactOperation.java | 27 ++- .../aggregation/ReplaceRootOperation.java | 2 + .../core/aggregation/ScriptOperators.java | 83 ++++---- .../core/aggregation/SelectionOperators.java | 26 +++ .../core/aggregation/SetOperation.java | 6 +- .../core/aggregation/SetOperators.java | 26 ++- .../aggregation/SetWindowFieldsOperation.java | 47 ++++- .../aggregation/SortByCountOperation.java | 3 +- .../SpelExpressionTransformer.java | 36 ++-- .../core/aggregation/StringOperators.java | 82 +++++++- .../core/aggregation/SystemVariable.java | 5 +- .../TypeBasedAggregationOperationContext.java | 3 +- .../core/aggregation/UnionWithOperation.java | 2 +- .../core/aggregation/UnsetOperation.java | 3 + .../core/aggregation/UnwindOperation.java | 10 +- .../core/aggregation/VariableOperators.java | 6 +- .../aggregation/VectorSearchOperation.java | 20 +- .../core/aggregation/package-info.java | 2 +- .../mongodb/core/annotation/package-info.java | 2 +- .../core/convert/AbstractMongoConverter.java | 2 +- .../core/convert/DbRefProxyHandler.java | 2 +- .../mongodb/core/convert/DbRefResolver.java | 4 +- .../core/convert/DbRefResolverCallback.java | 3 +- .../convert/DefaultDbRefProxyHandler.java | 3 +- .../core/convert/DefaultDbRefResolver.java | 9 +- .../convert/DefaultDbRefResolverCallback.java | 3 +- .../core/convert/DefaultMongoTypeMapper.java | 4 +- .../convert/DefaultReferenceResolver.java | 6 +- .../core/convert/DocumentAccessor.java | 8 +- .../core/convert/DocumentPointerFactory.java | 19 +- .../convert/DocumentPropertyAccessor.java | 2 +- .../core/convert/DocumentReferenceSource.java | 12 +- .../mongodb/core/convert/GeoConverters.java | 106 ++++++---- .../core/convert/LazyLoadingProxy.java | 5 +- .../core/convert/LazyLoadingProxyFactory.java | 39 ++-- .../core/convert/MappingMongoConverter.java | 138 +++++++------ .../core/convert/MongoConversionContext.java | 6 +- .../mongodb/core/convert/MongoConverter.java | 7 +- .../mongodb/core/convert/MongoConverters.java | 16 +- .../core/convert/MongoCustomConversions.java | 13 +- .../core/convert/MongoExampleMapper.java | 9 +- .../core/convert/MongoJsonSchemaMapper.java | 2 +- .../mongodb/core/convert/MongoWriter.java | 7 +- .../core/convert/NoOpDbRefResolver.java | 9 +- .../data/mongodb/core/convert/ObjectPath.java | 11 +- .../mongodb/core/convert/QueryMapper.java | 49 +++-- .../mongodb/core/convert/ReferenceLoader.java | 5 +- .../core/convert/ReferenceLookupDelegate.java | 20 +- .../core/convert/ReferenceResolver.java | 8 +- .../mongodb/core/convert/UpdateMapper.java | 23 ++- .../mongodb/core/convert/ValueResolver.java | 3 +- .../encryption/EncryptingConverter.java | 9 +- .../encryption/ExplicitEncryptionContext.java | 20 +- .../encryption/MongoEncryptionConverter.java | 5 +- .../core/convert/encryption/package-info.java | 2 +- .../mongodb/core/convert/package-info.java | 2 +- .../core/encryption/EncryptionContext.java | 21 +- .../mongodb/core/encryption/package-info.java | 2 +- .../core/geo/GeoJsonGeometryCollection.java | 2 +- .../data/mongodb/core/geo/GeoJsonModule.java | 18 +- .../core/geo/GeoJsonMultiLineString.java | 2 +- .../mongodb/core/geo/GeoJsonMultiPoint.java | 2 +- .../mongodb/core/geo/GeoJsonMultiPolygon.java | 2 +- .../data/mongodb/core/geo/GeoJsonPolygon.java | 6 +- .../data/mongodb/core/geo/Sphere.java | 2 +- .../data/mongodb/core/geo/package-info.java | 2 +- .../index/DefaultSearchIndexOperations.java | 4 +- .../mongodb/core/index/GeospatialIndex.java | 9 + .../data/mongodb/core/index/Index.java | 15 +- .../data/mongodb/core/index/IndexField.java | 5 +- .../data/mongodb/core/index/IndexInfo.java | 17 +- .../core/index/IndexOperationsProvider.java | 2 +- .../data/mongodb/core/index/IndexOptions.java | 17 +- .../mongodb/core/index/IndexPredicate.java | 5 +- .../MongoPersistentEntityIndexCreator.java | 6 +- .../MongoPersistentEntityIndexResolver.java | 26 ++- .../core/index/TextIndexDefinition.java | 13 +- .../data/mongodb/core/index/VectorIndex.java | 4 +- .../mongodb/core/index/WildcardIndex.java | 12 +- .../data/mongodb/core/index/package-info.java | 2 +- .../mapping/BasicMongoPersistentEntity.java | 34 ++-- .../mapping/BasicMongoPersistentProperty.java | 12 +- .../CachingMongoPersistentProperty.java | 6 +- .../data/mongodb/core/mapping/MongoField.java | 11 +- .../core/mapping/MongoMappingContext.java | 8 +- .../core/mapping/MongoPersistentEntity.java | 6 +- .../core/mapping/MongoPersistentProperty.java | 4 +- .../mapping/PersistentPropertyTranslator.java | 2 +- .../data/mongodb/core/mapping/ShardKey.java | 2 +- .../core/mapping/UnwrapEntityContext.java | 2 +- .../UnwrappedMongoPersistentEntity.java | 30 ++- .../UnwrappedMongoPersistentProperty.java | 46 ++--- .../mapping/event/AbstractDeleteEvent.java | 5 +- .../core/mapping/event/AfterDeleteEvent.java | 2 +- .../core/mapping/event/BeforeDeleteEvent.java | 2 +- .../core/mapping/event/MongoMappingEvent.java | 2 +- .../core/mapping/event/package-info.java | 2 +- .../mongodb/core/mapping/package-info.java | 2 +- .../core/mapreduce/MapReduceCounts.java | 2 +- .../core/mapreduce/MapReduceOptions.java | 24 ++- .../core/mapreduce/MapReduceResults.java | 11 +- .../core/mapreduce/MapReduceTiming.java | 2 +- .../mongodb/core/mapreduce/package-info.java | 2 +- .../core/messaging/ChangeStreamRequest.java | 21 +- .../core/messaging/ChangeStreamTask.java | 11 +- .../core/messaging/CursorReadingTask.java | 14 +- .../DefaultMessageListenerContainer.java | 2 +- .../LazyMappingDelegatingMessage.java | 5 +- .../data/mongodb/core/messaging/Message.java | 15 +- .../mongodb/core/messaging/SimpleMessage.java | 6 +- .../core/messaging/SubscriptionRequest.java | 7 +- .../core/messaging/TailableCursorRequest.java | 13 +- .../mongodb/core/messaging/package-info.java | 2 +- .../data/mongodb/core/package-info.java | 2 +- .../data/mongodb/core/query/BasicQuery.java | 5 +- .../data/mongodb/core/query/BasicUpdate.java | 15 +- .../data/mongodb/core/query/Collation.java | 25 ++- .../data/mongodb/core/query/Criteria.java | 54 +++++- .../core/query/CriteriaDefinition.java | 2 +- .../data/mongodb/core/query/Field.java | 14 +- .../data/mongodb/core/query/GeoCommand.java | 2 +- .../data/mongodb/core/query/Meta.java | 15 +- .../mongodb/core/query/MetricConversion.java | 9 +- .../mongodb/core/query/MongoRegexCreator.java | 5 +- .../data/mongodb/core/query/NearQuery.java | 39 ++-- .../data/mongodb/core/query/Query.java | 41 +++- .../core/query/SerializationUtils.java | 8 +- .../data/mongodb/core/query/Term.java | 4 +- .../data/mongodb/core/query/TextCriteria.java | 15 +- .../data/mongodb/core/query/TextQuery.java | 11 +- .../core/query/UntypedExampleMatcher.java | 2 +- .../data/mongodb/core/query/Update.java | 29 ++- .../data/mongodb/core/query/package-info.java | 2 +- .../core/schema/DefaultMongoJsonSchema.java | 5 +- .../IdentifiableJsonSchemaProperty.java | 95 ++++++++- .../mongodb/core/schema/JsonSchemaObject.java | 2 +- .../mongodb/core/schema/MongoJsonSchema.java | 27 ++- .../schema/TypeUnifyingMergeFunction.java | 7 +- .../core/schema/TypedJsonSchemaObject.java | 94 ++++++++- .../core/schema/UntypedJsonSchemaObject.java | 26 ++- .../mongodb/core/schema/package-info.java | 2 +- .../mongodb/core/script/package-info.java | 2 +- .../mongodb/core/spel/ExpressionNode.java | 5 +- ...xpressionTransformationContextSupport.java | 17 +- .../core/spel/ExpressionTransformer.java | 4 +- .../data/mongodb/core/spel/LiteralNode.java | 2 +- .../core/spel/MethodReferenceNode.java | 15 +- .../data/mongodb/core/spel/package-info.java | 2 +- .../core/validation/CriteriaValidator.java | 2 +- .../core/validation/DocumentValidator.java | 2 +- .../core/validation/JsonSchemaValidator.java | 2 +- .../mongodb/core/validation/package-info.java | 2 +- .../data/mongodb/gridfs/GridFsCriteria.java | 2 +- .../data/mongodb/gridfs/GridFsObject.java | 6 +- .../data/mongodb/gridfs/GridFsOperations.java | 5 +- .../gridfs/GridFsOperationsSupport.java | 2 +- .../data/mongodb/gridfs/GridFsResource.java | 12 +- .../data/mongodb/gridfs/GridFsTemplate.java | 4 +- .../data/mongodb/gridfs/GridFsUpload.java | 29 ++- .../gridfs/ReactiveGridFsOperations.java | 2 +- .../gridfs/ReactiveGridFsResource.java | 4 +- .../gridfs/ReactiveGridFsTemplate.java | 2 +- .../mongodb/gridfs/ReactiveGridFsUpload.java | 28 ++- .../data/mongodb/gridfs/package-info.java | 2 +- .../data/mongodb/monitor/package-info.java | 6 + ...aultMongoHandlerObservationConvention.java | 7 + .../observability/MapRequestContext.java | 10 +- .../observability/MongoHandlerContext.java | 29 ++- .../MongoObservationCommandListener.java | 5 +- .../mongodb/observability/package-info.java | 2 +- .../data/mongodb/package-info.java | 2 +- .../aot/RepositoryRuntimeHints.java | 2 +- .../mongodb/repository/aot/package-info.java | 2 +- .../mongodb/repository/cdi/package-info.java | 2 +- .../repository/config/package-info.java | 2 +- .../data/mongodb/repository/package-info.java | 2 +- .../repository/query/AbstractMongoQuery.java | 11 +- .../query/AbstractReactiveMongoQuery.java | 6 +- .../repository/query/AggregationUtils.java | 12 +- .../repository/query/CollationUtils.java | 8 +- .../query/ConvertingParameterAccessor.java | 29 ++- .../query/MongoEntityInformation.java | 5 +- .../query/MongoParameterAccessor.java | 6 +- .../repository/query/MongoParameters.java | 21 +- .../MongoParametersParameterAccessor.java | 20 +- .../repository/query/MongoQueryCreator.java | 14 +- .../repository/query/MongoQueryExecution.java | 10 +- .../repository/query/MongoQueryMethod.java | 6 +- .../repository/query/PartTreeMongoQuery.java | 1 + .../mongodb/repository/query/QueryUtils.java | 3 +- .../query/ReactiveMongoParameterAccessor.java | 12 +- .../query/ReactiveMongoQueryExecution.java | 7 +- .../query/ReactivePartTreeMongoQuery.java | 1 + .../query/ReactiveStringBasedAggregation.java | 2 +- .../query/ReactiveStringBasedMongoQuery.java | 13 +- .../query/StringAggregationOperation.java | 2 +- .../query/StringBasedAggregation.java | 6 +- .../query/StringBasedMongoQuery.java | 2 + ...ssionDelegateValueExpressionEvaluator.java | 3 +- .../repository/query/package-info.java | 2 +- .../CrudMethodMetadataPostProcessor.java | 8 +- .../MappingMongoEntityInformation.java | 7 +- .../support/MongoAnnotationProcessor.java | 3 +- .../MongoEntityInformationSupport.java | 2 +- .../support/MongoRepositoryFactory.java | 6 +- .../support/MongoRepositoryFactoryBean.java | 3 +- .../QuerydslMongoPredicateExecutor.java | 5 +- .../ReactiveMongoRepositoryFactory.java | 8 +- .../ReactiveMongoRepositoryFactoryBean.java | 3 +- .../ReactiveSpringDataMongodbQuery.java | 4 +- .../support/SimpleMongoRepository.java | 6 +- .../SimpleReactiveMongoRepository.java | 2 +- .../support/SpringDataMongodbQuery.java | 11 +- .../SpringDataMongodbQuerySupport.java | 2 + .../support/SpringDataMongodbSerializer.java | 10 +- .../repository/support/package-info.java | 2 +- .../data/mongodb/util/BsonUtils.java | 52 ++--- .../data/mongodb/util/DotPath.java | 2 +- .../data/mongodb/util/DurationUtil.java | 6 +- .../data/mongodb/util/EmptyDocument.java | 3 +- .../data/mongodb/util/MongoClientVersion.java | 13 +- .../data/mongodb/util/MongoDbErrorCodes.java | 13 +- .../data/mongodb/util/RegexFlags.java | 2 +- .../aggregation/TestAggregationContext.java | 2 +- .../util/encryption/EncryptionUtils.java | 5 +- .../mongodb/util/json/DateTimeFormatter.java | 3 + .../EvaluationContextExpressionEvaluator.java | 7 +- .../data/mongodb/util/json/JsonBuffer.java | 2 + .../data/mongodb/util/json/JsonScanner.java | 2 + .../data/mongodb/util/json/JsonToken.java | 2 + .../util/json/ParameterBindingContext.java | 11 +- .../json/ParameterBindingDocumentCodec.java | 8 +- .../util/json/ParameterBindingJsonReader.java | 10 +- .../data/mongodb/util/json/ValueProvider.java | 2 +- .../data/mongodb/util/json/package-info.java | 2 +- .../data/mongodb/util/package-info.java | 2 +- .../mongodb/util/spel/ExpressionUtils.java | 8 +- .../core/query/TypedUpdateExtensions.kt | 2 +- .../CapturingTransactionOptionsResolver.java | 5 +- .../MongoTransactionOptionsUnitTests.java | 14 +- .../MongoExceptionTranslatorUnitTests.java | 2 +- .../MongoTemplateDocumentReferenceTests.java | 11 +- .../core/MongoTemplateScrollTests.java | 2 +- .../data/mongodb/core/MongoTemplateTests.java | 2 +- .../mongodb/core/MongoTemplateUnitTests.java | 5 +- .../core/MongoTemplateValidationTests.java | 8 +- .../data/mongodb/core/Person.java | 2 +- ...iveMapReduceOperationSupportUnitTests.java | 10 +- .../core/ReactiveMongoTemplateUnitTests.java | 2 +- .../core/TransactionOptionsTestService.java | 23 +-- .../core/UpdateOperationsUnitTests.java | 4 +- .../data/mongodb/core/User.java | 2 +- .../AddFieldsOperationUnitTests.java | 2 +- .../DensifyOperationUnitTests.java | 2 +- .../GeoNearOperationUnitTests.java | 2 +- .../aggregation/MergeOperationUnitTests.java | 2 +- .../data/mongodb/core/aggregation/Order.java | 2 + .../aggregation/RedactOperationUnitTests.java | 2 +- .../aggregation/SetOperationUnitTests.java | 2 +- .../SetWindowFieldsOperationUnitTests.java | 2 +- .../UnionWithOperationUnitTests.java | 2 +- .../aggregation/UnsetOperationUnitTests.java | 2 +- .../DbRefMappingMongoConverterUnitTests.java | 2 +- .../MappingMongoConverterUnitTests.java | 32 ++-- .../core/convert/ReversingValueConverter.java | 2 +- .../mongodb/core/query/TextQueryTests.java | 2 +- .../performance/ReactivePerformanceTests.java | 5 +- .../data/mongodb/repository/Address.java | 3 +- ...oRepositoryTextSearchIntegrationTests.java | 2 +- .../data/mongodb/repository/MyId.java | 2 +- .../data/mongodb/repository/Person.java | 2 +- .../mongodb/repository/PersonRepository.java | 2 +- .../PersonRepositoryTransactionalTests.java | 5 +- .../SimpleReactiveMongoRepositoryTests.java | 2 +- .../mongodb/repository/UserWithComplexId.java | 2 +- .../mongodb/repository/VersionedPerson.java | 5 +- ...activeStringBasedAggregationUnitTests.java | 8 +- .../StringBasedAggregationUnitTests.java | 8 +- .../query/StubParameterAccessor.java | 4 +- .../test/util/MappingContextConfigurer.java | 2 +- .../util/MongoTestTemplateConfiguration.java | 2 +- 422 files changed, 3143 insertions(+), 1761 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 36dcb50ade..4d9d5a3b50 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -353,8 +353,76 @@ - + + + nullaway + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + com.querydsl + querydsl-apt + ${querydsl} + + + org.openjdk.jmh + jmh-generator-annprocess + ${jmh} + + + com.google.errorprone + error_prone_core + ${errorprone} + + + com.uber.nullaway + nullaway + ${nullaway} + + + + + + default-compile + none + + + default-testCompile + none + + + java-compile + compile + + compile + + + + -XDcompilePolicy=simple + --should-stop=ifError=FLOW + -Xplugin:ErrorProne -XepDisableAllChecks -Xep:NullAway:ERROR -XepOpt:NullAway:OnlyNullMarked=true -XepOpt:NullAway:TreatGeneratedAsUnannotated=true -XepOpt:NullAway:CustomContractAnnotations=org.springframework.lang.Contract + + + + + java-test-compile + test-compile + + testCompile + + + + + + + + + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java index 1f6875c080..3ae41aad35 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BindableMongoExpression.java @@ -20,9 +20,10 @@ import org.bson.Document; import org.bson.codecs.DocumentCodec; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -31,8 +32,7 @@ * A {@link MongoExpression} using the {@link ParameterBindingDocumentCodec} for parsing a raw ({@literal json}) * expression. The expression will be wrapped within { ... } if necessary. The actual parsing and parameter * binding of placeholders like {@code ?0} is delayed upon first call on the target {@link Document} via - * {@link #toDocument()}. - *
+ * {@link #toDocument()}.
* *

  * $toUpper : $name                -> { '$toUpper' : '$name' }
@@ -55,7 +55,7 @@ public class BindableMongoExpression implements MongoExpression {
 
 	private final @Nullable CodecRegistryProvider codecRegistryProvider;
 
-	private final @Nullable Object[] args;
+	private final Object @Nullable [] args;
 
 	private final Lazy target;
 
@@ -63,9 +63,9 @@ public class BindableMongoExpression implements MongoExpression {
 	 * Create a new instance of {@link BindableMongoExpression}.
 	 *
 	 * @param expression must not be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
-	public BindableMongoExpression(String expression, @Nullable Object[] args) {
+	public BindableMongoExpression(String expression, Object @Nullable [] args) {
 		this(expression, null, args);
 	}
 
@@ -74,10 +74,10 @@ public BindableMongoExpression(String expression, @Nullable Object[] args) {
 	 *
 	 * @param expression must not be {@literal null}.
 	 * @param codecRegistryProvider can be {@literal null}.
-	 * @param args can be {@literal null}.
+	 * @param args must not be {@literal null} but may contain {@literal null} elements.
 	 */
 	public BindableMongoExpression(String expression, @Nullable CodecRegistryProvider codecRegistryProvider,
-			@Nullable Object[] args) {
+			Object @Nullable [] args) {
 
 		Assert.notNull(expression, "Expression must not be null");
 
@@ -93,6 +93,7 @@ public BindableMongoExpression(String expression, @Nullable CodecRegistryProvide
 	 * @param codecRegistry must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 		return new BindableMongoExpression(expressionString, () -> codecRegistry, args);
 	}
@@ -103,6 +104,7 @@ public BindableMongoExpression withCodecRegistry(CodecRegistry codecRegistry) {
 	 * @param args must not be {@literal null}.
 	 * @return new instance of {@link BindableMongoExpression}.
 	 */
+	@Contract("_ -> new")
 	public BindableMongoExpression bind(Object... args) {
 		return new BindableMongoExpression(expressionString, codecRegistryProvider, args);
 	}
@@ -139,7 +141,7 @@ private Document parse() {
 
 	private static String wrapJsonIfNecessary(String json) {
 
-		if(!StringUtils.hasText(json)) {
+		if (!StringUtils.hasText(json)) {
 			return json;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
index b36382a58e..12d8c966af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/BulkOperationException.java
@@ -17,6 +17,7 @@
 
 import java.util.List;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.DataAccessException;
 
 import com.mongodb.MongoBulkWriteException;
@@ -40,10 +41,10 @@ public class BulkOperationException extends DataAccessException {
 	/**
 	 * Creates a new {@link BulkOperationException} with the given message and source {@link MongoBulkWriteException}.
 	 *
-	 * @param message must not be {@literal null}.
+	 * @param message can be {@literal null}.
 	 * @param source must not be {@literal null}.
 	 */
-	public BulkOperationException(String message, MongoBulkWriteException source) {
+	public BulkOperationException(@Nullable String message, MongoBulkWriteException source) {
 
 		super(message, source);
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
index 53acf65470..c59eecb43a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ClientSessionException.java
@@ -15,8 +15,8 @@
  */
 package org.springframework.data.mongodb;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.dao.NonTransientDataAccessException;
-import org.springframework.lang.Nullable;
 
 /**
  * {@link NonTransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
index c07e2dbe4a..87201ef9ee 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/DefaultMongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.Set;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 
 /**
  * Default implementation of {@link MongoTransactionOptions} using {@literal mongo:} as {@link #getLabelPrefix() label
@@ -42,9 +42,8 @@ public MongoTransactionOptions convert(Map options) {
 		return SimpleMongoTransactionOptions.of(options);
 	}
 
-	@Nullable
 	@Override
-	public String getLabelPrefix() {
+	public @Nullable String getLabelPrefix() {
 		return PREFIX;
 	}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
index f73f9fb7ed..042a5ba1d3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDatabaseUtils.java
@@ -15,7 +15,7 @@
  */
 package org.springframework.data.mongodb;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.support.ResourceHolderSynchronization;
 import org.springframework.transaction.support.TransactionSynchronization;
 import org.springframework.transaction.support.TransactionSynchronizationManager;
@@ -29,8 +29,7 @@
 /**
  * Helper class for managing a {@link MongoDatabase} instances via {@link MongoDatabaseFactory}. Used for obtaining
  * {@link ClientSession session bound} resources, such as {@link MongoDatabase} and
- * {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
- * 
+ * {@link com.mongodb.client.MongoCollection} suitable for transactional usage.
* Note: Intended for internal usage only. * * @author Christoph Strobl @@ -42,8 +41,7 @@ public class MongoDatabaseUtils { /** * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -55,8 +53,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory) { } /** - * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}. - *
+ * Obtain the default {@link MongoDatabase database} form the given {@link MongoDatabaseFactory factory}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -70,8 +67,7 @@ public static MongoDatabase getDatabase(MongoDatabaseFactory factory, SessionSyn /** * Obtain the {@link MongoDatabase database} with given name form the given {@link MongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the current * {@link Thread} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -139,8 +135,7 @@ public static boolean isTransactionActive(MongoDatabaseFactory dbFactory) { return resourceHolder != null && resourceHolder.hasActiveTransaction(); } - @Nullable - private static ClientSession doGetSession(MongoDatabaseFactory dbFactory, + private static @Nullable ClientSession doGetSession(MongoDatabaseFactory dbFactory, SessionSynchronization sessionSynchronization) { MongoResourceHolder resourceHolder = (MongoResourceHolder) TransactionSynchronizationManager.getResource(dbFactory); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java index a1e8344a9f..81c25d0998 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoResourceHolder.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.ResourceHolderSupport; @@ -23,8 +23,7 @@ /** * MongoDB specific {@link ResourceHolderSupport resource holder}, wrapping a {@link ClientSession}. - * {@link MongoTransactionManager} binds instances of this class to the thread. - *
+ * {@link MongoTransactionManager} binds instances of this class to the thread.
* Note: Intended for internal usage only. * * @author Christoph Strobl diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java index 4215479f62..3d7bec6780 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionException.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A specific {@link ClientSessionException} related to issues with a transaction such as aborted or non existing diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java index eda657f5f1..1f97bb69e9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionManager.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -36,19 +36,15 @@ /** * A {@link org.springframework.transaction.PlatformTransactionManager} implementation that manages - * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}. - *
- * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread. - *
+ * {@link ClientSession} based transactions for a single {@link MongoDatabaseFactory}.
+ * Binds a {@link ClientSession} from the specified {@link MongoDatabaseFactory} to the thread.
* {@link TransactionDefinition#isReadOnly() Readonly} transactions operate on a {@link ClientSession} and enable causal * consistency, and also {@link ClientSession#startTransaction() start}, {@link ClientSession#commitTransaction() - * commit} or {@link ClientSession#abortTransaction() abort} a transaction. - *
+ * commit} or {@link ClientSession#abortTransaction() abort} a transaction.
* Application code is required to retrieve the {@link com.mongodb.client.MongoDatabase} via * {@link MongoDatabaseUtils#getDatabase(MongoDatabaseFactory)} instead of a standard * {@link MongoDatabaseFactory#getMongoDatabase()} call. Spring classes such as - * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly. - *
+ * {@link org.springframework.data.mongodb.core.MongoTemplate} use this strategy implicitly.
* By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. One may override * {@link #doCommit(MongoTransactionObject)} to implement the * Retry Commit Operation @@ -80,7 +76,9 @@ public class MongoTransactionManager extends AbstractPlatformTransactionManager * @see #setTransactionSynchronization(int) */ public MongoTransactionManager() { + this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver(); + this.options = MongoTransactionOptions.NONE; } /** @@ -151,7 +149,8 @@ protected void doBegin(Object transaction, TransactionDefinition definition) thr } try { - MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition).mergeWith(options); + MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition) + .mergeWith(options); mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions()); } catch (MongoException ex) { throw new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.", @@ -206,6 +205,7 @@ protected final void doCommit(DefaultTransactionStatus status) throws Transactio * By default those labels are ignored, nevertheless one might check for * {@link MongoException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the * commit.
+ * *
 	 * 
 	 * int retries = 3;
@@ -302,8 +302,7 @@ public void setOptions(@Nullable TransactionOptions options) {
 	 *
 	 * @return can be {@literal null}.
 	 */
-	@Nullable
-	public MongoDatabaseFactory getDatabaseFactory() {
+	public @Nullable MongoDatabaseFactory getDatabaseFactory() {
 		return databaseFactory;
 	}
 
@@ -461,8 +460,7 @@ void closeSession() {
 			}
 		}
 
-		@Nullable
-		public ClientSession getSession() {
+		public @Nullable ClientSession getSession() {
 			return resourceHolder != null ? resourceHolder.getSession() : null;
 		}
 
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
index e411bd5d2d..04bcd36e35 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptions.java
@@ -19,15 +19,16 @@
 import java.util.concurrent.TimeUnit;
 import java.util.function.Function;
 
+import org.jspecify.annotations.Nullable;
 import org.springframework.data.mongodb.core.ReadConcernAware;
 import org.springframework.data.mongodb.core.ReadPreferenceAware;
 import org.springframework.data.mongodb.core.WriteConcernAware;
-import org.springframework.lang.Nullable;
 
 import com.mongodb.ReadConcern;
 import com.mongodb.ReadPreference;
 import com.mongodb.TransactionOptions;
 import com.mongodb.WriteConcern;
+import org.springframework.lang.Contract;
 
 /**
  * Options to be applied within a specific transaction scope.
@@ -43,27 +44,23 @@ public interface MongoTransactionOptions
 	 */
 	MongoTransactionOptions NONE = new MongoTransactionOptions() {
 
-		@Nullable
 		@Override
-		public Duration getMaxCommitTime() {
+		public @Nullable Duration getMaxCommitTime() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadConcern getReadConcern() {
+		public @Nullable ReadConcern getReadConcern() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public ReadPreference getReadPreference() {
+		public @Nullable ReadPreference getReadPreference() {
 			return null;
 		}
 
-		@Nullable
 		@Override
-		public WriteConcern getWriteConcern() {
+		public @Nullable WriteConcern getWriteConcern() {
 			return null;
 		}
 	};
@@ -76,6 +73,7 @@ public WriteConcern getWriteConcern() {
 	 * @return new instance of {@link MongoTransactionOptions} or this if {@literal fallbackOptions} is {@literal null} or
 	 *         {@link #NONE}.
 	 */
+	@Contract("null -> this")
 	default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fallbackOptions) {
 
 		if (fallbackOptions == null || MongoTransactionOptions.NONE.equals(fallbackOptions)) {
@@ -84,30 +82,26 @@ default MongoTransactionOptions mergeWith(@Nullable MongoTransactionOptions fall
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 				return MongoTransactionOptions.this.hasMaxCommitTime() ? MongoTransactionOptions.this.getMaxCommitTime()
 						: fallbackOptions.getMaxCommitTime();
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return MongoTransactionOptions.this.hasReadConcern() ? MongoTransactionOptions.this.getReadConcern()
 						: fallbackOptions.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return MongoTransactionOptions.this.hasReadPreference() ? MongoTransactionOptions.this.getReadPreference()
 						: fallbackOptions.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return MongoTransactionOptions.this.hasWriteConcern() ? MongoTransactionOptions.this.getWriteConcern()
 						: fallbackOptions.getWriteConcern();
 			}
@@ -128,8 +122,8 @@ default  T map(Function mappingFunction) {
 	 * @return MongoDB driver native {@link TransactionOptions}.
 	 * @see MongoTransactionOptions#map(Function)
 	 */
-	@Nullable
-	default TransactionOptions toDriverOptions() {
+	@SuppressWarnings("NullAway")
+	default @Nullable TransactionOptions toDriverOptions() {
 
 		return map(it -> {
 
@@ -157,7 +151,7 @@ default TransactionOptions toDriverOptions() {
 	/**
 	 * Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
 	 *
-	 * @param options
+	 * @param options can be {@literal null}.
 	 * @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
 	 */
 	static MongoTransactionOptions of(@Nullable TransactionOptions options) {
@@ -168,35 +162,30 @@ static MongoTransactionOptions of(@Nullable TransactionOptions options) {
 
 		return new MongoTransactionOptions() {
 
-			@Nullable
 			@Override
-			public Duration getMaxCommitTime() {
+			public @Nullable Duration getMaxCommitTime() {
 
 				Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS);
 				return millis != null ? Duration.ofMillis(millis) : null;
 			}
 
-			@Nullable
 			@Override
-			public ReadConcern getReadConcern() {
+			public @Nullable ReadConcern getReadConcern() {
 				return options.getReadConcern();
 			}
 
-			@Nullable
 			@Override
-			public ReadPreference getReadPreference() {
+			public @Nullable ReadPreference getReadPreference() {
 				return options.getReadPreference();
 			}
 
-			@Nullable
 			@Override
-			public WriteConcern getWriteConcern() {
+			public @Nullable WriteConcern getWriteConcern() {
 				return options.getWriteConcern();
 			}
 
-			@Nullable
 			@Override
-			public TransactionOptions toDriverOptions() {
+			public @Nullable TransactionOptions toDriverOptions() {
 				return options;
 			}
 		};
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
index b73b079a99..c4bdbcca53 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
@@ -18,7 +18,7 @@
 import java.util.Map;
 import java.util.stream.Collectors;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.TransactionDefinition;
 import org.springframework.transaction.interceptor.TransactionAttribute;
 import org.springframework.util.Assert;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
index f397818a4c..3d1c2ee89c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
@@ -18,7 +18,7 @@
 import reactor.core.publisher.Mono;
 import reactor.util.context.Context;
 
-import org.springframework.lang.Nullable;
+import org.jspecify.annotations.Nullable;
 import org.springframework.transaction.NoTransactionException;
 import org.springframework.transaction.reactive.ReactiveResourceSynchronization;
 import org.springframework.transaction.reactive.TransactionSynchronization;
@@ -35,8 +35,7 @@
 /**
  * Helper class for managing reactive {@link MongoDatabase} instances via {@link ReactiveMongoDatabaseFactory}. Used for
  * obtaining {@link ClientSession session bound} resources, such as {@link MongoDatabase} and {@link MongoCollection}
- * suitable for transactional usage.
- * 
+ * suitable for transactional usage.
* Note: Intended for internal usage only. * * @author Mark Paluch @@ -74,8 +73,7 @@ public static Mono isTransactionActive(ReactiveMongoDatabaseFactory dat /** * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using - * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * @@ -103,32 +101,32 @@ public static Mono getDatabase(ReactiveMongoDatabaseFactory facto /** * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory - * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}. - *
+ * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * - * @param dbName the name of the {@link MongoDatabase} to get. + * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the + * {@link ReactiveMongoDatabaseFactory}. * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. */ - public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory) { + public static Mono getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory) { return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION); } /** * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory - * factory}. - *
+ * factory}.
* Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}. * - * @param dbName the name of the {@link MongoDatabase} to get. + * @param dbName the name of the {@link MongoDatabase} to get. If {@literal null} the default database of the * + * {@link ReactiveMongoDatabaseFactory}. * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from. * @param sessionSynchronization the synchronization to use. Must not be {@literal null}. * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}. */ - public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory, + public static Mono getDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory, SessionSynchronization sessionSynchronization) { return doGetMongoDatabase(dbName, factory, sessionSynchronization); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java index 33caa5e7fe..d01364b202 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java @@ -15,16 +15,15 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ReactiveMongoTemplate; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import com.mongodb.reactivestreams.client.ClientSession; /** * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveMongoTransactionManager} binds - * instances of this class to the subscriber context. - *
+ * instances of this class to the subscriber context.
* Note: Intended for internal usage only. * * @author Mark Paluch @@ -103,8 +102,7 @@ boolean hasSession() { * @param session * @return */ - @Nullable - public ClientSession setSessionIfAbsent(@Nullable ClientSession session) { + public @Nullable ClientSession setSessionIfAbsent(@Nullable ClientSession session) { if (!hasSession()) { setSession(session); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java index 2c65c26b79..4f293c8ed6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java @@ -17,8 +17,8 @@ import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionSystemException; @@ -64,7 +64,7 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean { private @Nullable ReactiveMongoDatabaseFactory databaseFactory; - private @Nullable MongoTransactionOptions options; + private MongoTransactionOptions options; private final MongoTransactionOptionsResolver transactionOptionsResolver; /** @@ -79,7 +79,9 @@ public class ReactiveMongoTransactionManager extends AbstractReactiveTransaction * @see #setDatabaseFactory(ReactiveMongoDatabaseFactory) */ public ReactiveMongoTransactionManager() { + this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver(); + this.options = MongoTransactionOptions.NONE; } /** @@ -98,7 +100,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact * starting a new transaction. * * @param databaseFactory must not be {@literal null}. - * @param options can be {@literal null}. + * @param options can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if {@literal null}. */ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory, @Nullable TransactionOptions options) { @@ -112,7 +114,8 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact * * @param databaseFactory must not be {@literal null}. * @param transactionOptionsResolver must not be {@literal null}. - * @param defaultTransactionOptions can be {@literal null}. + * @param defaultTransactionOptions can be {@literal null}. Will default {@link MongoTransactionOptions#NONE} if + * {@literal null}. * @since 4.3 */ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory, @@ -124,7 +127,7 @@ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFact this.databaseFactory = databaseFactory; this.transactionOptionsResolver = transactionOptionsResolver; - this.options = defaultTransactionOptions; + this.options = defaultTransactionOptions != null ? defaultTransactionOptions : MongoTransactionOptions.NONE; } @Override @@ -318,8 +321,7 @@ public void setOptions(@Nullable TransactionOptions options) { * * @return can be {@literal null}. */ - @Nullable - public ReactiveMongoDatabaseFactory getDatabaseFactory() { + public @Nullable ReactiveMongoDatabaseFactory getDatabaseFactory() { return databaseFactory; } @@ -470,8 +472,7 @@ void closeSession() { } } - @Nullable - public ClientSession getSession() { + public @Nullable ClientSession getSession() { return resourceHolder != null ? resourceHolder.getSession() : null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java index 93dbf5db69..ec30478a54 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java @@ -22,8 +22,8 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodClassKey; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -34,8 +34,7 @@ /** * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having - * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base. - *
+ * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
* The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself * like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them * if not already proxied. @@ -95,13 +94,13 @@ public SessionAwareMethodInterceptor(ClientSession session, T target, Class< this.sessionType = sessionType; } - @Nullable @Override - public Object invoke(MethodInvocation methodInvocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable { if (requiresDecoration(methodInvocation.getMethod())) { Object target = methodInvocation.proceed(); + Assert.notNull(target, "invocation target was null"); if (target instanceof Proxy) { return target; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java index b52fc0bd71..5c50ba686a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.Function; @@ -41,10 +41,10 @@ class SimpleMongoTransactionOptions implements MongoTransactionOptions { static final Set KNOWN_KEYS = Arrays.stream(OptionKey.values()).map(OptionKey::getKey) .collect(Collectors.toSet()); - private final Duration maxCommitTime; - private final ReadConcern readConcern; - private final ReadPreference readPreference; - private final WriteConcern writeConcern; + private final @Nullable Duration maxCommitTime; + private final @Nullable ReadConcern readConcern; + private final @Nullable ReadPreference readPreference; + private final @Nullable WriteConcern writeConcern; static SimpleMongoTransactionOptions of(Map options) { return new SimpleMongoTransactionOptions(options); @@ -58,27 +58,23 @@ private SimpleMongoTransactionOptions(Map options) { this.writeConcern = doGetWriteConcern(options); } - @Nullable @Override - public Duration getMaxCommitTime() { + public @Nullable Duration getMaxCommitTime() { return maxCommitTime; } - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return readConcern; } - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return readPreference; } - @Nullable @Override - public WriteConcern getWriteConcern() { + public @Nullable WriteConcern getWriteConcern() { return writeConcern; } @@ -89,8 +85,7 @@ public String toString() { + ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}'; } - @Nullable - private static Duration doGetMaxCommitTime(Map options) { + private static @Nullable Duration doGetMaxCommitTime(Map options) { return getValue(options, OptionKey.MAX_COMMIT_TIME, value -> { @@ -100,18 +95,15 @@ private static Duration doGetMaxCommitTime(Map options) { }); } - @Nullable - private static ReadConcern doGetReadConcern(Map options) { + private static @Nullable ReadConcern doGetReadConcern(Map options) { return getValue(options, OptionKey.READ_CONCERN, value -> new ReadConcern(ReadConcernLevel.fromString(value))); } - @Nullable - private static ReadPreference doGetReadPreference(Map options) { + private static @Nullable ReadPreference doGetReadPreference(Map options) { return getValue(options, OptionKey.READ_PREFERENCE, ReadPreference::valueOf); } - @Nullable - private static WriteConcern doGetWriteConcern(Map options) { + private static @Nullable WriteConcern doGetWriteConcern(Map options) { return getValue(options, OptionKey.WRITE_CONCERN, value -> { @@ -123,8 +115,8 @@ private static WriteConcern doGetWriteConcern(Map options) { }); } - @Nullable - private static T getValue(Map options, OptionKey key, Function convertFunction) { + private static @Nullable T getValue(Map options, OptionKey key, + Function convertFunction) { String value = options.get(key.getKey()); return value != null ? convertFunction.apply(value) : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java index cd5f58d5b1..57ecec0342 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java @@ -17,7 +17,7 @@ import java.time.Duration; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * MongoDB-specific transaction metadata. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java index 37c7e3686b..e42c26d95a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.TransactionDefinition; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java index bec05d0d68..69ec086e5a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; public class UncategorizedMongoDbException extends UncategorizedDataAccessException { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java index 2fe27a2c9e..86a70600a8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java @@ -17,11 +17,11 @@ import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReactiveWrappers.ReactiveLibrary; import org.springframework.data.util.TypeUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java index a33f20ffb6..4b7aa10c3f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; import org.springframework.core.ResolvableType; import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor; import org.springframework.data.mongodb.MongoManagedTypes; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java index 538fe4e812..f2442960ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java @@ -15,10 +15,11 @@ */ package org.springframework.data.mongodb.aot; -import static org.springframework.data.mongodb.aot.MongoAotPredicates.*; +import static org.springframework.data.mongodb.aot.MongoAotPredicates.isReactorPresent; import java.util.Arrays; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.MongoClientSettings; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java index b070a0190f..0f6ba01704 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.ConnectionString; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java index 164b4defb6..f3a7dc0437 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -56,7 +58,6 @@ import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -76,6 +77,7 @@ * @author Zied Yaich * @author Tomasz Forys */ +@NullUnmarked public class MappingMongoConverterParser implements BeanDefinitionParser { private static final String BASE_PACKAGE = "base-package"; @@ -157,8 +159,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { return null; } - @Nullable - private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) { String disableValidation = element.getAttribute("disable-validation"); boolean validationDisabled = StringUtils.hasText(disableValidation) && Boolean.parseBoolean(disableValidation); @@ -291,8 +292,7 @@ private static void parseFieldNamingStrategy(Element element, ReaderContext cont } } - @Nullable - private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition getCustomConversions(Element element, ParserContext parserContext) { List customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters"); @@ -354,8 +354,7 @@ private static Set getInitialEntityClasses(Element element) { return classes; } - @Nullable - public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) { + public @Nullable BeanMetadataElement parseConverter(Element element, ParserContext parserContext) { String converterRef = element.getAttribute("ref"); if (StringUtils.hasText(converterRef)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java index 4e05fe6c39..a304199776 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java @@ -18,6 +18,8 @@ import static org.springframework.data.config.ParsingUtils.*; import static org.springframework.data.mongodb.config.BeanNames.*; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -29,7 +31,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -42,6 +43,7 @@ * @author Oliver Gierke * @author Mark Paluch */ +@NullUnmarked public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser { private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono", diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java index 0594f6176c..b01827d8c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java @@ -35,6 +35,7 @@ import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -52,7 +53,7 @@ public abstract class MongoConfigurationSupport { /** * Return the name of the database to connect to. * - * @return must not be {@literal null}. + * @return never {@literal null}. */ protected abstract String getDatabaseName(); @@ -76,7 +77,7 @@ protected Collection getMappingBasePackages() { * Creates a {@link MongoMappingContext} equipped with entity classes scanned from the mapping base package. * * @see #getMappingBasePackages() - * @return + * @return never {@literal null}. */ @Bean public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions, @@ -172,8 +173,10 @@ protected Set> scanForEntities(String basePackage) throws ClassNotFound for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) { - initialEntitySet - .add(ClassUtils.forName(candidate.getBeanClassName(), MongoConfigurationSupport.class.getClassLoader())); + String beanClassName = candidate.getBeanClassName(); + Assert.notNull(beanClassName, "BeanClassName cannot be null"); + + initialEntitySet.add(ClassUtils.forName(beanClassName, MongoConfigurationSupport.class.getClassLoader())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java index b8f23a35af..93d778c861 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java @@ -26,7 +26,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java index 2e733cc79f..2d3649c53a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java @@ -20,6 +20,8 @@ import java.util.Set; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -31,7 +33,6 @@ import org.springframework.data.config.BeanComponentDefinitionBuilder; import org.springframework.data.mongodb.core.MongoClientFactoryBean; import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.w3c.dom.Element; @@ -47,6 +48,7 @@ * @author Viktor Khoroshko * @author Mark Paluch */ +@NullUnmarked public class MongoDbFactoryParser extends AbstractBeanDefinitionParser { private static final Set MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES = Set.of("id", "write-concern"); @@ -125,8 +127,7 @@ private BeanDefinition registerMongoBeanDefinition(Element element, ParserContex * @param parserContext * @return {@literal null} in case no client-/uri defined. */ - @Nullable - private BeanDefinition getConnectionString(Element element, ParserContext parserContext) { + private @Nullable BeanDefinition getConnectionString(Element element, ParserContext parserContext) { String type = null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java index 95b56b58f3..00e993fdc8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java @@ -19,6 +19,7 @@ import java.util.Map; +import org.jspecify.annotations.NullUnmarked; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.CustomEditorConfigurer; import org.springframework.beans.factory.support.BeanDefinitionBuilder; @@ -40,6 +41,7 @@ * @author Christoph Strobl * @author Mark Paluch */ +@NullUnmarked abstract class MongoParsingUtils { private MongoParsingUtils() {} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java index 1e1b11356f..5053e540fe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java @@ -18,6 +18,7 @@ import static org.springframework.data.config.ParsingUtils.*; import static org.springframework.data.mongodb.config.MongoParsingUtils.*; +import org.jspecify.annotations.NullUnmarked; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; @@ -37,6 +38,7 @@ * @author Martin Baumgartner * @author Oliver Gierke */ +@NullUnmarked class MongoTemplateParser extends AbstractBeanDefinitionParser { @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java index 60bf126ae7..3f5cb0ca62 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java index 5ed9b66619..f24c435348 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java @@ -17,10 +17,10 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; - import com.mongodb.ReadPreference; +import org.jspecify.annotations.Nullable; + /** * Parse a {@link String} to a {@link ReadPreference}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java index 9c51900902..9ff59e5b22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -80,11 +80,10 @@ public void setAsText(@Nullable String replicaSetString) { * @param source * @return the */ - @Nullable - private ServerAddress parseServerAddress(String source) { + private @Nullable ServerAddress parseServerAddress(String source) { if (!StringUtils.hasText(source)) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source)); } return null; @@ -93,7 +92,7 @@ private ServerAddress parseServerAddress(String source) { String[] hostAndPort = extractHostAddressAndPort(source.trim()); if (hostAndPort.length > 2) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source)); } return null; @@ -105,11 +104,11 @@ private ServerAddress parseServerAddress(String source) { return port == null ? new ServerAddress(hostAddress) : new ServerAddress(hostAddress, port); } catch (UnknownHostException e) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "host", hostAndPort[0])); } } catch (NumberFormatException e) { - if(LOG.isWarnEnabled()) { + if (LOG.isWarnEnabled()) { LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "port", hostAndPort[1])); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java index b777969967..23c15102ac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java @@ -18,7 +18,7 @@ import java.beans.PropertyEditorSupport; import org.bson.UuidRepresentation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java index ee0d09e555..32c19e24c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java @@ -17,7 +17,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java index 5a1e5b725e..555cc9f66e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java @@ -1,6 +1,6 @@ /** * Spring XML namespace configuration for MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.config; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java index a00d95a9ad..ec7c368eaf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java @@ -18,7 +18,7 @@ import java.util.List; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; @@ -30,7 +30,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * Utility methods to map {@link org.springframework.data.mongodb.core.aggregation.Aggregation} pipeline definitions and diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java index 17b8835b7e..8a74ace28b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java @@ -21,9 +21,9 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.messaging.Message; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -78,8 +78,7 @@ public ChangeStreamEvent(@Nullable ChangeStreamDocument raw, Class * * @return can be {@literal null}. */ - @Nullable - public ChangeStreamDocument getRaw() { + public @Nullable ChangeStreamDocument getRaw() { return raw; } @@ -88,10 +87,10 @@ public ChangeStreamDocument getRaw() { * * @return can be {@literal null}. */ - @Nullable - public Instant getTimestamp() { + public @Nullable Instant getTimestamp() { - return getBsonTimestamp() != null ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) + return getBsonTimestamp() != null && raw != null + ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class) : null; } @@ -111,8 +110,7 @@ public BsonTimestamp getBsonTimestamp() { * * @return can be {@literal null}. */ - @Nullable - public BsonValue getResumeToken() { + public @Nullable BsonValue getResumeToken() { return raw != null ? raw.getResumeToken() : null; } @@ -121,8 +119,7 @@ public BsonValue getResumeToken() { * * @return can be {@literal null}. */ - @Nullable - public OperationType getOperationType() { + public @Nullable OperationType getOperationType() { return raw != null ? raw.getOperationType() : null; } @@ -131,8 +128,7 @@ public OperationType getOperationType() { * * @return can be {@literal null}. */ - @Nullable - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return raw != null ? raw.getNamespace().getDatabaseName() : null; } @@ -141,8 +137,7 @@ public String getDatabaseName() { * * @return can be {@literal null}. */ - @Nullable - public String getCollectionName() { + public @Nullable String getCollectionName() { return raw != null ? raw.getNamespace().getCollectionName() : null; } @@ -152,8 +147,7 @@ public String getCollectionName() { * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocument()} is * {@literal null}. */ - @Nullable - public T getBody() { + public @Nullable T getBody() { if (raw == null || raw.getFullDocument() == null) { return null; @@ -163,14 +157,14 @@ public T getBody() { } /** - * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being changed. + * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being + * changed. * * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocumentBeforeChange()} is * {@literal null}. * @since 4.0 */ - @Nullable - public T getBodyBeforeChange() { + public @Nullable T getBodyBeforeChange() { if (raw == null || raw.getFullDocumentBeforeChange() == null) { return null; @@ -189,6 +183,7 @@ private T getConvertedFullDocument(Document fullDocument) { return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_UPDATER); } + @SuppressWarnings("NullAway") private Object doGetConverted(Document fullDocument, AtomicReferenceFieldUpdater updater) { Object result = updater.get(this); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java index aaee3b76af..9c99b0e01f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java @@ -23,9 +23,10 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -248,6 +249,7 @@ private ChangeStreamOptionsBuilder() {} * @param collation must not be {@literal null} nor {@literal empty}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder collation(Collation collation) { Assert.notNull(collation, "Collation must not be null nor empty"); @@ -257,14 +259,12 @@ public ChangeStreamOptionsBuilder collation(Collation collation) { } /** - * Set the filter to apply. - *
+ * Set the filter to apply.
* Fields on aggregation expression root level are prefixed to map to fields contained in * {@link ChangeStreamDocument#getFullDocument() fullDocument}. However {@literal operationType}, {@literal ns}, * {@literal documentKey} and {@literal fullDocument} are reserved words that will be omitted, and therefore taken * as given, during the mapping procedure. You may want to have a look at the - * structure of Change Events. - *
+ * structure of Change Events.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to ensure filter expressions are * mapped to domain type fields. * @@ -272,6 +272,7 @@ public ChangeStreamOptionsBuilder collation(Collation collation) { * {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder filter(Aggregation filter) { Assert.notNull(filter, "Filter must not be null"); @@ -286,6 +287,7 @@ public ChangeStreamOptionsBuilder filter(Aggregation filter) { * @param filter must not be {@literal null} nor contain {@literal null} values. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder filter(Document... filter) { Assert.noNullElements(filter, "Filter must not contain null values"); @@ -301,6 +303,7 @@ public ChangeStreamOptionsBuilder filter(Document... filter) { * @param resumeToken must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeToken(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -330,6 +333,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentOnUpdate() { * @param lookup must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) { Assert.notNull(lookup, "Lookup must not be null"); @@ -345,6 +349,7 @@ public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) { * @return this. * @since 4.0 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) { Assert.notNull(lookup, "Lookup must not be null"); @@ -358,7 +363,7 @@ public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBef * * @return this. * @since 4.0 - * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) + * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) */ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() { return fullDocumentBeforeChangeLookup(FullDocumentBeforeChange.WHEN_AVAILABLE); @@ -370,6 +375,7 @@ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() { * @param resumeTimestamp must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) { Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null"); @@ -385,6 +391,7 @@ public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) { Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null"); @@ -400,6 +407,7 @@ public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) { resumeToken(resumeToken); @@ -415,6 +423,7 @@ public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) { resumeToken(resumeToken); @@ -426,6 +435,7 @@ public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) { /** * @return the built {@link ChangeStreamOptions} */ + @Contract("-> new") public ChangeStreamOptions build() { ChangeStreamOptions options = new ChangeStreamOptions(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java index c142aca173..bf8be5ba69 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java @@ -16,8 +16,8 @@ package org.springframework.data.mongodb.core; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import com.mongodb.MongoException; import com.mongodb.client.MongoCollection; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index 5df30e0b92..da1cdfa335 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -245,7 +245,7 @@ public CollectionOptions collation(@Nullable Collation collation) { * @since 2.1 */ public CollectionOptions schema(MongoJsonSchema schema) { - return validator(Validator.schema(schema)); + return validator(schema != null ? Validator.schema(schema) : null); } /** @@ -574,6 +574,7 @@ public static ValidationOptions none() { * @param validator can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validator(@Nullable Validator validator) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -584,6 +585,7 @@ public ValidationOptions validator(@Nullable Validator validator) { * @param validationLevel can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validationLevel(ValidationLevel validationLevel) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -594,6 +596,7 @@ public ValidationOptions validationLevel(ValidationLevel validationLevel) { * @param validationAction can be {@literal null}. * @return new instance of {@link ValidationOptions}. */ + @Contract("_ -> new") public ValidationOptions validationAction(ValidationAction validationAction) { return new ValidationOptions(validator, validationLevel, validationAction); } @@ -950,6 +953,7 @@ public static TimeSeriesOptions timeSeries(String timeField) { * @param metaField must not be {@literal null}. * @return new instance of {@link TimeSeriesOptions}. */ + @Contract("_ -> new") public TimeSeriesOptions metaField(String metaField) { return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter); } @@ -961,6 +965,7 @@ public TimeSeriesOptions metaField(String metaField) { * @return new instance of {@link TimeSeriesOptions}. * @see Granularity */ + @Contract("_ -> new") public TimeSeriesOptions granularity(GranularityDefinition granularity) { return new TimeSeriesOptions(timeField, metaField, granularity, expireAfter); } @@ -973,6 +978,7 @@ public TimeSeriesOptions granularity(GranularityDefinition granularity) { * @see com.mongodb.client.model.CreateCollectionOptions#expireAfter(long, java.util.concurrent.TimeUnit) * @since 4.4 */ + @Contract("_ -> new") public TimeSeriesOptions expireAfter(Duration ttl) { return new TimeSeriesOptions(timeField, metaField, granularity, ttl); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java index 644a3a54d1..bdf0b90ee3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionPreparerSupport.java @@ -21,6 +21,7 @@ import java.util.function.Function; import org.bson.Document; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -84,7 +85,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { for (Object aware : sources) { if (aware instanceof ReadConcernAware rca && rca.hasReadConcern()) { @@ -108,7 +109,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { for (Object aware : sources) { if (aware instanceof ReadPreferenceAware rpa && rpa.hasReadPreference()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java index 4fa6b3e97d..11d9f09afd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CountQuery.java @@ -23,9 +23,9 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.MetricConversion; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -154,7 +154,7 @@ private Collection rewriteCollection(Collection source) { * @param $and potentially existing {@code $and} condition. * @return the rewritten query {@link Document}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) private static Document createGeoWithin(String key, Document source, @Nullable Object $and) { boolean spheric = source.containsKey("$nearSphere"); @@ -233,6 +233,7 @@ private static boolean containsNearWithMinDistance(Document source) { return source.containsKey("$minDistance"); } + @SuppressWarnings("NullAway") private static Object toCenterCoordinates(Object value) { if (ObjectUtils.isArray(value)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java index 9b7408b0cf..3b53cef8d0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CursorPreparer.java @@ -18,7 +18,7 @@ import java.util.function.Function; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -76,8 +76,7 @@ default FindIterable initiateFind(MongoCollection collection * @since 2.2 */ @Override - @Nullable - default ReadPreference getReadPreference() { + default @Nullable ReadPreference getReadPreference() { return null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java index 9d588ad16d..f450bddb30 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DbCallback.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import com.mongodb.MongoException; import com.mongodb.client.MongoDatabase; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java index 52343522a7..8bc5349e61 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultBulkOperations.java @@ -21,6 +21,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.dao.DataIntegrityViolationException; @@ -40,7 +41,7 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.util.Pair; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.MongoBulkWriteException; @@ -115,6 +116,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) { } @Override + @Contract("_ -> this") public BulkOperations insert(Object document) { Assert.notNull(document, "Document must not be null"); @@ -127,6 +129,7 @@ public BulkOperations insert(Object document) { } @Override + @Contract("_ -> this") public BulkOperations insert(List documents) { Assert.notNull(documents, "Documents must not be null"); @@ -137,6 +140,7 @@ public BulkOperations insert(List documents) { } @Override + @Contract("_, _ -> this") public BulkOperations updateOne(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -146,6 +150,7 @@ public BulkOperations updateOne(Query query, UpdateDefinition update) { } @Override + @Contract("_ -> this") public BulkOperations updateOne(List> updates) { Assert.notNull(updates, "Updates must not be null"); @@ -158,6 +163,7 @@ public BulkOperations updateOne(List> updates) { } @Override + @Contract("_, _ -> this") public BulkOperations updateMulti(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -169,6 +175,7 @@ public BulkOperations updateMulti(Query query, UpdateDefinition update) { } @Override + @Contract("_ -> this") public BulkOperations updateMulti(List> updates) { Assert.notNull(updates, "Updates must not be null"); @@ -181,11 +188,13 @@ public BulkOperations updateMulti(List> updates) { } @Override + @Contract("_, _ -> this") public BulkOperations upsert(Query query, UpdateDefinition update) { return update(query, update, true, true); } @Override + @Contract("_ -> this") public BulkOperations upsert(List> updates) { for (Pair update : updates) { @@ -196,6 +205,7 @@ public BulkOperations upsert(List> updates) { } @Override + @Contract("_ -> this") public BulkOperations remove(Query query) { Assert.notNull(query, "Query must not be null"); @@ -209,6 +219,7 @@ public BulkOperations remove(Query query) { } @Override + @Contract("_ -> this") public BulkOperations remove(List removes) { Assert.notNull(removes, "Removals must not be null"); @@ -221,6 +232,7 @@ public BulkOperations remove(List removes) { } @Override + @Contract("_, _, _ -> this") public BulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) { Assert.notNull(query, "Query must not be null"); @@ -412,7 +424,7 @@ public boolean skipEventPublishing() { return eventPublisher == null; } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public T callback(Class callbackType, T entity, String collectionName) { if (skipEntityCallbacks()) { @@ -422,7 +434,7 @@ public T callback(Class callbackType, T entity, St return entityCallbacks.callback(callbackType, entity, collectionName); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public T callback(Class callbackType, T entity, Document document, String collectionName) { @@ -433,6 +445,7 @@ public T callback(Class callbackType, T entity, Do return entityCallbacks.callback(callbackType, entity, document, collectionName); } + @SuppressWarnings("NullAway") public void publishEvent(ApplicationEvent event) { if (skipEventPublishing()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 2057e2f046..24d22bd80a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.UncategorizedMongoDbException; @@ -28,7 +29,6 @@ import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -115,6 +115,7 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection } @Override + @SuppressWarnings("NullAway") public String ensureIndex(IndexDefinition indexDefinition) { return execute(collection -> { @@ -131,8 +132,7 @@ public String ensureIndex(IndexDefinition indexDefinition) { }); } - @Nullable - private MongoPersistentEntity lookupPersistentEntity(@Nullable Class entityType, String collection) { + private @Nullable MongoPersistentEntity lookupPersistentEntity(@Nullable Class entityType, String collection) { if (entityType != null) { return mapper.getMappingContext().getRequiredPersistentEntity(entityType); @@ -160,6 +160,7 @@ public void dropIndex(String name) { } @Override + @SuppressWarnings("NullAway") public void alterIndex(String name, org.springframework.data.mongodb.core.index.IndexOptions options) { Document indexOptions = new Document("name", name); @@ -180,6 +181,7 @@ public void dropAllIndexes() { } @Override + @SuppressWarnings("NullAway") public List getIndexInfo() { return execute(new CollectionCallback>() { @@ -208,8 +210,7 @@ private List getIndexData(MongoCursor cursor) { }); } - @Nullable - public T execute(CollectionCallback callback) { + public @Nullable T execute(CollectionCallback callback) { Assert.notNull(callback, "CollectionCallback must not be null"); @@ -228,6 +229,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source mapper.getMappedSort((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); } + @SuppressWarnings("NullAway") private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, @Nullable MongoPersistentEntity entity) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java index e2471dbb14..a34c1fb945 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperationsProvider.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.IndexOperations; @@ -43,7 +44,7 @@ class DefaultIndexOperationsProvider implements IndexOperationsProvider { } @Override - public IndexOperations indexOps(String collectionName, Class type) { + public IndexOperations indexOps(String collectionName, @Nullable Class type) { return new DefaultIndexOperations(mongoDbFactory, collectionName, mapper, type); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java index 59b7ccd63e..92c6a957dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveBulkOperations.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.springframework.lang.Contract; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -24,6 +25,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.mapping.callback.EntityCallback; @@ -40,7 +42,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.WriteConcern; @@ -107,6 +108,7 @@ void setDefaultWriteConcern(@Nullable WriteConcern defaultWriteConcern) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations insert(Object document) { Assert.notNull(document, "Document must not be null"); @@ -120,6 +122,7 @@ public ReactiveBulkOperations insert(Object document) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations insert(List documents) { Assert.notNull(documents, "Documents must not be null"); @@ -130,6 +133,7 @@ public ReactiveBulkOperations insert(List documents) { } @Override + @Contract("_, _, _ -> this") public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -140,6 +144,7 @@ public ReactiveBulkOperations updateOne(Query query, UpdateDefinition update) { } @Override + @Contract("_, _ -> this") public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update) { Assert.notNull(query, "Query must not be null"); @@ -150,11 +155,13 @@ public ReactiveBulkOperations updateMulti(Query query, UpdateDefinition update) } @Override + @Contract("_, _ -> this") public ReactiveBulkOperations upsert(Query query, UpdateDefinition update) { return update(query, update, true, true); } @Override + @Contract("_ -> this") public ReactiveBulkOperations remove(Query query) { Assert.notNull(query, "Query must not be null"); @@ -169,6 +176,7 @@ public ReactiveBulkOperations remove(Query query) { } @Override + @Contract("_ -> this") public ReactiveBulkOperations remove(List removes) { Assert.notNull(removes, "Removals must not be null"); @@ -181,6 +189,7 @@ public ReactiveBulkOperations remove(List removes) { } @Override + @Contract("_, _, _ -> this") public ReactiveBulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options) { Assert.notNull(query, "Query must not be null"); @@ -359,7 +368,7 @@ public boolean skipEventPublishing() { return eventPublisher == null; } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public Mono callback(Class callbackType, T entity, String collectionName) { if (skipEntityCallbacks()) { @@ -369,7 +378,7 @@ public Mono callback(Class callbackType, T enti return entityCallbacks.callback(callbackType, entity, collectionName); } - @SuppressWarnings("rawtypes") + @SuppressWarnings({ "rawtypes", "NullAway" }) public Mono callback(Class callbackType, T entity, Document document, String collectionName) { @@ -380,6 +389,7 @@ public Mono callback(Class callbackType, T enti return entityCallbacks.callback(callbackType, entity, document, collectionName); } + @SuppressWarnings("NullAway") public void publishEvent(ApplicationEvent event) { if (skipEventPublishing()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java index 8e78f421f4..69ade2e163 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultReactiveIndexOperations.java @@ -19,16 +19,15 @@ import reactor.core.publisher.Mono; import java.util.Collection; -import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; import org.springframework.data.mongodb.core.index.ReactiveIndexOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; @@ -48,7 +47,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { private final ReactiveMongoOperations mongoOperations; private final String collectionName; private final QueryMapper queryMapper; - private final Optional> type; + private final @Nullable Class type; /** * Creates a new {@link DefaultReactiveIndexOperations}. @@ -59,7 +58,7 @@ public class DefaultReactiveIndexOperations implements ReactiveIndexOperations { */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, QueryMapper queryMapper) { - this(mongoOperations, collectionName, queryMapper, Optional.empty()); + this(mongoOperations, collectionName, queryMapper, null); } /** @@ -71,12 +70,7 @@ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, S * @param type used for mapping potential partial index filter expression, must not be {@literal null}. */ public DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, - QueryMapper queryMapper, Class type) { - this(mongoOperations, collectionName, queryMapper, Optional.of(type)); - } - - private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, String collectionName, - QueryMapper queryMapper, Optional> type) { + QueryMapper queryMapper, @Nullable Class type) { Assert.notNull(mongoOperations, "ReactiveMongoOperations must not be null"); Assert.notNull(collectionName, "Collection must not be null"); @@ -89,13 +83,12 @@ private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, } @Override + @SuppressWarnings("NullAway") public Mono ensureIndex(IndexDefinition indexDefinition) { return mongoOperations.execute(collectionName, collection -> { - MongoPersistentEntity entity = type - .map(val -> (MongoPersistentEntity) queryMapper.getMappingContext().getRequiredPersistentEntity(val)) - .orElseGet(() -> lookupPersistentEntity(collectionName)); + MongoPersistentEntity entity = getConfiguredEntity(); IndexOptions indexOptions = IndexConverters.indexDefinitionToIndexOptionsConverter().convert(indexDefinition); @@ -124,8 +117,7 @@ public Mono alterIndex(String name, org.springframework.data.mongodb.core. }).then(); } - @Nullable - private MongoPersistentEntity lookupPersistentEntity(String collection) { + private @Nullable MongoPersistentEntity lookupPersistentEntity(String collection) { Collection> entities = queryMapper.getMappingContext().getPersistentEntities(); @@ -152,6 +144,14 @@ public Flux getIndexInfo() { .map(IndexConverters.documentToIndexInfoConverter()::convert); } + private @Nullable MongoPersistentEntity getConfiguredEntity() { + + if (type != null) { + return queryMapper.getMappingContext().getRequiredPersistentEntity(type); + } + return lookupPersistentEntity(collectionName); + } + private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document sourceOptions, @Nullable MongoPersistentEntity entity) { @@ -164,6 +164,7 @@ private IndexOptions addPartialFilterIfPresent(IndexOptions ops, Document source queryMapper.getMappedObject((Document) sourceOptions.get(PARTIAL_FILTER_EXPRESSION_KEY), entity)); } + @SuppressWarnings("NullAway") private static IndexOptions addDefaultCollationIfRequired(IndexOptions ops, @Nullable MongoPersistentEntity entity) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java index b236b4df28..6dde79e0e8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultScriptOperations.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.core; -import static java.util.UUID.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static java.util.UUID.randomUUID; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import java.util.ArrayList; import java.util.Arrays; @@ -28,7 +28,8 @@ import org.bson.Document; import org.bson.types.ObjectId; -import org.springframework.dao.DataAccessException; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.script.ExecutableMongoScript; import org.springframework.data.mongodb.core.script.NamedMongoScript; @@ -38,8 +39,6 @@ import org.springframework.util.StringUtils; import com.mongodb.BasicDBList; -import com.mongodb.MongoException; -import com.mongodb.client.MongoDatabase; /** * Default implementation of {@link ScriptOperations} capable of saving and executing {@link ExecutableMongoScript}. @@ -51,6 +50,7 @@ * @deprecated since 2.2. The {@code eval} command has been removed in MongoDB Server 4.2.0. */ @Deprecated +@NullUnmarked class DefaultScriptOperations implements ScriptOperations { private static final String SCRIPT_COLLECTION_NAME = "system.js"; @@ -85,38 +85,28 @@ public NamedMongoScript register(NamedMongoScript script) { } @Override - public Object execute(ExecutableMongoScript script, Object... args) { + public @Nullable Object execute(ExecutableMongoScript script, Object... args) { Assert.notNull(script, "Script must not be null"); - return mongoOperations.execute(new DbCallback() { + return mongoOperations.execute(db -> { - @Override - public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException { - - Document command = new Document("$eval", script.getCode()); - BasicDBList commandArgs = new BasicDBList(); - commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args))); - command.append("args", commandArgs); - return db.runCommand(command).get("retval"); - } + Document command = new Document("$eval", script.getCode()); + BasicDBList commandArgs = new BasicDBList(); + commandArgs.addAll(Arrays.asList(convertScriptArgs(false, args))); + command.append("args", commandArgs); + return db.runCommand(command).get("retval"); }); } @Override - public Object call(String scriptName, Object... args) { + public @Nullable Object call(String scriptName, Object... args) { Assert.hasText(scriptName, "ScriptName must not be null or empty"); - return mongoOperations.execute(new DbCallback() { - - @Override - public Object doInDB(MongoDatabase db) throws MongoException, DataAccessException { - - return db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args)))) - .get("retval"); - } - }); + return mongoOperations.execute( + db -> db.runCommand(new Document("eval", String.format("%s(%s)", scriptName, convertAndJoinScriptArgs(args)))) + .get("retval")); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java index 8b4de14e05..c445e06f8a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultWriteConcernResolver.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; + import com.mongodb.WriteConcern; /** @@ -26,7 +28,7 @@ enum DefaultWriteConcernResolver implements WriteConcernResolver { INSTANCE; - public WriteConcern resolve(MongoAction action) { + public @Nullable WriteConcern resolve(MongoAction action) { return action.getDefaultWriteConcern(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java index 94352ad65c..ad3c2b8564 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityLifecycleEventDelegate.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; -import org.springframework.lang.Nullable; /** * Delegate class to encapsulate lifecycle event configuration and publishing. @@ -47,6 +47,7 @@ public void setEventsEnabled(boolean eventsEnabled) { * * @param event the application event. */ + @SuppressWarnings("NullAway") public void publishEvent(Object event) { if (canPublishEvent()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java index 38269787cb..1327656356 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityOperations.java @@ -26,9 +26,11 @@ import org.bson.BsonNull; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; +import org.springframework.core.env.StandardEnvironment; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.expression.ValueEvaluationContext; @@ -65,7 +67,6 @@ import org.springframework.data.projection.TargetAware; import org.springframework.data.util.Optionals; import org.springframework.expression.spel.support.SimpleEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.LinkedMultiValueMap; @@ -423,6 +424,7 @@ interface Entity { * * @return */ + @Nullable Object getId(); /** @@ -528,10 +530,9 @@ interface AdaptibleEntity extends Entity { * Populates the identifier of the backing entity if it has an identifier property and there's no identifier * currently present. * - * @param id must not be {@literal null}. + * @param id can be {@literal null}. * @return */ - @Nullable T populateIdIfNecessary(@Nullable Object id); /** @@ -574,12 +575,12 @@ public String getIdFieldName() { } @Override - public Object getId() { + public @Nullable Object getId() { return getPropertyValue(ID_FIELD); } @Override - public Object getPropertyValue(String key) { + public @Nullable Object getPropertyValue(String key) { return map.get(key); } @@ -588,7 +589,6 @@ public Query getByIdQuery() { return Query.query(Criteria.where(ID_FIELD).is(map.get(ID_FIELD))); } - @Nullable @Override public T populateIdIfNecessary(@Nullable Object id) { @@ -615,8 +615,7 @@ public T initializeVersionProperty() { } @Override - @Nullable - public Number getVersion() { + public @Nullable Number getVersion() { return null; } @@ -733,7 +732,7 @@ public Object getId() { } @Override - public Object getPropertyValue(String key) { + public @Nullable Object getPropertyValue(String key) { return propertyAccessor.getProperty(entity.getRequiredPersistentProperty(key)); } @@ -800,8 +799,7 @@ public boolean isVersionedEntity() { } @Override - @Nullable - public Object getVersion() { + public @Nullable Object getVersion() { return propertyAccessor.getProperty(entity.getRequiredVersionProperty()); } @@ -849,7 +847,6 @@ public Map extractKeys(Document sortObject, Class sourceType) return keyset; } - @Nullable private Object getNestedPropertyValue(String key) { String[] segments = key.split("\\."); @@ -862,6 +859,10 @@ private Object getNestedPropertyValue(String key) { currentValue = currentEntity.getPropertyValue(segment); if (i < segments.length - 1) { + if (currentValue == null) { + return BsonNull.VALUE; + } + currentEntity = entityOperations.forEntity(currentValue); } } @@ -898,7 +899,6 @@ private static AdaptibleEntity of(T bean, new ConvertingPropertyAccessor<>(propertyAccessor, conversionService), entityOperations); } - @Nullable @Override public T populateIdIfNecessary(@Nullable Object id) { @@ -920,8 +920,7 @@ public T populateIdIfNecessary(@Nullable Object id) { } @Override - @Nullable - public Number getVersion() { + public @Nullable Number getVersion() { MongoPersistentProperty versionProperty = entity.getRequiredVersionProperty(); @@ -1137,7 +1136,7 @@ public TimeSeriesOptions mapTimeSeriesOptions(TimeSeriesOptions source) { @Override public String getIdKeyName() { - return entity.getIdProperty().getName(); + return entity.getIdProperty() != null ? entity.getIdProperty().getName() : ID_FIELD; } private String mappedNameOrDefault(String name) { @@ -1157,7 +1156,8 @@ private ValueEvaluationContext getEvaluationContextForEntity(@Nullable Persisten return mongoEntity.getValueEvaluationContext(null); } - return ValueEvaluationContext.of(this.environment, SimpleEvaluationContext.forReadOnlyDataBinding().build()); + return ValueEvaluationContext.of(this.environment != null ? this.environment : new StandardEnvironment(), + SimpleEvaluationContext.forReadOnlyDataBinding().build()); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java index ca5aa7a513..d28afada0a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java @@ -17,6 +17,7 @@ import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; @@ -55,11 +56,11 @@ static class ExecutableAggregationSupport private final MongoTemplate template; private final Class domainType; - private final Aggregation aggregation; - private final String collection; + private final @Nullable Aggregation aggregation; + private final @Nullable String collection; - public ExecutableAggregationSupport(MongoTemplate template, Class domainType, Aggregation aggregation, - String collection) { + public ExecutableAggregationSupport(MongoTemplate template, Class domainType, @Nullable Aggregation aggregation, + @Nullable String collection) { this.template = template; this.domainType = domainType; this.aggregation = aggregation; @@ -84,21 +85,25 @@ public TerminatingAggregation by(Aggregation aggregation) { @Override public AggregationResults all() { + + Assert.notNull(aggregation, "Aggregation must be set first"); return template.aggregate(aggregation, getCollectionName(aggregation), domainType); } @Override public Stream stream() { + + Assert.notNull(aggregation, "Aggregation must be set first"); return template.aggregateStream(aggregation, getCollectionName(aggregation), domainType); } - private String getCollectionName(Aggregation aggregation) { + private String getCollectionName(@Nullable Aggregation aggregation) { if (StringUtils.hasText(collection)) { return collection; } - if (aggregation instanceof TypedAggregation typedAggregation) { + if (aggregation instanceof TypedAggregation typedAggregation) { if (typedAggregation.getInputType() != null) { return template.getCollectionName(typedAggregation.getInputType()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java index 3358ff2b17..21cb37ae86 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java @@ -19,6 +19,7 @@ import java.util.Optional; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.ScrollPosition; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; import com.mongodb.client.MongoCollection; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java index 4e6c3547c5..af8c80903a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java @@ -16,18 +16,17 @@ package org.springframework.data.mongodb.core; import java.util.List; -import java.util.Optional; import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -53,6 +52,7 @@ class ExecutableFindOperationSupport implements ExecutableFindOperation { } @Override + @Contract("_ -> new") public ExecutableFind query(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -84,6 +84,7 @@ static class ExecutableFindSupport } @Override + @Contract("_ -> new") public FindWithProjection inCollection(String collection) { Assert.hasText(collection, "Collection name must not be null nor empty"); @@ -92,6 +93,7 @@ public FindWithProjection inCollection(String collection) { } @Override + @Contract("_ -> new") public FindWithQuery as(Class returnType) { Assert.notNull(returnType, "ReturnType must not be null"); @@ -100,6 +102,7 @@ public FindWithQuery as(Class returnType) { } @Override + @Contract("_ -> new") public TerminatingFind matching(Query query) { Assert.notNull(query, "Query must not be null"); @@ -108,7 +111,7 @@ public TerminatingFind matching(Query query) { } @Override - public T oneValue() { + public @Nullable T oneValue() { List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(2)); @@ -124,7 +127,7 @@ public T oneValue() { } @Override - public T firstValue() { + public @Nullable T firstValue() { List result = doFind(new DelegatingQueryCursorPreparer(getCursorPreparer(query, null)).limit(1)); @@ -206,10 +209,10 @@ private String asString() { * @author Christoph Strobl * @since 2.0 */ - static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer { + static class DelegatingQueryCursorPreparer implements CursorPreparer { private final @Nullable CursorPreparer delegate; - private Optional limit = Optional.empty(); + private int limit = -1; DelegatingQueryCursorPreparer(@Nullable CursorPreparer delegate) { this.delegate = delegate; @@ -219,25 +222,22 @@ static class DelegatingQueryCursorPreparer implements SortingQueryCursorPreparer public FindIterable prepare(FindIterable iterable) { FindIterable target = delegate != null ? delegate.prepare(iterable) : iterable; - return limit.map(target::limit).orElse(target); + if (limit >= 0) { + target.limit(limit); + } + return target; } + @Contract("_ -> this") CursorPreparer limit(int limit) { - this.limit = Optional.of(limit); + this.limit = limit; return this; } @Override - @Nullable - public ReadPreference getReadPreference() { - return delegate.getReadPreference(); - } - - @Override - @Nullable - public Document getSortObject() { - return delegate instanceof SortingQueryCursorPreparer sqcp ? sqcp.getSortObject() : null; + public @Nullable ReadPreference getReadPreference() { + return delegate != null ? delegate.getReadPreference() : null; } } @@ -258,6 +258,7 @@ public DistinctOperationSupport(ExecutableFindSupport delegate, String field) @Override @SuppressWarnings("unchecked") + @Contract("_ -> new") public TerminatingDistinct as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); @@ -266,6 +267,7 @@ public TerminatingDistinct as(Class resultType) { } @Override + @Contract("_ -> new") public TerminatingDistinct matching(Query query) { Assert.notNull(query, "Query must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java index 47b7127deb..599a910035 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableInsertOperationSupport.java @@ -18,8 +18,9 @@ import java.util.ArrayList; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.BulkOperations.BulkMode; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ class ExecutableInsertOperationSupport implements ExecutableInsertOperation { } @Override + @Contract("_ -> new") public ExecutableInsert insert(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -56,10 +58,11 @@ static class ExecutableInsertSupport implements ExecutableInsert { private final MongoTemplate template; private final Class domainType; - @Nullable private final String collection; - @Nullable private final BulkMode bulkMode; + private final @Nullable String collection; + private final @Nullable BulkMode bulkMode; - ExecutableInsertSupport(MongoTemplate template, Class domainType, String collection, BulkMode bulkMode) { + ExecutableInsertSupport(MongoTemplate template, Class domainType, @Nullable String collection, + @Nullable BulkMode bulkMode) { this.template = template; this.domainType = domainType; @@ -93,6 +96,7 @@ public BulkWriteResult bulk(Collection objects) { } @Override + @Contract("_ -> new") public InsertWithBulkMode inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -101,6 +105,7 @@ public InsertWithBulkMode inCollection(String collection) { } @Override + @Contract("_ -> new") public TerminatingBulkInsert withBulkMode(BulkMode bulkMode) { Assert.notNull(bulkMode, "BulkMode must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java index 9f78693540..55864cbd8e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableMapReduceOperationSupport.java @@ -17,9 +17,10 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,6 +47,7 @@ class ExecutableMapReduceOperationSupport implements ExecutableMapReduceOperatio * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation#mapReduce(java.lang.Class) */ @Override + @Contract("_ -> new") public ExecutableMapReduceSupport mapReduce(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -89,6 +91,7 @@ static class ExecutableMapReduceSupport * @see in org.springframework.data.mongodb.core.ExecutableMapReduceOperation.TerminatingMapReduce#all() */ @Override + @SuppressWarnings("NullAway") public List all() { return template.mapReduce(query, domainType, getCollectionName(), mapFunction, reduceFunction, options, returnType); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index 8e84aa7dd6..e53e80b10f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -17,8 +17,9 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -42,6 +43,7 @@ public ExecutableRemoveOperationSupport(MongoTemplate tempate) { } @Override + @Contract("_ -> new") public ExecutableRemove remove(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -60,7 +62,8 @@ static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWi private final Query query; @Nullable private final String collection; - public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, String collection) { + public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, + @Nullable String collection) { this.template = template; this.domainType = domainType; this.query = query; @@ -68,6 +71,7 @@ public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Quer } @Override + @Contract("_ -> new") public RemoveWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -76,6 +80,7 @@ public RemoveWithQuery inCollection(String collection) { } @Override + @Contract("_ -> new") public TerminatingRemove matching(Query query) { Assert.notNull(query, "Query must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index a5c63e9b67..69365459ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -17,12 +17,12 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import com.mongodb.client.result.UpdateResult; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index 593d863d39..75756c6f1e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -15,9 +15,10 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -41,6 +42,7 @@ class ExecutableUpdateOperationSupport implements ExecutableUpdateOperation { } @Override + @Contract("_ -> new") public ExecutableUpdate update(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); @@ -52,6 +54,7 @@ public ExecutableUpdate update(Class domainType) { * @author Christoph Strobl * @since 2.0 */ + @SuppressWarnings("rawtypes") static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace, FindAndReplaceWithProjection { @@ -66,9 +69,9 @@ static class ExecutableUpdateSupport @Nullable private final Object replacement; private final Class targetType; - ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, UpdateDefinition update, - String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions, - Object replacement, Class targetType) { + ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, + @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType) { this.template = template; this.domainType = domainType; @@ -82,6 +85,7 @@ static class ExecutableUpdateSupport } @Override + @Contract("_ -> new") public TerminatingUpdate apply(UpdateDefinition update) { Assert.notNull(update, "Update must not be null"); @@ -91,6 +95,7 @@ public TerminatingUpdate apply(UpdateDefinition update) { } @Override + @Contract("_ -> new") public UpdateWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -100,6 +105,7 @@ public UpdateWithQuery inCollection(String collection) { } @Override + @Contract("_ -> new") public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { Assert.notNull(options, "Options must not be null"); @@ -109,6 +115,7 @@ public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { } @Override + @Contract("_ -> new") public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null"); @@ -118,6 +125,7 @@ public FindAndReplaceWithProjection replaceWith(T replacement) { } @Override + @Contract("_ -> new") public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options) { Assert.notNull(options, "Options must not be null"); @@ -127,6 +135,7 @@ public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options } @Override + @Contract("_ -> new") public TerminatingReplace withOptions(ReplaceOptions options) { FindAndReplaceOptions target = new FindAndReplaceOptions(); @@ -138,6 +147,7 @@ public TerminatingReplace withOptions(ReplaceOptions options) { } @Override + @Contract("_ -> new") public UpdateWithUpdate matching(Query query) { Assert.notNull(query, "Query must not be null"); @@ -147,6 +157,7 @@ public UpdateWithUpdate matching(Query query) { } @Override + @Contract("_ -> new") public FindAndReplaceWithOptions as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); @@ -171,6 +182,7 @@ public UpdateResult upsert() { } @Override + @SuppressWarnings("NullAway") public @Nullable T findAndModifyValue() { return template.findAndModify(query, update, @@ -179,6 +191,7 @@ public UpdateResult upsert() { } @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public @Nullable T findAndReplaceValue() { return (T) template.findAndReplace(query, replacement, @@ -187,6 +200,7 @@ public UpdateResult upsert() { } @Override + @SuppressWarnings({ "unchecked", "NullAway" }) public UpdateResult replaceFirst() { if (replacement != null) { @@ -198,6 +212,7 @@ public UpdateResult replaceFirst() { findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName()); } + @SuppressWarnings("NullAway") private UpdateResult doUpdate(boolean multi, boolean upsert) { return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java index 51a2c5b86a..6e9b775324 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndModifyOptions.java @@ -17,8 +17,9 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * @author Mark Pollak @@ -99,16 +100,19 @@ public static FindAndModifyOptions of(@Nullable FindAndModifyOptions source) { return options; } + @Contract("_ -> this") public FindAndModifyOptions returnNew(boolean returnNew) { this.returnNew = returnNew; return this; } + @Contract("_ -> this") public FindAndModifyOptions upsert(boolean upsert) { this.upsert = upsert; return this; } + @Contract("_ -> this") public FindAndModifyOptions remove(boolean remove) { this.remove = remove; return this; @@ -121,6 +125,7 @@ public FindAndModifyOptions remove(boolean remove) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public FindAndModifyOptions collation(@Nullable Collation collation) { this.collation = collation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java index 266a0742c2..2005ba3c6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindAndReplaceOptions.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.springframework.lang.Contract; + /** * Options for * findOneAndReplace. @@ -95,6 +97,7 @@ public static FindAndReplaceOptions empty() { * * @return this. */ + @Contract("-> this") public FindAndReplaceOptions returnNew() { this.returnNew = true; @@ -106,6 +109,7 @@ public FindAndReplaceOptions returnNew() { * * @return this. */ + @Contract("-> this") public FindAndReplaceOptions upsert() { super.upsert(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java index 625a85950e..f04417325c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/FindPublisherPreparer.java @@ -18,7 +18,7 @@ import java.util.function.Function; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -76,8 +76,7 @@ default FindPublisher initiateFind(MongoCollection collectio * @since 2.2 */ @Override - @Nullable - default ReadPreference getReadPreference() { + default @Nullable ReadPreference getReadPreference() { return null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java index 57abe9a529..043613122a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/HintFunction.java @@ -18,9 +18,9 @@ import java.util.function.Function; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java index da4766343a..cd9ba90453 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappedDocument.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -70,7 +71,7 @@ public boolean hasNonNullId() { return hasId() && document.get(ID_FIELD) != null; } - public Object getId() { + public @Nullable Object getId() { return document.get(ID_FIELD); } @@ -86,7 +87,7 @@ public Bson getIdFilter() { return new Document(ID_FIELD, document.get(ID_FIELD)); } - public Object get(String key) { + public @Nullable Object get(String key) { return document.get(key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java index bc26dfb68c..396ae1ce8a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MappingMongoJsonSchemaCreator.java @@ -24,7 +24,7 @@ import java.util.stream.Collectors; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; @@ -46,6 +46,7 @@ import org.springframework.data.mongodb.core.schema.QueryCharacteristics; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject; import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -94,6 +95,7 @@ class MappingMongoJsonSchemaCreator implements MongoJsonSchemaCreator { } @Override + @Contract("_ -> new") public MongoJsonSchemaCreator filter(Predicate filter) { return new MappingMongoJsonSchemaCreator(converter, mappingContext, filter, mergeProperties); } @@ -111,6 +113,7 @@ public PropertySpecifier property(String path) { * @return new instance of {@link MongoJsonSchemaCreator}. * @since 3.4 */ + @Contract("_, _ -> new") public MongoJsonSchemaCreator withTypesFor(String path, Class... types) { LinkedMultiValueMap> clone = mergeProperties.clone(); @@ -183,6 +186,7 @@ private List computePropertiesForEntity(List path) { String stringPath = path.stream().map(MongoPersistentProperty::getName).collect(Collectors.joining(".")); @@ -373,7 +377,9 @@ private TypedJsonSchemaObject createSchemaObject(Object type, Collection poss return schemaObject; } - private String computePropertyFieldName(PersistentProperty property) { + private String computePropertyFieldName(@Nullable PersistentProperty property) { + + Assert.notNull(property, "Property must not be null"); return property instanceof MongoPersistentProperty mongoPersistentProperty ? mongoPersistentProperty.getFieldName() : property.getName(); @@ -445,7 +451,8 @@ public MongoPersistentProperty getProperty() { } @Override - public MongoPersistentEntity resolveEntity(MongoPersistentProperty property) { + @SuppressWarnings("unchecked") + public @Nullable MongoPersistentEntity resolveEntity(MongoPersistentProperty property) { return (MongoPersistentEntity) mappingContext.getPersistentEntity(property); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java index fdfeaa81ad..c827c5b8a9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoAction.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.WriteConcern; @@ -72,28 +72,23 @@ public String getCollectionName() { return collectionName; } - @Nullable - public WriteConcern getDefaultWriteConcern() { + public @Nullable WriteConcern getDefaultWriteConcern() { return defaultWriteConcern; } - @Nullable - public Class getEntityType() { + public @Nullable Class getEntityType() { return entityType; } - @Nullable - public MongoActionOperation getMongoActionOperation() { + public @Nullable MongoActionOperation getMongoActionOperation() { return mongoActionOperation; } - @Nullable - public Document getQuery() { + public @Nullable Document getQuery() { return query; } - @Nullable - public Document getDocument() { + public @Nullable Document getDocument() { return document; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java index c5fee9cf54..9210dd85ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientFactoryBean.java @@ -24,11 +24,11 @@ import java.util.stream.Collectors; import org.bson.UuidRepresentation; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.SpringDataMongoDB; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -78,7 +78,7 @@ public void setMongoClientSettings(@Nullable MongoClientSettings mongoClientOpti * * @param credential can be {@literal null}. */ - public void setCredential(@Nullable MongoCredential[] credential) { + public void setCredential(MongoCredential @Nullable[] credential) { this.credential = Arrays.asList(credential); } @@ -119,8 +119,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce } @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } @@ -316,13 +315,13 @@ private void applySettings(Consumer settingsBuilder, @Nullable T value) { settingsBuilder.accept(value); } - private T computeSettingsValue(Function function, S defaultValueHolder, S settingsValueHolder, + private @Nullable T computeSettingsValue(Function function, S defaultValueHolder, S settingsValueHolder, @Nullable T connectionStringValue) { return computeSettingsValue(function.apply(defaultValueHolder), function.apply(settingsValueHolder), connectionStringValue); } - private T computeSettingsValue(T defaultValue, T fromSettings, T fromConnectionString) { + private @Nullable T computeSettingsValue(@Nullable T defaultValue, T fromSettings, @Nullable T fromConnectionString) { boolean fromSettingsIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromSettings); boolean fromConnectionStringIsDefault = ObjectUtils.nullSafeEquals(defaultValue, fromConnectionString); @@ -337,7 +336,7 @@ private MongoClient createMongoClient(MongoClientSettings settings) throws Unkno return MongoClients.create(settings, SpringDataMongoDB.driverInformation()); } - private String getOrDefault(Object value, String defaultValue) { + private String getOrDefault(@Nullable Object value, String defaultValue) { if(value == null) { return defaultValue; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java index eab6b5d7f4..0a62b7aa49 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoDatabaseFactorySupport.java @@ -15,12 +15,13 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.SessionAwareMethodInterceptor; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -132,6 +133,7 @@ public void destroy() throws Exception { } @Override + @Contract("_ -> new") public MongoDatabaseFactory withSession(ClientSession session) { return new MongoDatabaseFactorySupport.ClientSessionBoundMongoDbFactory(session, this); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java index 7aef5a3a82..f361b19bba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoEncryptionSettingsFactoryBean.java @@ -19,8 +19,8 @@ import java.util.Map; import org.bson.BsonDocument; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import com.mongodb.AutoEncryptionSettings; import com.mongodb.MongoClientSettings; @@ -34,11 +34,11 @@ public class MongoEncryptionSettingsFactoryBean implements FactoryBean { private boolean bypassAutoEncryption; - private String keyVaultNamespace; - private Map extraOptions; - private MongoClientSettings keyVaultClientSettings; - private Map> kmsProviders; - private Map schemaMap; + private @Nullable String keyVaultNamespace; + private @Nullable Map extraOptions; + private @Nullable MongoClientSettings keyVaultClientSettings; + private @Nullable Map> kmsProviders; + private @Nullable Map schemaMap; /** * @param bypassAutoEncryption diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index 1ec7d3ffc0..2bde873c2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -18,7 +18,7 @@ import java.util.Set; import org.bson.BsonInvalidOperationException; - +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.dao.DataIntegrityViolationException; @@ -31,7 +31,6 @@ import org.springframework.data.mongodb.TransientClientSessionException; import org.springframework.data.mongodb.UncategorizedMongoDbException; import org.springframework.data.mongodb.util.MongoDbErrorCodes; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.MongoBulkWriteException; @@ -69,12 +68,12 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator private static final Set SECURITY_EXCEPTIONS = Set.of("MongoCryptException"); @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return doTranslateException(ex); } @Nullable + @SuppressWarnings("NullAway") DataAccessException doTranslateException(RuntimeException ex) { // Check for well-known MongoException subclasses. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java index 66b1cf209e..84c395bf2f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoJsonSchemaCreator.java @@ -139,8 +139,7 @@ interface JsonSchemaPropertyContext { * @return {@literal null} if the property is not an entity. It is nevertheless recommend to check * {@link PersistentProperty#isEntity()} first. */ - @Nullable - MongoPersistentEntity resolveEntity(MongoPersistentProperty property); + @Nullable MongoPersistentEntity resolveEntity(MongoPersistentProperty property); } @@ -162,6 +161,7 @@ public boolean test(JsonSchemaPropertyContext context) { return extracted(context.getProperty(), context); } + @SuppressWarnings("NullAway") private boolean extracted(MongoPersistentProperty property, JsonSchemaPropertyContext context) { if (property.isAnnotationPresent(Encrypted.class)) { return true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 65396bc7fe..7fdd157528 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Window; @@ -49,7 +50,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -196,7 +196,8 @@ default SessionScoped withSession(Supplier sessionProvider) { private @Nullable ClientSession session; @Override - public T execute(SessionCallback action, Consumer onComplete) { + @SuppressWarnings("NullAway") + public @Nullable T execute(SessionCallback action, Consumer onComplete) { lock.executeWithoutResult(() -> { @@ -733,8 +734,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param entityClass the parametrized type of the returned list. * @return the converted object. */ - @Nullable - T findOne(Query query, Class entityClass); + @Nullable T findOne(Query query, Class entityClass); /** * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified @@ -750,8 +750,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param collectionName name of the collection to retrieve the objects from. * @return the converted object. */ - @Nullable - T findOne(Query query, Class entityClass, String collectionName); + @Nullable T findOne(Query query, Class entityClass, String collectionName); /** * Determine result of given {@link Query} contains at least one element.
@@ -871,8 +870,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param entityClass the type the document shall be converted into. Must not be {@literal null}. * @return the document with the given id mapped onto the given target class. */ - @Nullable - T findById(Object id, Class entityClass); + @Nullable T findById(Object id, Class entityClass); /** * Returns the document with the given id from the given collection mapped onto the given target class. @@ -882,8 +880,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin * @param collectionName the collection to query for the document. * @return he converted object or {@literal null} if document does not exist. */ - @Nullable - T findById(Object id, Class entityClass, String collectionName); + @Nullable T findById(Object id, Class entityClass, String collectionName); /** * Finds the distinct values for a specified {@literal field} across a single {@link MongoCollection} or view and @@ -960,8 +957,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, Class entityClass); + @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass); /** * Triggers findAndModify @@ -980,8 +976,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName); + @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName); /** * Triggers findAndModify @@ -1003,8 +998,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass); + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass); /** * Triggers findAndModify @@ -1027,8 +1021,7 @@ default List findDistinct(Query query, String field, String collection, C * @see Update * @see AggregationUpdate */ - @Nullable - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName); /** @@ -1048,8 +1041,7 @@ T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions o * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement) { + default @Nullable T findAndReplace(Query query, T replacement) { return findAndReplace(query, replacement, FindAndReplaceOptions.empty()); } @@ -1068,8 +1060,7 @@ default T findAndReplace(Query query, T replacement) { * @return the converted object that was updated or {@literal null}, if not found. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, String collectionName) { + default @Nullable T findAndReplace(Query query, T replacement, String collectionName) { return findAndReplace(query, replacement, FindAndReplaceOptions.empty(), collectionName); } @@ -1091,8 +1082,7 @@ default T findAndReplace(Query query, T replacement, String collectionName) * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options) { return findAndReplace(query, replacement, options, getCollectionName(ClassUtils.getUserClass(replacement))); } @@ -1112,8 +1102,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * as it is after the update. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, String collectionName) { Assert.notNull(replacement, "Replacement must not be null"); return findAndReplace(query, replacement, options, (Class) ClassUtils.getUserClass(replacement), collectionName); @@ -1137,8 +1126,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * as it is after the update. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, + default @Nullable T findAndReplace(Query query, T replacement, FindAndReplaceOptions options, Class entityType, String collectionName) { return findAndReplace(query, replacement, options, entityType, collectionName, entityType); @@ -1166,8 +1154,7 @@ default T findAndReplace(Query query, T replacement, FindAndReplaceOptions o * {@link #getCollectionName(Class) derived} from the given replacement value. * @since 2.1 */ - @Nullable - default T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + default @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, Class resultType) { return findAndReplace(query, replacement, options, entityType, @@ -1194,8 +1181,7 @@ default T findAndReplace(Query query, S replacement, FindAndReplaceOption * as it is after the update. * @since 2.1 */ - @Nullable - T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, + @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType); /** @@ -1211,8 +1197,7 @@ T findAndReplace(Query query, S replacement, FindAndReplaceOptions option * @param entityClass the parametrized type of the returned list. * @return the converted object */ - @Nullable - T findAndRemove(Query query, Class entityClass); + @Nullable T findAndRemove(Query query, Class entityClass); /** * Map the results of an ad-hoc query on the specified collection to a single instance of an object of the specified @@ -1229,8 +1214,7 @@ T findAndReplace(Query query, S replacement, FindAndReplaceOptions option * @param collectionName name of the collection to retrieve the objects from. * @return the converted object. */ - @Nullable - T findAndRemove(Query query, Class entityClass, String collectionName); + @Nullable T findAndRemove(Query query, Class entityClass, String collectionName); /** * Returns the number of documents for the given {@link Query} by querying the collection of the given entity class. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java index 37001faa4e..574c0c8931 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoServerApiFactoryBean.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.ServerApi; @@ -31,7 +31,7 @@ */ public class MongoServerApiFactoryBean implements FactoryBean { - private String version; + private @Nullable String version; private @Nullable Boolean deprecationErrors; private @Nullable Boolean strict; @@ -59,9 +59,8 @@ public void setStrict(@Nullable Boolean strict) { this.strict = strict; } - @Nullable @Override - public ServerApi getObject() throws Exception { + public @Nullable ServerApi getObject() throws Exception { Builder builder = ServerApi.builder().version(version()); @@ -81,6 +80,11 @@ public Class getObjectType() { } private ServerApiVersion version() { + + if(version == null) { + return ServerApiVersion.V1; + } + try { // lookup by name eg. 'V1' return ObjectUtils.caseInsensitiveValueOf(ServerApiVersion.values(), version); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index e0442ab684..5c7df76cc5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -15,12 +15,22 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.*; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; import java.util.stream.Collectors; @@ -31,6 +41,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -95,7 +106,18 @@ 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.mongodb.core.mapping.event.*; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.BasicQuery; @@ -110,7 +132,7 @@ import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -133,7 +155,21 @@ import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.*; +import com.mongodb.client.model.CountOptions; +import com.mongodb.client.model.CreateCollectionOptions; +import com.mongodb.client.model.CreateViewOptions; +import com.mongodb.client.model.DeleteOptions; +import com.mongodb.client.model.EstimatedDocumentCountOptions; +import com.mongodb.client.model.FindOneAndDeleteOptions; +import com.mongodb.client.model.FindOneAndReplaceOptions; +import com.mongodb.client.model.FindOneAndUpdateOptions; +import com.mongodb.client.model.ReturnDocument; +import com.mongodb.client.model.TimeSeriesGranularity; +import com.mongodb.client.model.TimeSeriesOptions; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.ValidationAction; +import com.mongodb.client.model.ValidationLevel; +import com.mongodb.client.model.ValidationOptions; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -342,7 +378,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return this.readPreference; } @@ -478,7 +514,7 @@ public Stream stream(Query query, Class entityType, String collectionN return doStream(query, entityType, collectionName, entityType); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Stream doStream(Query query, Class entityType, String collectionName, Class returnType) { Assert.notNull(query, "Query must not be null"); @@ -511,7 +547,7 @@ public String getCollectionName(Class entityClass) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(String jsonCommand) { Assert.hasText(jsonCommand, "JsonCommand must not be null nor empty"); @@ -520,7 +556,7 @@ public Document executeCommand(String jsonCommand) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(Document command) { Assert.notNull(command, "Command must not be null"); @@ -529,7 +565,7 @@ public Document executeCommand(Document command) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Document executeCommand(Document command, @Nullable ReadPreference readPreference) { Assert.notNull(command, "Command must not be null"); @@ -576,7 +612,7 @@ protected void executeQuery(Query query, String collectionName, DocumentCallback } @Override - public T execute(DbCallback action) { + public @Nullable T execute(DbCallback action) { Assert.notNull(action, "DbCallback must not be null"); @@ -589,14 +625,14 @@ public T execute(DbCallback action) { } @Override - public T execute(Class entityClass, CollectionCallback callback) { + public @Nullable T execute(Class entityClass, CollectionCallback callback) { Assert.notNull(entityClass, "EntityClass must not be null"); return execute(getCollectionName(entityClass), callback); } @Override - public T execute(String collectionName, CollectionCallback callback) { + public @Nullable T execute(String collectionName, CollectionCallback callback) { Assert.notNull(collectionName, "CollectionName must not be null"); Assert.notNull(callback, "CollectionCallback must not be null"); @@ -618,6 +654,7 @@ public SessionScoped withSession(ClientSessionOptions options) { } @Override + @Contract("_ -> new") public MongoTemplate withSession(ClientSession session) { Assert.notNull(session, "ClientSession must not be null"); @@ -691,6 +728,7 @@ private MongoCollection createView(String name, String source, Aggrega return doCreateView(name, source, aggregation.getAggregationPipeline(), options); } + @SuppressWarnings("NullAway") protected MongoCollection doCreateView(String name, String source, List pipeline, @Nullable ViewOptions options) { @@ -706,8 +744,9 @@ protected MongoCollection doCreateView(String name, String source, Lis } @Override - @SuppressWarnings("ConstantConditions") - public MongoCollection getCollection(String collectionName) { + @SuppressWarnings({ "ConstantConditions", "NullAway" }) + @Contract("null -> fail") + public MongoCollection getCollection(@Nullable String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null"); @@ -720,7 +759,7 @@ public boolean collectionExists(Class entityClass) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public boolean collectionExists(String collectionName) { Assert.notNull(collectionName, "CollectionName must not be null"); @@ -854,7 +893,7 @@ public boolean exists(Query query, String collectionName) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public boolean exists(Query query, @Nullable Class entityClass, String collectionName) { if (query == null) { @@ -956,7 +995,7 @@ public List findDistinct(Query query, String field, Class entityClass, } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "NullAway" }) public List findDistinct(Query query, String field, String collectionName, Class entityClass, Class resultClass) { @@ -1074,20 +1113,22 @@ public T findAndModify(Query query, UpdateDefinition update, Class entity @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName) { + public T findAndModify(Query query, UpdateDefinition update, Class entityClass, + String collectionName) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName); } @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass) { + public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass) { return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, - String collectionName) { + public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); @@ -1111,8 +1152,8 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp } @Override - public T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, - String collectionName, Class resultType) { + public @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1223,6 +1264,7 @@ public long estimatedCount(String collectionName) { return doEstimatedCount(CollectionPreparerDelegate.of(this), collectionName, new EstimatedDocumentCountOptions()); } + @SuppressWarnings("NullAway") protected long doEstimatedCount(CollectionPreparer> collectionPreparer, String collectionName, EstimatedDocumentCountOptions options) { return execute(collectionName, @@ -1240,6 +1282,7 @@ public long exactCount(Query query, @Nullable Class entityClass, String colle return doExactCount(createDelegate(query), collectionName, mappedQuery, options); } + @SuppressWarnings("NullAway") protected long doExactCount(CollectionPreparer> collectionPreparer, String collectionName, Document filter, CountOptions options) { return execute(collectionName, collection -> collectionPreparer.prepare(collection) @@ -1543,7 +1586,7 @@ protected T doSave(String collectionName, T objectToSave, MongoWriter wri return maybeCallAfterSave(saved, dbDoc, collectionName); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Object insertDocument(String collectionName, Document document, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1597,6 +1640,7 @@ protected List insertDocumentList(String collectionName, List return MappedDocument.toIds(documents); } + @SuppressWarnings("NullAway") protected Object saveDocument(String collectionName, Document dbDoc, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1695,7 +1739,7 @@ public UpdateResult updateMulti(Query query, UpdateDefinition update, Class e return doUpdate(collectionName, query, update, entityClass, false, true); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected UpdateResult doUpdate(String collectionName, Query query, UpdateDefinition update, @Nullable Class entityClass, boolean upsert, boolean multi) { @@ -1805,7 +1849,7 @@ public DeleteResult remove(Query query, Class entityClass, String collectionN return doRemove(collectionName, query, entityClass, true); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected DeleteResult doRemove(String collectionName, Query query, @Nullable Class entityClass, boolean multi) { @@ -2126,6 +2170,7 @@ protected UpdateResult replace(Query query, Class entityType, T replac * @param entityClass * @return */ + @SuppressWarnings("NullAway") protected List doFindAndDelete(String collectionName, Query query, Class entityClass) { List result = find(query, entityClass, collectionName); @@ -2159,7 +2204,7 @@ private AggregationResults doAggregate(Aggregation aggregation, String co return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext()); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { @@ -2242,7 +2287,7 @@ protected AggregationResults doAggregate(Aggregation aggregation, String }); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Stream aggregateStream(Aggregation aggregation, String collectionName, Class outputType, @Nullable AggregationOperationContext context) { @@ -2357,7 +2402,7 @@ protected String replaceWithResourceIfNecessary(String function) { } @Override - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) public Set getCollectionNames() { return execute(db -> { Set result = new LinkedHashSet<>(); @@ -2441,7 +2486,7 @@ protected MongoCollection doCreateCollection(String collectionName, Do * @return the collection that was created * @since 3.3.3 */ - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected MongoCollection doCreateCollection(String collectionName, CreateCollectionOptions collectionOptions) { @@ -2520,8 +2565,9 @@ private CreateCollectionOptions getCreateCollectionOptions(Document document) { * @return the converted object or {@literal null} if none exists. */ @Nullable - protected T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, Class entityClass) { + protected T doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + Class entityClass) { return doFindOne(collectionName, collectionPreparer, query, fields, CursorPreparer.NO_OP_PREPARER, entityClass); } @@ -2540,8 +2586,9 @@ protected T doFindOne(String collectionName, CollectionPreparer T doFindOne(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, CursorPreparer preparer, Class entityClass) { + protected T doFindOne(String collectionName, + CollectionPreparer> collectionPreparer, Document query, Document fields, + CursorPreparer preparer, Class entityClass) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); @@ -2717,8 +2764,9 @@ Document getMappedValidator(Validator validator, Class domainType) { * @return the List of converted objects. */ @SuppressWarnings("ConstantConditions") - protected T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, Document query, - Document fields, Document sort, @Nullable Collation collation, Class entityClass) { + protected @Nullable T doFindAndRemove(CollectionPreparer collectionPreparer, String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, @Nullable Collation collation, + Class entityClass) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s", @@ -2733,8 +2781,8 @@ protected T doFindAndRemove(CollectionPreparer collectionPreparer, String co } @SuppressWarnings("ConstantConditions") - protected T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, Document query, - Document fields, Document sort, Class entityClass, UpdateDefinition update, + protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, @Nullable FindAndModifyOptions options) { if (options == null) { @@ -2779,9 +2827,10 @@ protected T doFindAndModify(CollectionPreparer collectionPreparer, String co * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, - Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, - Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { + protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + Document mappedQuery, Document mappedFields, Document mappedSort, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, Class resultType) { EntityProjection projection = operations.introspectProjection(resultType, entityType); @@ -2821,9 +2870,10 @@ CollectionPreparer> createCollectionPreparer(Query que * @since 3.4 */ @Nullable - private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, - Document mappedFields, Document mappedSort, @Nullable com.mongodb.client.model.Collation collation, - Class entityType, Document replacement, FindAndReplaceOptions options, EntityProjection projection) { + private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + Document mappedQuery, Document mappedFields, Document mappedSort, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, EntityProjection projection) { if (LOGGER.isDebugEnabled()) { LOGGER @@ -2839,6 +2889,7 @@ private T doFindAndReplace(CollectionPreparer collectionPreparer, String col collectionName); } + @SuppressWarnings("NullAway") private UpdateResult doReplace(ReplaceOptions options, Class entityType, String collectionName, UpdateContext updateContext, CollectionPreparer> collectionPreparer, Document replacement) { @@ -2999,13 +3050,12 @@ private Document getMappedSortObject(@Nullable Query query, Class type) { return getMappedSortObject(query.getSortObject(), type); } - @Nullable - private Document getMappedSortObject(Document sortObject, Class type) { + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class type) { return getMappedSortObject(sortObject, mappingContext.getPersistentEntity(type)); } - @Nullable - private Document getMappedSortObject(Document sortObject, @Nullable MongoPersistentEntity entity) { + + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, @Nullable MongoPersistentEntity entity) { if (ObjectUtils.isEmpty(sortObject)) { return null; @@ -3081,10 +3131,10 @@ private static class FindCallback implements CollectionCallback> collectionPreparer; private final Document query; private final Document fields; - private final @Nullable com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; public FindCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, @Nullable com.mongodb.client.model.Collation collation) { + Document fields, com.mongodb.client.model.@Nullable Collation collation) { Assert.notNull(query, "Query must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -3120,10 +3170,10 @@ private class ExistsCallback implements CollectionCallback { private final CollectionPreparer collectionPreparer; private final Document mappedQuery; - private final com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; ExistsCallback(CollectionPreparer collectionPreparer, Document mappedQuery, - com.mongodb.client.model.Collation collation) { + com.mongodb.client.model.@Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.mappedQuery = mappedQuery; @@ -3148,12 +3198,12 @@ private static class FindAndRemoveCallback implements CollectionCallback> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Optional collation; FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, @Nullable Collation collation) { + @Nullable Document fields, @Nullable Document sort, @Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3176,14 +3226,15 @@ private static class FindAndModifyCallback implements CollectionCallback> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + @Nullable Document fields, @Nullable Document sort, Object update, List arrayFilters, + FindAndModifyOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3237,11 +3288,11 @@ private static class FindAndReplaceCallback implements CollectionCallback> collectionPreparer, Document query, - Document fields, Document sort, Document update, @Nullable com.mongodb.client.model.Collation collation, + Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation, FindAndReplaceOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3347,10 +3398,6 @@ private class ProjectingReadCallback implements DocumentCallback { @SuppressWarnings("unchecked") public T doWith(Document document) { - if (document == null) { - return null; - } - maybeEmitEvent(new AfterLoadEvent<>(document, projection.getMappedType().getType(), collectionName)); Object entity = mongoConverter.project(projection, document); @@ -3595,8 +3642,6 @@ public void close() { throw potentiallyConvertRuntimeException(ex, exceptionTranslator); } finally { cursor = null; - exceptionTranslator = null; - objectReadCallback = null; } } } @@ -3628,7 +3673,7 @@ static class SessionBoundMongoTemplate extends MongoTemplate { } @Override - public MongoCollection getCollection(String collectionName) { + public MongoCollection getCollection(@Nullable String collectionName) { // native MongoDB objects that offer methods with ClientSession must not be proxied. return delegate.getCollection(collectionName); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java index 28ca85fbd7..4ae618eaa1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryOperations.java @@ -31,6 +31,7 @@ import org.bson.codecs.Codec; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PropertyPath; import org.springframework.data.mapping.PropertyReferenceException; import org.springframework.data.mapping.context.MappingContext; @@ -62,7 +63,7 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import com.mongodb.client.model.CountOptions; @@ -283,6 +284,7 @@ MappedDocument prepareId(Class type) { * @param * @return the {@link MappedDocument} containing the changes. */ + @SuppressWarnings("NullAway") MappedDocument prepareId(@Nullable MongoPersistentEntity entity) { if (entity == null || source.hasId()) { @@ -361,6 +363,7 @@ Document getMappedQuery(@Nullable MongoPersistentEntity entity) { return queryMapper.getMappedObject(getQueryObject(), entity); } + @SuppressWarnings("NullAway") Document getMappedFields(@Nullable MongoPersistentEntity entity, EntityProjection projection) { Document fields = evaluateFields(entity); @@ -888,6 +891,8 @@ Document getMappedShardKey(MongoPersistentEntity entity) { */ List getUpdatePipeline(@Nullable Class domainType) { + Assert.isInstanceOf(AggregationUpdate.class, update); + Class type = domainType != null ? domainType : Object.class; AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type, mappingContext, @@ -901,6 +906,7 @@ List getUpdatePipeline(@Nullable Class domainType) { * @param entity * @return */ + @SuppressWarnings("NullAway") Document getMappedUpdate(@Nullable MongoPersistentEntity entity) { if (update != null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java index 954fd61716..978aa9634f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import org.springframework.data.mongodb.core.aggregation.Aggregation; @@ -59,11 +60,11 @@ static class ReactiveAggregationSupport private final ReactiveMongoTemplate template; private final Class domainType; - private final Aggregation aggregation; - private final String collection; + private final @Nullable Aggregation aggregation; + private final @Nullable String collection; - ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, Aggregation aggregation, - String collection) { + ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, @Nullable Aggregation aggregation, + @Nullable String collection) { this.template = template; this.domainType = domainType; @@ -89,6 +90,9 @@ public TerminatingAggregationOperation by(Aggregation aggregation) { @Override public Flux all() { + + Assert.notNull(aggregation, "Aggregation must be set first"); + return template.aggregate(aggregation, getCollectionName(aggregation), domainType); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java index afeb6c5e0e..589f264f17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveChangeStreamOperationSupport.java @@ -24,11 +24,11 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.MatchOperation; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java index d1aec8af36..9445dbdadb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java @@ -19,6 +19,7 @@ import reactor.core.publisher.Mono; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; @@ -26,7 +27,6 @@ import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -67,10 +67,10 @@ static class ReactiveFindSupport private final ReactiveMongoTemplate template; private final Class domainType; private final Class returnType; - private final String collection; + private final @Nullable String collection; private final Query query; - ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, String collection, + ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, @Nullable String collection, Query query) { this.template = template; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java index 06d3c6eae7..9d424c2446 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveInsertOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -50,9 +51,9 @@ static class ReactiveInsertSupport implements ReactiveInsert { private final ReactiveMongoTemplate template; private final Class domainType; - private final String collection; + private final @Nullable String collection; - ReactiveInsertSupport(ReactiveMongoTemplate template, Class domainType, String collection) { + ReactiveInsertSupport(ReactiveMongoTemplate template, Class domainType, @Nullable String collection) { this.template = template; this.domainType = domainType; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java index 4f0d395950..4e3379bad0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupport.java @@ -17,9 +17,9 @@ import reactor.core.publisher.Flux; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -89,8 +89,11 @@ static class ReactiveMapReduceSupport @Override public Flux all() { + Assert.notNull(mapFunction, "MapFunction must be set first"); + Assert.notNull(reduceFunction, "ReduceFunction must be set first"); + return template.mapReduce(query, domainType, getCollectionName(), returnType, mapFunction, reduceFunction, - options); + options != null ? options : MapReduceOptions.options()); } /* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java index 89d1cd78ac..89caf3273c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoClientFactoryBean.java @@ -16,10 +16,10 @@ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.MongoClientSettings; @@ -89,7 +89,7 @@ public void setExceptionTranslator(@Nullable PersistenceExceptionTranslator exce } @Override - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return exceptionTranslator.translateExceptionIfPossible(ex); } @@ -124,7 +124,9 @@ protected MongoClient createInstance() throws Exception { @Override protected void destroyInstance(@Nullable MongoClient instance) throws Exception { - instance.close(); + if (instance != null) { + instance.close(); + } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 90f2d2345d..ebec41e3aa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -23,6 +23,7 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscription; import org.springframework.data.domain.KeysetScrollPosition; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 9b5af77f65..325a96dc85 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,9 +44,9 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -109,7 +109,18 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; -import org.springframework.data.mongodb.core.mapping.event.*; +import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; +import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; +import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; +import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; +import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; @@ -120,7 +131,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.Optionals; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -344,7 +355,8 @@ public void setWriteConcern(@Nullable WriteConcern writeConcern) { * @param writeConcernResolver can be {@literal null}. */ public void setWriteConcernResolver(@Nullable WriteConcernResolver writeConcernResolver) { - this.writeConcernResolver = writeConcernResolver; + this.writeConcernResolver = writeConcernResolver != null ? writeConcernResolver + : DefaultWriteConcernResolver.INSTANCE; } /** @@ -738,10 +750,11 @@ public Mono collectionExists(Class entityClass) { @Override public Mono collectionExists(String collectionName) { - return createMono(db -> Flux.from(db.listCollectionNames()) // - .filter(s -> s.equals(collectionName)) // - .map(s -> true) // - .single(false)); + return createMono( + db -> Flux.from(db.listCollectionNames()) // + .filter(s -> s.equals(collectionName)) // + .map(s -> true) // + .single(false)); } @Override @@ -898,7 +911,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass, new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback) - .collectList(); + .collectList(); return result.map(it -> ScrollUtils.createWindow(query, it, sourceClass, operations)); } @@ -906,7 +919,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), query.getFieldsObject(), sourceClass, new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback) - .collectList(); + .collectList(); return result.map( it -> ScrollUtils.createWindow(it, query.getLimit(), OffsetScrollPosition.positionFunction(query.getSkip()))); @@ -1145,6 +1158,7 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo } @Override + @SuppressWarnings("NullAway") public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType) { @@ -1350,6 +1364,7 @@ public Mono insert(T objectToSave, String collectionName) { return doInsert(collectionName, objectToSave, this.mongoConverter); } + @SuppressWarnings("NullAway") protected Mono doInsert(String collectionName, T objectToSave, MongoWriter writer) { return Mono.just(PersistableEntityModel.of(objectToSave, collectionName)) // @@ -1400,6 +1415,7 @@ public Flux insertAll(Mono> objectsToSa return Flux.from(objectsToSave).flatMapSequential(this::insertAll); } + @SuppressWarnings("NullAway") protected Flux doInsertAll(Collection listToSave, MongoWriter writer) { Map> elementsByCollection = new HashMap<>(); @@ -1416,6 +1432,7 @@ protected Flux doInsertAll(Collection listToSave, MongoWrite .concatMap(collectionName -> doInsertBatch(collectionName, elementsByCollection.get(collectionName), writer)); } + @SuppressWarnings("NullAway") protected Flux doInsertBatch(String collectionName, Collection batchToSave, MongoWriter writer) { @@ -1536,6 +1553,7 @@ private Mono doSaveVersioned(AdaptibleEntity source, String collection }); } + @SuppressWarnings("NullAway") protected Mono doSave(String collectionName, T objectToSave, MongoWriter writer) { assertUpdateableIdIfNotSet(objectToSave); @@ -1629,6 +1647,7 @@ private MongoCollection prepareCollection(MongoCollection co return collectionToUse; } + @SuppressWarnings("NullAway") protected Mono saveDocument(String collectionName, Document document, Class entityClass) { if (LOGGER.isDebugEnabled()) { @@ -1732,7 +1751,8 @@ public Mono updateMulti(Query query, UpdateDefinition update, Clas return doUpdate(collectionName, query, update, entityClass, false, true); } - protected Mono doUpdate(String collectionName, Query query, @Nullable UpdateDefinition update, + @SuppressWarnings("NullAway") + protected Mono doUpdate(String collectionName, Query query, UpdateDefinition update, @Nullable Class entityClass, boolean upsert, boolean multi) { MongoPersistentEntity entity = entityClass == null ? null : getPersistentEntity(entityClass); @@ -1814,7 +1834,8 @@ protected Mono doUpdate(String collectionName, Query query, @Nulla Document updateObj = updateContext.getMappedUpdate(entity); if (containsVersionProperty(queryObj, entity)) - throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s".formatted(entity.getName(), collectionName)); + throw new OptimisticLockingFailureException("Optimistic lock exception on saving entity %s to collection %s" + .formatted(entity.getName(), collectionName)); } } }); @@ -2012,18 +2033,18 @@ public Flux tail(Query query, Class entityClass) { @Override public Flux tail(@Nullable Query query, Class entityClass, String collectionName) { - ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); if (query == null) { LOGGER.debug(String.format("Tail for class: %s in collection: %s", entityClass, collectionName)); return executeFindMultiInternal( - collection -> new FindCallback(collectionPreparer, null).doInCollection(collection) + collection -> new FindCallback(CollectionPreparer.identity(), null).doInCollection(collection) .cursorType(CursorType.TailableAwait), FindPublisherPreparer.NO_OP_PREPARER, new ReadDocumentCallback<>(mongoConverter, entityClass, collectionName), collectionName); } + ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(query); return doFind(collectionName, collectionPreparer, query.getQueryObject(), query.getFieldsObject(), entityClass, new TailingQueryFindPublisherPreparer(query, entityClass)); } @@ -2382,8 +2403,8 @@ protected Flux doFind(String collectionName, serializeToJsonSafely(mappedQuery), mappedFields, entityClass, collectionName)); } - return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), preparer, - objectCallback, collectionName); + return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields), + preparer != null ? preparer : FindPublisherPreparer.NO_OP_PREPARER, objectCallback, collectionName); } CollectionPreparer> createCollectionPreparer(Query query) { @@ -2448,8 +2469,8 @@ protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable Col * @return the List of converted objects. */ protected Mono doFindAndRemove(String collectionName, - CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, - @Nullable Collation collation, Class entityClass) { + CollectionPreparer> collectionPreparer, Document query, Document fields, + @Nullable Document sort, @Nullable Collation collation, Class entityClass) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("findAndRemove using query: %s fields: %s sort: %s for class: %s in collection: %s", @@ -2464,8 +2485,8 @@ protected Mono doFindAndRemove(String collectionName, } protected Mono doFindAndModify(String collectionName, - CollectionPreparer> collectionPreparer, Document query, Document fields, Document sort, - Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { + CollectionPreparer> collectionPreparer, Document query, Document fields, + @Nullable Document sort, Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false); @@ -2481,8 +2502,7 @@ protected Mono doFindAndModify(String collectionName, LOGGER.debug(String.format( "findAndModify using query: %s fields: %s sort: %s for class: %s and update: %s " + "in collection: %s", serializeToJsonSafely(mappedQuery), fields, serializeToJsonSafely(sort), entityClass, - serializeToJsonSafely(mappedUpdate), - collectionName)); + serializeToJsonSafely(mappedUpdate), collectionName)); } return executeFindOneInternal( @@ -2659,8 +2679,7 @@ protected MongoDatabase prepareDatabase(MongoDatabase database) { * @see #setWriteConcern(WriteConcern) * @see #setWriteConcernResolver(WriteConcernResolver) */ - @Nullable - protected WriteConcern prepareWriteConcern(MongoAction mongoAction) { + protected @Nullable WriteConcern prepareWriteConcern(MongoAction mongoAction) { WriteConcern wc = writeConcernResolver.resolve(mongoAction); return potentiallyForceAcknowledgedWrite(wc); @@ -2679,7 +2698,7 @@ private WriteConcern potentiallyForceAcknowledgedWrite(@Nullable WriteConcern wc if (ObjectUtils.nullSafeEquals(WriteResultChecking.EXCEPTION, writeResultChecking)) { if (wc == null || wc.getWObject() == null - || (wc.getWObject()instanceof Number concern && concern.intValue() < 1)) { + || (wc.getWObject() instanceof Number concern && concern.intValue() < 1)) { return WriteConcern.ACKNOWLEDGED; } } @@ -2725,7 +2744,7 @@ private Mono executeFindOneInternal(ReactiveCollectionCallback * @return */ private Flux executeFindMultiInternal(ReactiveCollectionQueryCallback collectionCallback, - @Nullable FindPublisherPreparer preparer, DocumentCallback objectCallback, String collectionName) { + FindPublisherPreparer preparer, DocumentCallback objectCallback, String collectionName) { return createFlux(collectionName, collection -> { return Flux.from(preparer.initiateFind(collection, collectionCallback::doInCollection)) @@ -2764,8 +2783,7 @@ private static RuntimeException potentiallyConvertRuntimeException(RuntimeExcept return resolved == null ? ex : resolved; } - @Nullable - private MongoPersistentEntity getPersistentEntity(@Nullable Class type) { + private @Nullable MongoPersistentEntity getPersistentEntity(@Nullable Class type) { return type == null ? null : mappingContext.getPersistentEntity(type); } @@ -2785,8 +2803,8 @@ private MappingMongoConverter getDefaultMongoConverter() { return converter; } - @Nullable - private Document getMappedSortObject(Query query, Class type) { + @Contract("null, _ -> null") + private @Nullable Document getMappedSortObject(@Nullable Query query, Class type) { if (query == null) { return null; @@ -2795,8 +2813,8 @@ private Document getMappedSortObject(Query query, Class type) { return getMappedSortObject(query.getSortObject(), type); } - @Nullable - private Document getMappedSortObject(Document sortObject, Class type) { + @Contract("null, _ -> null") + private @Nullable Document getMappedSortObject(@Nullable Document sortObject, Class type) { if (ObjectUtils.isEmpty(sortObject)) { return null; @@ -2862,7 +2880,8 @@ private static class FindCallback implements ReactiveCollectionQueryCallback> collectionPreparer, Document query, Document fields) { + FindCallback(CollectionPreparer> collectionPreparer, @Nullable Document query, + @Nullable Document fields) { this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; @@ -2898,11 +2917,11 @@ private static class FindAndRemoveCallback implements ReactiveCollectionCallback private final CollectionPreparer> collectionPreparer; private final Document query; private final Document fields; - private final Document sort; + private final @Nullable Document sort; private final Optional collation; FindAndRemoveCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, @Nullable Collation collation) { + Document fields, @Nullable Document sort, @Nullable Collation collation) { this.collectionPreparer = collectionPreparer; this.query = query; this.fields = fields; @@ -2928,14 +2947,15 @@ private static class FindAndModifyCallback implements ReactiveCollectionCallback private final CollectionPreparer> collectionPreparer; private final Document query; - private final Document fields; - private final Document sort; + private final @Nullable Document fields; + private final @Nullable Document sort; private final Object update; private final List arrayFilters; private final FindAndModifyOptions options; FindAndModifyCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Object update, List arrayFilters, FindAndModifyOptions options) { + @Nullable Document fields, @Nullable Document sort, Object update, List arrayFilters, + FindAndModifyOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -2973,7 +2993,7 @@ public Publisher doInCollection(MongoCollection collection) } private static FindOneAndUpdateOptions convertToFindOneAndUpdateOptions(FindAndModifyOptions options, - Document fields, Document sort, List arrayFilters) { + @Nullable Document fields, @Nullable Document sort, List arrayFilters) { FindOneAndUpdateOptions result = new FindOneAndUpdateOptions(); @@ -3009,11 +3029,11 @@ private static class FindAndReplaceCallback implements ReactiveCollectionCallbac private final Document fields; private final Document sort; private final Document update; - private final @Nullable com.mongodb.client.model.Collation collation; + private final com.mongodb.client.model.@Nullable Collation collation; private final FindAndReplaceOptions options; FindAndReplaceCallback(CollectionPreparer> collectionPreparer, Document query, - Document fields, Document sort, Document update, com.mongodb.client.model.Collation collation, + Document fields, Document sort, Document update, com.mongodb.client.model.@Nullable Collation collation, FindAndReplaceOptions options) { this.collectionPreparer = collectionPreparer; this.query = query; @@ -3049,7 +3069,8 @@ private FindOneAndReplaceOptions convertToFindOneAndReplaceOptions(FindAndReplac } } - private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(Document fields, Document sort) { + private static FindOneAndDeleteOptions convertToFindOneAndDeleteOptions(@Nullable Document fields, + @Nullable Document sort) { FindOneAndDeleteOptions result = new FindOneAndDeleteOptions(); result = result.projection(fields).sort(sort); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java index 97c9cb0d0e..5c935ec628 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -54,9 +55,9 @@ static class ReactiveRemoveSupport implements ReactiveRemove, RemoveWithCo private final ReactiveMongoTemplate template; private final Class domainType; private final Query query; - private final String collection; + private final @Nullable String collection; - ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, String collection) { + ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable String collection) { this.template = template; this.domainType = domainType; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java index 51cd99dc93..75bfeef314 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java @@ -17,9 +17,9 @@ import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -57,16 +57,16 @@ static class ReactiveUpdateSupport private final ReactiveMongoTemplate template; private final Class domainType; private final Query query; - private final org.springframework.data.mongodb.core.query.UpdateDefinition update; - @Nullable private final String collection; - @Nullable private final FindAndModifyOptions findAndModifyOptions; - @Nullable private final FindAndReplaceOptions findAndReplaceOptions; - @Nullable private final Object replacement; + private final org.springframework.data.mongodb.core.query.@Nullable UpdateDefinition update; + private final @Nullable String collection; + private final @Nullable FindAndModifyOptions findAndModifyOptions; + private final @Nullable FindAndReplaceOptions findAndReplaceOptions; + private final @Nullable Object replacement; private final Class targetType; - ReactiveUpdateSupport(ReactiveMongoTemplate template, Class domainType, Query query, UpdateDefinition update, - String collection, FindAndModifyOptions findAndModifyOptions, FindAndReplaceOptions findAndReplaceOptions, - Object replacement, Class targetType) { + ReactiveUpdateSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions, + @Nullable Object replacement, Class targetType) { this.template = template; this.domainType = domainType; @@ -108,6 +108,7 @@ public Mono upsert() { } @Override + @SuppressWarnings("NullAway") public Mono findAndModify() { String collectionName = getCollectionName(); @@ -118,7 +119,11 @@ public Mono findAndModify() { } @Override + @SuppressWarnings({"unchecked","rawtypes"}) public Mono findAndReplace() { + + Assert.notNull(replacement, "Replacement must be set first"); + return template.findAndReplace(query, replacement, findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType, getCollectionName(), targetType); @@ -186,6 +191,7 @@ public FindAndReplaceWithOptions as(Class resultType) { } @Override + @SuppressWarnings("NullAway") public Mono replaceFirst() { if (replacement != null) { @@ -197,6 +203,7 @@ public Mono replaceFirst() { findAndReplaceOptions != null ? findAndReplaceOptions : ReplaceOptions.none(), getCollectionName()); } + @SuppressWarnings("NullAway") private Mono doUpdate(boolean multi, boolean upsert) { return template.doUpdate(getCollectionName(), query, update, domainType, upsert, multi); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java index 00c5815fc9..7a7e5fdfb2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadConcernAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java index 74bca9abea..e6f3fc0daf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReadPreferenceAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ReadPreference; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java index a2e2ba24c0..a487cde669 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReplaceOptions.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; /** * Options for {@link org.springframework.data.mongodb.core.MongoOperations#replace(Query, Object) replace operations}. Defaults to @@ -69,6 +70,7 @@ public static ReplaceOptions none() { * * @return this. */ + @Contract("-> this") public ReplaceOptions upsert() { this.upsert = true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java index a01760368a..2ec71b415a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScriptOperations.java @@ -17,9 +17,9 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.script.ExecutableMongoScript; import org.springframework.data.mongodb.core.script.NamedMongoScript; -import org.springframework.lang.Nullable; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java index 85ddce7656..62e6d6c513 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ScrollUtils.java @@ -29,6 +29,7 @@ import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.EntityOperations.Entity; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.util.Assert; /** * Utilities to run scroll queries and create {@link Window} results. @@ -48,7 +49,11 @@ class ScrollUtils { */ static KeysetScrollQuery createKeysetPaginationQuery(Query query, String idPropertyName) { + KeysetScrollPosition keyset = query.getKeyset(); + + Assert.notNull(keyset, "Query.keyset must not be null"); + KeysetScrollDirector director = KeysetScrollDirector.of(keyset.getDirection()); Document sortObject = director.getSortObject(idPropertyName, query); Document fieldsObject = director.getFieldsObject(query.getFieldsObject(), sortObject); @@ -61,6 +66,9 @@ static Window createWindow(Query query, List result, Class sourceTy Document sortObject = query.getSortObject(); KeysetScrollPosition keyset = query.getKeyset(); + + Assert.notNull(keyset, "Query.keyset must not be null"); + Direction direction = keyset.getDirection(); KeysetScrollDirector director = KeysetScrollDirector.of(direction); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java index 55a87ecadf..76a6d525f8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; /** * Callback interface for executing operations within a {@link com.mongodb.session.ClientSession}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java index 33ad9d7318..906d682685 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java @@ -17,10 +17,10 @@ import java.util.function.Consumer; -import org.springframework.lang.Nullable; - import com.mongodb.client.ClientSession; +import org.jspecify.annotations.Nullable; + /** * Gateway interface to execute {@link ClientSession} bound operations against MongoDB via a {@link SessionCallback}. *
@@ -42,8 +42,7 @@ public interface SessionScoped { * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ - @Nullable - default T execute(SessionCallback action) { + default @Nullable T execute(SessionCallback action) { return execute(action, session -> {}); } @@ -60,6 +59,5 @@ default T execute(SessionCallback action) { * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ - @Nullable - T execute(SessionCallback action, Consumer doFinally); + @Nullable T execute(SessionCallback action, Consumer doFinally); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java index 84edf13d57..529f912e6c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java @@ -18,13 +18,13 @@ import reactor.core.publisher.Mono; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.DisposableBean; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.SessionAwareMethodInterceptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java index e50e1088cb..b4b525fc97 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ViewOptions.java @@ -17,8 +17,9 @@ import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Immutable object holding additional options to be applied when creating a MongoDB @@ -59,6 +60,7 @@ public Optional getCollation() { * @param collation the {@link Collation} to use for language-specific string comparison. * @return new instance of {@link ViewOptions}. */ + @Contract("_ -> new") public ViewOptions collation(Collation collation) { return new ViewOptions(collation); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java index d6e4119b20..bdc7de6663 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernAware.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java index 8df4171844..a72c656e47 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/WriteConcernResolver.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.WriteConcern; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java index d4cdece411..710b570ed7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AbstractAggregationExpression.java @@ -26,6 +26,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; @@ -282,7 +283,7 @@ protected T get(int index) { * @since 2.1 */ @SuppressWarnings("unchecked") - protected T get(Object key) { + protected @Nullable T get(Object key) { Assert.isInstanceOf(Map.class, this.value, "Value must be a type of Map"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java index cf6485c230..fa44656c99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AccumulatorOperators.java @@ -22,6 +22,8 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -60,8 +62,8 @@ public static AccumulatorOperatorFactory valueOf(AggregationExpression expressio */ public static class AccumulatorOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link AccumulatorOperatorFactory} for given {@literal fieldReference}. @@ -93,6 +95,7 @@ public AccumulatorOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Sum}. */ + @SuppressWarnings("NullAway") public Sum sum() { return usesFieldRef() ? Sum.sumOf(fieldReference) : Sum.sumOf(expression); } @@ -103,6 +106,7 @@ public Sum sum() { * * @return new instance of {@link Avg}. */ + @SuppressWarnings("NullAway") public Avg avg() { return usesFieldRef() ? Avg.avgOf(fieldReference) : Avg.avgOf(expression); } @@ -113,6 +117,7 @@ public Avg avg() { * * @return new instance of {@link Max}. */ + @SuppressWarnings("NullAway") public Max max() { return usesFieldRef() ? Max.maxOf(fieldReference) : Max.maxOf(expression); } @@ -134,6 +139,7 @@ public Max max(int numberOfResults) { * * @return new instance of {@link Min}. */ + @SuppressWarnings("NullAway") public Min min() { return usesFieldRef() ? Min.minOf(fieldReference) : Min.minOf(expression); } @@ -155,6 +161,7 @@ public Min min(int numberOfResults) { * * @return new instance of {@link StdDevPop}. */ + @SuppressWarnings("NullAway") public StdDevPop stdDevPop() { return usesFieldRef() ? StdDevPop.stdDevPopOf(fieldReference) : StdDevPop.stdDevPopOf(expression); } @@ -165,6 +172,7 @@ public StdDevPop stdDevPop() { * * @return new instance of {@link StdDevSamp}. */ + @SuppressWarnings("NullAway") public StdDevSamp stdDevSamp() { return usesFieldRef() ? StdDevSamp.stdDevSampOf(fieldReference) : StdDevSamp.stdDevSampOf(expression); } @@ -193,6 +201,7 @@ public CovariancePop covariancePop(AggregationExpression expression) { return covariancePop().and(expression); } + @SuppressWarnings("NullAway") private CovariancePop covariancePop() { return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression); } @@ -221,6 +230,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) { return covarianceSamp().and(expression); } + @SuppressWarnings("NullAway") private CovarianceSamp covarianceSamp() { return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference) : CovarianceSamp.covarianceSampOf(expression); @@ -233,6 +243,7 @@ private CovarianceSamp covarianceSamp() { * @return new instance of {@link ExpMovingAvg}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ExpMovingAvgBuilder expMovingAvg() { ExpMovingAvg expMovingAvg = usesFieldRef() ? ExpMovingAvg.expMovingAvgOf(fieldReference) @@ -252,13 +263,14 @@ public ExpMovingAvg alpha(double exponentialDecayValue) { } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * associated numeric value expression. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the associated numeric + * value expression. * * @return new instance of {@link Percentile}. * @param percentages must not be {@literal null}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Percentile percentile(Double... percentages) { Percentile percentile = usesFieldRef() ? Percentile.percentileOf(fieldReference) : Percentile.percentileOf(expression); @@ -271,6 +283,7 @@ public Percentile percentile(Double... percentages) { * @return new instance of {@link Median}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Median median() { return usesFieldRef() ? Median.medianOf(fieldReference) : Median.medianOf(expression); } @@ -339,6 +352,7 @@ public static Sum sumOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public static Sum sumOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -352,6 +366,7 @@ public static Sum sumOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public Sum and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -365,6 +380,7 @@ public Sum and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Sum}. */ + @Contract("_ -> new") public Sum and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -379,6 +395,7 @@ public Sum and(AggregationExpression expression) { * @return new instance of {@link Sum}. * @since 2.2 */ + @Contract("_ -> new") public Sum and(Number value) { Assert.notNull(value, "Value must not be null"); @@ -386,7 +403,6 @@ public Sum and(Number value) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -444,6 +460,7 @@ public static Avg avgOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Avg}. */ + @Contract("_ -> new") public Avg and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -457,6 +474,7 @@ public Avg and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Avg}. */ + @Contract("_ -> new") public Avg and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -464,7 +482,6 @@ public Avg and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -522,6 +539,7 @@ public static Max maxOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -535,6 +553,7 @@ public Max and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -548,11 +567,13 @@ public Max and(AggregationExpression expression) { * @param numberOfResults * @return new instance of {@link Max}. */ + @Contract("_ -> new") public Max limit(int numberOfResults) { return new Max(append("n", numberOfResults)); } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { if (get("n") == null) { return toDocument(get("input"), context); @@ -619,6 +640,7 @@ public static Min minOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -632,6 +654,7 @@ public Min and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -645,11 +668,13 @@ public Min and(AggregationExpression expression) { * @param numberOfResults * @return new instance of {@link Min}. */ + @Contract("_ -> new") public Min limit(int numberOfResults) { return new Min(append("n", numberOfResults)); } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { if (get("n") == null) { @@ -659,7 +684,6 @@ public Document toDocument(AggregationOperationContext context) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -717,6 +741,7 @@ public static StdDevPop stdDevPopOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link StdDevPop}. */ + @Contract("_ -> new") public StdDevPop and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -730,6 +755,7 @@ public StdDevPop and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link StdDevPop}. */ + @Contract("_ -> new") public StdDevPop and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -737,7 +763,6 @@ public StdDevPop and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -795,6 +820,7 @@ public static StdDevSamp stdDevSampOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link StdDevSamp}. */ + @Contract("_ -> new") public StdDevSamp and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -808,6 +834,7 @@ public StdDevSamp and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link StdDevSamp}. */ + @Contract("_ -> new") public StdDevSamp and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -815,7 +842,6 @@ public StdDevSamp and(AggregationExpression expression) { } @Override - @SuppressWarnings("unchecked") public Document toDocument(Object value, AggregationOperationContext context) { if (value instanceof List list && list.size() == 1) { @@ -866,6 +892,7 @@ public static CovariancePop covariancePopOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link CovariancePop}. */ + @Contract("_ -> new") public CovariancePop and(String fieldReference) { return new CovariancePop(append(asFields(fieldReference))); } @@ -876,6 +903,7 @@ public CovariancePop and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link CovariancePop}. */ + @Contract("_ -> new") public CovariancePop and(AggregationExpression expression) { return new CovariancePop(append(expression)); } @@ -926,6 +954,7 @@ public static CovarianceSamp covarianceSampOf(AggregationExpression expression) * @param fieldReference must not be {@literal null}. * @return new instance of {@link CovarianceSamp}. */ + @Contract("_ -> new") public CovarianceSamp and(String fieldReference) { return new CovarianceSamp(append(asFields(fieldReference))); } @@ -936,6 +965,7 @@ public CovarianceSamp and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link CovarianceSamp}. */ + @Contract("_ -> new") public CovarianceSamp and(AggregationExpression expression) { return new CovarianceSamp(append(expression)); } @@ -986,6 +1016,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) { * @param numberOfHistoricalDocuments * @return new instance of {@link ExpMovingAvg}. */ + @Contract("_ -> new") public ExpMovingAvg n/*umber of historical documents*/(int numberOfHistoricalDocuments) { return new ExpMovingAvg(append("N", numberOfHistoricalDocuments)); } @@ -997,6 +1028,7 @@ public static ExpMovingAvg expMovingAvgOf(AggregationExpression expression) { * @param exponentialDecayValue * @return new instance of {@link ExpMovingAvg}. */ + @Contract("_ -> new") public ExpMovingAvg alpha(double exponentialDecayValue) { return new ExpMovingAvg(append("alpha", exponentialDecayValue)); } @@ -1055,6 +1087,7 @@ public static Percentile percentileOf(AggregationExpression expression) { * @param percentages must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile percentages(Double... percentages) { Assert.notEmpty(percentages, "Percentages must not be null or empty"); @@ -1068,6 +1101,7 @@ public Percentile percentages(Double... percentages) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1081,6 +1115,7 @@ public Percentile and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Percentile}. */ + @Contract("_ -> new") public Percentile and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1142,6 +1177,7 @@ public static Median medianOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Median}. */ + @Contract("_ -> new") public Median and(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1155,6 +1191,7 @@ public Median and(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Median}. */ + @Contract("_ -> new") public Median and(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java index b79d978b8b..3cb75d3050 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperation.java @@ -19,8 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AddFieldsOperation.AddFieldsOperationBuilder.ValueAppender; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Adds new fields to documents. {@code $addFields} outputs documents that contain all existing fields from the input @@ -83,6 +84,7 @@ public static ValueAppender addField(String field) { * @param value the value to assign. * @return new instance of {@link AddFieldsOperation}. */ + @Contract("_ -> new") public AddFieldsOperation addField(Object field, Object value) { LinkedHashMap target = new LinkedHashMap<>(getValueMap()); @@ -96,6 +98,7 @@ public AddFieldsOperation addField(Object field, Object value) { * * @return new instance of {@link AddFieldsOperationBuilder}. */ + @Contract("-> new") public AddFieldsOperationBuilder and() { return new AddFieldsOperationBuilder(getValueMap()); } @@ -140,7 +143,7 @@ public ValueAppender addField(String field) { return new ValueAppender() { @Override - public AddFieldsOperationBuilder withValue(Object value) { + public AddFieldsOperationBuilder withValue(@Nullable Object value) { valueMap.put(field, value); return AddFieldsOperationBuilder.this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java index 00db38329f..e33c565d11 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationExpressionTransformer.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AggregationExpressionTransformer.AggregationExpressionTransformationContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.spel.ExpressionNode; import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; import org.springframework.data.mongodb.core.spel.ExpressionTransformer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java index a49c7e46d5..5027328461 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationContext.java @@ -21,10 +21,10 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java index fd5f7ed979..6437ec981d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOperationRenderer.java @@ -19,12 +19,12 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; -import org.springframework.lang.Nullable; /** * Rendering support for {@link AggregationOperation} into a {@link List} of {@link org.bson.Document}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java index 327d40b8c7..278da408c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationOptions.java @@ -19,11 +19,12 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.ReadConcern; @@ -299,7 +300,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return readConcern.orElse(null); } @@ -309,7 +310,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return readPreference.orElse(null); } @@ -426,7 +427,7 @@ static Document createCursor(int cursorBatchSize) { */ public static class Builder { - private Boolean allowDiskUse; + private @Nullable Boolean allowDiskUse; private boolean explain; private @Nullable Document cursor; private @Nullable Collation collation; @@ -444,6 +445,7 @@ public static class Builder { * @param allowDiskUse use {@literal true} to allow disk use during the aggregation. * @return this. */ + @Contract("_ -> this") public Builder allowDiskUse(boolean allowDiskUse) { this.allowDiskUse = allowDiskUse; @@ -456,6 +458,7 @@ public Builder allowDiskUse(boolean allowDiskUse) { * @param explain use {@literal true} to enable explain feature. * @return this. */ + @Contract("_ -> this") public Builder explain(boolean explain) { this.explain = explain; @@ -468,6 +471,7 @@ public Builder explain(boolean explain) { * @param cursor must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Builder cursor(Document cursor) { this.cursor = cursor; @@ -481,6 +485,7 @@ public Builder cursor(Document cursor) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Builder cursorBatchSize(int batchSize) { this.cursor = createCursor(batchSize); @@ -494,6 +499,7 @@ public Builder cursorBatchSize(int batchSize) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Builder collation(@Nullable Collation collation) { this.collation = collation; @@ -507,6 +513,7 @@ public Builder collation(@Nullable Collation collation) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Builder comment(@Nullable String comment) { this.comment = comment; @@ -520,6 +527,7 @@ public Builder comment(@Nullable String comment) { * @return this. * @since 3.1 */ + @Contract("_ -> this") public Builder hint(@Nullable Document hint) { this.hint = hint; @@ -533,6 +541,7 @@ public Builder hint(@Nullable Document hint) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder hint(@Nullable String indexName) { this.hint = indexName; @@ -546,6 +555,7 @@ public Builder hint(@Nullable String indexName) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder readConcern(@Nullable ReadConcern readConcern) { this.readConcern = readConcern; @@ -559,6 +569,7 @@ public Builder readConcern(@Nullable ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Builder readPreference(@Nullable ReadPreference readPreference) { this.readPreference = readPreference; @@ -573,6 +584,7 @@ public Builder readPreference(@Nullable ReadPreference readPreference) { * @return this. * @since 3.0 */ + @Contract("_ -> this") public Builder maxTime(@Nullable Duration maxTime) { this.maxTime = maxTime; @@ -587,6 +599,7 @@ public Builder maxTime(@Nullable Duration maxTime) { * @return this. * @since 3.0.2 */ + @Contract("-> this") public Builder skipOutput() { this.resultOptions = ResultOptions.SKIP; @@ -600,6 +613,7 @@ public Builder skipOutput() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder strictMapping() { this.domainTypeMapping = DomainTypeMapping.STRICT; @@ -613,6 +627,7 @@ public Builder strictMapping() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder relaxedMapping() { this.domainTypeMapping = DomainTypeMapping.RELAXED; @@ -625,6 +640,7 @@ public Builder relaxedMapping() { * @return this. * @since 3.2 */ + @Contract("-> this") public Builder noMapping() { this.domainTypeMapping = DomainTypeMapping.NONE; @@ -636,6 +652,7 @@ public Builder noMapping() { * * @return new instance of {@link AggregationOptions}. */ + @Contract("-> new") public AggregationOptions build() { AggregationOptions options = new AggregationOptions(allowDiskUse, explain, cursor, collation, comment, hint); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java index 68662ec0df..40966bcf3d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java @@ -22,6 +22,7 @@ import java.util.function.Predicate; import org.bson.Document; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -63,6 +64,7 @@ public AggregationPipeline(List aggregationOperations) { * @param aggregationOperation must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public AggregationPipeline add(AggregationOperation aggregationOperation) { Assert.notNull(aggregationOperation, "AggregationOperation must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java index 438eb9e49f..f5a861cddd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java @@ -20,7 +20,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -71,8 +71,7 @@ public List getMappedResults() { * @return the single already mapped result object or raise an error if more than one found. * @throws IllegalArgumentException in case more than one result is available. */ - @Nullable - public T getUniqueMappedResult() { + public @Nullable T getUniqueMappedResult() { Assert.isTrue(mappedResults.size() < 2, "Expected unique result or null, but got more than one"); return mappedResults.size() == 1 ? mappedResults.get(0) : null; } @@ -101,8 +100,7 @@ public Document getRawResults() { return rawResults; } - @Nullable - private String parseServerUsed() { + private @Nullable String parseServerUsed() { Object object = rawResults.get("serverUsed"); return object instanceof String stringValue ? stringValue : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java index 1626d672bc..c5b53ef0c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationSpELExpression.java @@ -66,6 +66,8 @@ public static AggregationSpELExpression expressionOf(String expressionString, Ob @Override public Document toDocument(AggregationOperationContext context) { - return (Document) TRANSFORMER.transform(rawExpression, context, parameters); + + Document doc = (Document) TRANSFORMER.transform(rawExpression, context, parameters); + return doc != null ? doc : new Document(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java index 15d700309e..9e8564c03e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationUpdate.java @@ -25,11 +25,11 @@ import java.util.stream.Collectors; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.mongodb.core.query.UpdateDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -129,6 +129,7 @@ public static AggregationUpdate from(List pipeline) { * @return this. * @see $set Aggregation Reference */ + @Contract("_ -> this") public AggregationUpdate set(SetOperation setOperation) { Assert.notNull(setOperation, "SetOperation must not be null"); @@ -148,6 +149,7 @@ public AggregationUpdate set(SetOperation setOperation) { * @see $unset Aggregation * Reference */ + @Contract("_ -> this") public AggregationUpdate unset(UnsetOperation unsetOperation) { Assert.notNull(unsetOperation, "UnsetOperation must not be null"); @@ -166,6 +168,7 @@ public AggregationUpdate unset(UnsetOperation unsetOperation) { * @see $replaceWith Aggregation * Reference */ + @Contract("_ -> this") public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) { Assert.notNull(replaceWithOperation, "ReplaceWithOperation must not be null"); @@ -179,6 +182,7 @@ public AggregationUpdate replaceWith(ReplaceWithOperation replaceWithOperation) * @param value must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public AggregationUpdate replaceWith(Object value) { Assert.notNull(value, "Value must not be null"); @@ -193,6 +197,7 @@ public AggregationUpdate replaceWith(Object value) { * @return new instance of {@link SetValueAppender}. * @see #set(SetOperation) */ + @Contract("_ -> new") public SetValueAppender set(String key) { Assert.notNull(key, "Key must not be null"); @@ -219,6 +224,7 @@ public AggregationUpdate toValueOf(Object value) { * @param keys the fields to remove. * @return this. */ + @Contract("_ -> this") public AggregationUpdate unset(String... keys) { Assert.notNull(keys, "Keys must not be null"); @@ -234,6 +240,7 @@ public AggregationUpdate unset(String... keys) { * * @return never {@literal null}. */ + @Contract("-> this") public AggregationUpdate isolated() { isolated = true; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java index ed79202345..522dd5eae5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationVariable.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java index e2c31c6346..c7787b382c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArithmeticOperators.java @@ -20,6 +20,7 @@ import java.util.Locale; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Avg; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovariancePop; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.CovarianceSamp; @@ -32,7 +33,7 @@ import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnit; import org.springframework.data.mongodb.core.aggregation.SetWindowFieldsOperation.WindowUnits; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -84,8 +85,8 @@ public static Rand rand() { */ public static class ArithmeticOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link ArithmeticOperatorFactory} for given {@literal fieldReference}. @@ -116,6 +117,7 @@ public ArithmeticOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Abs}. */ + @SuppressWarnings("NullAway") public Abs abs() { return usesFieldRef() ? Abs.absoluteValueOf(fieldReference) : Abs.absoluteValueOf(expression); } @@ -158,6 +160,7 @@ public Add add(Number value) { return createAdd().add(value); } + @SuppressWarnings("NullAway") private Add createAdd() { return usesFieldRef() ? Add.valueOf(fieldReference) : Add.valueOf(expression); } @@ -168,6 +171,7 @@ private Add createAdd() { * * @return new instance of {@link Ceil}. */ + @SuppressWarnings("NullAway") public Ceil ceil() { return usesFieldRef() ? Ceil.ceilValueOf(fieldReference) : Ceil.ceilValueOf(expression); } @@ -205,6 +209,7 @@ public Derivative derivative(WindowUnit unit) { * @return new instance of {@link Derivative}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Derivative derivative(@Nullable String unit) { Derivative derivative = usesFieldRef() ? Derivative.derivativeOf(fieldReference) @@ -250,6 +255,7 @@ public Divide divideBy(Number value) { return createDivide().divideBy(value); } + @SuppressWarnings("NullAway") private Divide createDivide() { return usesFieldRef() ? Divide.valueOf(fieldReference) : Divide.valueOf(expression); } @@ -259,6 +265,7 @@ private Divide createDivide() { * * @return new instance of {@link Exp}. */ + @SuppressWarnings("NullAway") public Exp exp() { return usesFieldRef() ? Exp.expValueOf(fieldReference) : Exp.expValueOf(expression); } @@ -269,6 +276,7 @@ public Exp exp() { * * @return new instance of {@link Floor}. */ + @SuppressWarnings("NullAway") public Floor floor() { return usesFieldRef() ? Floor.floorValueOf(fieldReference) : Floor.floorValueOf(expression); } @@ -279,6 +287,7 @@ public Floor floor() { * @return new instance of {@link Integral}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Integral integral() { return usesFieldRef() ? Integral.integralOf(fieldReference) : Integral.integralOf(expression); } @@ -318,6 +327,7 @@ public Integral integral(String unit) { * * @return new instance of {@link Ln}. */ + @SuppressWarnings("NullAway") public Ln ln() { return usesFieldRef() ? Ln.lnValueOf(fieldReference) : Ln.lnValueOf(expression); } @@ -345,7 +355,7 @@ public Log log(String fieldReference) { public Log log(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); - return createLog().log(fieldReference); + return createLog().log(expression); } /** @@ -361,6 +371,7 @@ public Log log(Number base) { return createLog().log(base); } + @SuppressWarnings("NullAway") private Log createLog() { return usesFieldRef() ? Log.valueOf(fieldReference) : Log.valueOf(expression); } @@ -370,6 +381,7 @@ private Log createLog() { * * @return new instance of {@link Log10}. */ + @SuppressWarnings("NullAway") public Log10 log10() { return usesFieldRef() ? Log10.log10ValueOf(fieldReference) : Log10.log10ValueOf(expression); } @@ -413,6 +425,7 @@ public Mod mod(Number value) { return createMod().mod(value); } + @SuppressWarnings("NullAway") private Mod createMod() { return usesFieldRef() ? Mod.valueOf(fieldReference) : Mod.valueOf(expression); } @@ -453,6 +466,7 @@ public Multiply multiplyBy(Number value) { return createMultiply().multiplyBy(value); } + @SuppressWarnings("NullAway") private Multiply createMultiply() { return usesFieldRef() ? Multiply.valueOf(fieldReference) : Multiply.valueOf(expression); } @@ -493,6 +507,7 @@ public Pow pow(Number value) { return createPow().pow(value); } + @SuppressWarnings("NullAway") private Pow createPow() { return usesFieldRef() ? Pow.valueOf(fieldReference) : Pow.valueOf(expression); } @@ -502,6 +517,7 @@ private Pow createPow() { * * @return new instance of {@link Sqrt}. */ + @SuppressWarnings("NullAway") public Sqrt sqrt() { return usesFieldRef() ? Sqrt.sqrtOf(fieldReference) : Sqrt.sqrtOf(expression); } @@ -542,6 +558,7 @@ public Subtract subtract(Number value) { return createSubtract().subtract(value); } + @SuppressWarnings("NullAway") private Subtract createSubtract() { return usesFieldRef() ? Subtract.valueOf(fieldReference) : Subtract.valueOf(expression); } @@ -551,6 +568,7 @@ private Subtract createSubtract() { * * @return new instance of {@link Trunc}. */ + @SuppressWarnings("NullAway") public Trunc trunc() { return usesFieldRef() ? Trunc.truncValueOf(fieldReference) : Trunc.truncValueOf(expression); } @@ -560,6 +578,7 @@ public Trunc trunc() { * * @return new instance of {@link Sum}. */ + @SuppressWarnings("NullAway") public Sum sum() { return usesFieldRef() ? AccumulatorOperators.Sum.sumOf(fieldReference) : AccumulatorOperators.Sum.sumOf(expression); @@ -570,6 +589,7 @@ public Sum sum() { * * @return new instance of {@link Avg}. */ + @SuppressWarnings("NullAway") public Avg avg() { return usesFieldRef() ? AccumulatorOperators.Avg.avgOf(fieldReference) : AccumulatorOperators.Avg.avgOf(expression); @@ -580,6 +600,7 @@ public Avg avg() { * * @return new instance of {@link Max}. */ + @SuppressWarnings("NullAway") public Max max() { return usesFieldRef() ? AccumulatorOperators.Max.maxOf(fieldReference) : AccumulatorOperators.Max.maxOf(expression); @@ -590,6 +611,7 @@ public Max max() { * * @return new instance of {@link Min}. */ + @SuppressWarnings("NullAway") public Min min() { return usesFieldRef() ? AccumulatorOperators.Min.minOf(fieldReference) : AccumulatorOperators.Min.minOf(expression); @@ -600,6 +622,7 @@ public Min min() { * * @return new instance of {@link StdDevPop}. */ + @SuppressWarnings("NullAway") public StdDevPop stdDevPop() { return usesFieldRef() ? AccumulatorOperators.StdDevPop.stdDevPopOf(fieldReference) : AccumulatorOperators.StdDevPop.stdDevPopOf(expression); @@ -610,6 +633,7 @@ public StdDevPop stdDevPop() { * * @return new instance of {@link StdDevSamp}. */ + @SuppressWarnings("NullAway") public StdDevSamp stdDevSamp() { return usesFieldRef() ? AccumulatorOperators.StdDevSamp.stdDevSampOf(fieldReference) : AccumulatorOperators.StdDevSamp.stdDevSampOf(expression); @@ -639,6 +663,7 @@ public CovariancePop covariancePop(AggregationExpression expression) { return covariancePop().and(expression); } + @SuppressWarnings("NullAway") private CovariancePop covariancePop() { return usesFieldRef() ? CovariancePop.covariancePopOf(fieldReference) : CovariancePop.covariancePopOf(expression); } @@ -667,6 +692,7 @@ public CovarianceSamp covarianceSamp(AggregationExpression expression) { return covarianceSamp().and(expression); } + @SuppressWarnings("NullAway") private CovarianceSamp covarianceSamp() { return usesFieldRef() ? CovarianceSamp.covarianceSampOf(fieldReference) : CovarianceSamp.covarianceSampOf(expression); @@ -679,6 +705,7 @@ private CovarianceSamp covarianceSamp() { * @return new instance of {@link Round}. * @since 3.0 */ + @SuppressWarnings("NullAway") public Round round() { return usesFieldRef() ? Round.roundValueOf(fieldReference) : Round.roundValueOf(expression); } @@ -712,6 +739,7 @@ public Sin sin() { * @return new instance of {@link Sin}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Sin sin(AngularUnit unit) { return usesFieldRef() ? Sin.sinOf(fieldReference, unit) : Sin.sinOf(expression, unit); } @@ -734,6 +762,7 @@ public Sinh sinh() { * @return new instance of {@link Sinh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Sinh sinh(AngularUnit unit) { return usesFieldRef() ? Sinh.sinhOf(fieldReference, unit) : Sinh.sinhOf(expression, unit); } @@ -744,6 +773,7 @@ public Sinh sinh(AngularUnit unit) { * @return new instance of {@link ASin}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ASin asin() { return usesFieldRef() ? ASin.asinOf(fieldReference) : ASin.asinOf(expression); } @@ -754,6 +784,7 @@ public ASin asin() { * @return new instance of {@link ASinh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ASinh asinh() { return usesFieldRef() ? ASinh.asinhOf(fieldReference) : ASinh.asinhOf(expression); } @@ -777,6 +808,7 @@ public Cos cos() { * @return new instance of {@link Cos}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Cos cos(AngularUnit unit) { return usesFieldRef() ? Cos.cosOf(fieldReference, unit) : Cos.cosOf(expression, unit); } @@ -799,6 +831,7 @@ public Cosh cosh() { * @return new instance of {@link Cosh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Cosh cosh(AngularUnit unit) { return usesFieldRef() ? Cosh.coshOf(fieldReference, unit) : Cosh.coshOf(expression, unit); } @@ -809,6 +842,7 @@ public Cosh cosh(AngularUnit unit) { * @return new instance of {@link ACos}. * @since 3.4 */ + @SuppressWarnings("NullAway") public ACos acos() { return usesFieldRef() ? ACos.acosOf(fieldReference) : ACos.acosOf(expression); } @@ -819,6 +853,7 @@ public ACos acos() { * @return new instance of {@link ACosh}. * @since 3.4 */ + @SuppressWarnings("NullAway") public ACosh acosh() { return usesFieldRef() ? ACosh.acoshOf(fieldReference) : ACosh.acoshOf(expression); } @@ -840,6 +875,7 @@ public Tan tan() { * @return new instance of {@link ATan}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATan atan() { return usesFieldRef() ? ATan.atanOf(fieldReference) : ATan.atanOf(expression); } @@ -852,6 +888,7 @@ public ATan atan() { * @return new instance of {@link ATan2}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATan2 atan2(Number value) { Assert.notNull(value, "Value must not be null"); @@ -886,8 +923,8 @@ public ATan2 atan2(AggregationExpression expression) { return createATan2().atan2of(expression); } + @SuppressWarnings("NullAway") private ATan2 createATan2() { - return usesFieldRef() ? ATan2.valueOf(fieldReference) : ATan2.valueOf(expression); } @@ -897,6 +934,7 @@ private ATan2 createATan2() { * @return new instance of {@link ATanh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public ATanh atanh() { return usesFieldRef() ? ATanh.atanhOf(fieldReference) : ATanh.atanhOf(expression); } @@ -909,6 +947,7 @@ public ATanh atanh() { * @return new instance of {@link Tan}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Tan tan(AngularUnit unit) { return usesFieldRef() ? Tan.tanOf(fieldReference, unit) : Tan.tanOf(expression, unit); } @@ -931,18 +970,19 @@ public Tanh tanh() { * @return new instance of {@link Tanh}. * @since 3.3 */ + @SuppressWarnings("NullAway") public Tanh tanh(AngularUnit unit) { return usesFieldRef() ? Tanh.tanhOf(fieldReference, unit) : Tanh.tanhOf(expression, unit); } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * numeric value. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value. * * @return new instance of {@link Percentile}. * @param percentages must not be {@literal null}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Percentile percentile(Double... percentages) { Percentile percentile = usesFieldRef() ? AccumulatorOperators.Percentile.percentileOf(fieldReference) : AccumulatorOperators.Percentile.percentileOf(expression); @@ -950,12 +990,12 @@ public Percentile percentile(Double... percentages) { } /** - * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the - * numeric value. + * Creates new {@link AggregationExpression} that calculates the requested percentile(s) of the numeric value. * * @return new instance of {@link Median}. * @since 4.2 */ + @SuppressWarnings("NullAway") public Median median() { return usesFieldRef() ? AccumulatorOperators.Median.medianOf(fieldReference) : AccumulatorOperators.Median.medianOf(expression); @@ -1077,6 +1117,7 @@ public static Add valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1089,6 +1130,7 @@ public Add add(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1101,6 +1143,7 @@ public Add add(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Add}. */ + @Contract("_ -> new") public Add add(Number value) { return new Add(append(value)); } @@ -1217,6 +1260,7 @@ public static Divide valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1229,6 +1273,7 @@ public Divide divideBy(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1241,6 +1286,7 @@ public Divide divideBy(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Divide}. */ + @Contract("_ -> new") public Divide divideBy(Number value) { return new Divide(append(value)); } @@ -1463,6 +1509,7 @@ public static Log valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1475,6 +1522,7 @@ public Log log(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1487,6 +1535,7 @@ public Log log(AggregationExpression expression) { * @param base must not be {@literal null}. * @return new instance of {@link Log}. */ + @Contract("_ -> new") public Log log(Number base) { return new Log(append(base)); } @@ -1603,6 +1652,7 @@ public static Mod valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1615,6 +1665,7 @@ public Mod mod(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1627,6 +1678,7 @@ public Mod mod(AggregationExpression expression) { * @param base must not be {@literal null}. * @return new instance of {@link Mod}. */ + @Contract("_ -> new") public Mod mod(Number base) { return new Mod(append(base)); } @@ -1690,6 +1742,7 @@ public static Multiply valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1702,6 +1755,7 @@ public Multiply multiplyBy(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1714,6 +1768,7 @@ public Multiply multiplyBy(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Multiply}. */ + @Contract("_ -> new") public Multiply multiplyBy(Number value) { return new Multiply(append(value)); } @@ -1777,6 +1832,7 @@ public static Pow valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1789,6 +1845,7 @@ public Pow pow(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1801,6 +1858,7 @@ public Pow pow(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Pow pow(Number value) { return new Pow(append(value)); } @@ -1917,6 +1975,7 @@ public static Subtract valueOf(Number value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1929,6 +1988,7 @@ public Subtract subtract(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1941,6 +2001,7 @@ public Subtract subtract(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Pow}. */ + @Contract("_ -> new") public Subtract subtract(Number value) { return new Subtract(append(value)); } @@ -2060,6 +2121,7 @@ public static Round round(Number value) { * @param place value between -20 and 100, exclusive. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round place(int place) { return new Round(append(place)); } @@ -2070,6 +2132,7 @@ public Round place(int place) { * @param expression must not be {@literal null}. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round placeOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2083,6 +2146,7 @@ public Round placeOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Round}. */ + @Contract("_ -> new") public Round placeOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2133,6 +2197,7 @@ public static Derivative derivativeOfValue(Number value) { return new Derivative(Collections.singletonMap("input", value)); } + @Contract("_ -> new") public Derivative unit(String unit) { return new Derivative(append("unit", unit)); } @@ -2183,6 +2248,7 @@ public static Integral integralOf(AggregationExpression expression) { * @param unit the unit of measure. * @return new instance of {@link Integral}. */ + @Contract("_ -> new") public Integral unit(String unit) { return new Integral(append("unit", unit)); } @@ -2217,8 +2283,7 @@ private Sin(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the sine of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for * *
@@ -2330,8 +2395,7 @@ public static Sinh sinhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for * *
@@ -2350,8 +2414,7 @@ public static Sinh sinhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic sine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -2434,8 +2497,7 @@ public static ASin asinOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse sine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ASin}. @@ -2484,8 +2546,7 @@ public static ASinh asinhOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic sine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ASinh}. @@ -2525,8 +2586,7 @@ private Cos(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the cosine of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code cosOf("angle", DEGREES)} as shortcut for * *
@@ -2636,8 +2696,7 @@ public static Cosh coshOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code coshOf("angle", DEGREES)} as shortcut for * *
@@ -2654,8 +2713,7 @@ public static Cosh coshOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic cosine of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -2738,8 +2796,7 @@ public static ACos acosOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse cosine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ACos}. @@ -2788,8 +2845,7 @@ public static ACosh acoshOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic cosine of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ACosh}. @@ -2829,8 +2885,7 @@ private Tan(Object value) { /** * Creates a new {@link AggregationExpression} that calculates the tangent of a value that is measured in - * {@link AngularUnit#RADIANS radians}. - *
+ * {@link AngularUnit#RADIANS radians}.
* Use {@code tanOf("angle", DEGREES)} as shortcut for * *
@@ -3008,8 +3063,8 @@ public static ATan2 valueOf(AggregationExpression expression) {
 		 * Creates a new {@link AggregationExpression} that calculates the inverse tangent of of y / x, where y and x are
 		 * the first and second values passed to the expression respectively.
 		 *
-		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param fieldReference anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves
+		 *          to a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(String fieldReference) {
@@ -3022,8 +3077,8 @@ public ATan2 atan2of(String fieldReference) {
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
 		 * {@link AngularUnit#RADIANS}.
 		 *
-		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a
-		 *          numeric value.
+		 * @param expression anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to
+		 *          a numeric value.
 		 * @return new instance of {@link ATan2}.
 		 */
 		public ATan2 atan2of(AggregationExpression expression) {
@@ -3075,8 +3130,7 @@ public static Tanh tanhOf(String fieldReference) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * the given {@link AngularUnit unit}.
-		 * 
+ * the given {@link AngularUnit unit}.
* Use {@code tanhOf("angle", DEGREES)} as shortcut for * *
@@ -3093,8 +3147,7 @@ public static Tanh tanhOf(String fieldReference, AngularUnit unit) {
 
 		/**
 		 * Creates a new {@link AggregationExpression} that calculates the hyperbolic tangent of a value that is measured in
-		 * {@link AngularUnit#RADIANS}.
-		 * 
+ * {@link AngularUnit#RADIANS}.
* Use {@code sinhOf("angle", DEGREES)} as shortcut for eg. * {@code sinhOf(ConvertOperators.valueOf("angle").degreesToRadians())}. * @@ -3165,11 +3218,9 @@ private ATanh(Object value) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse - * hyperbolic tangent of a value. + * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. * - * @param fieldReference the name of the {@link Field field} that resolves to a - * numeric value. + * @param fieldReference the name of the {@link Field field} that resolves to a numeric value. * @return new instance of {@link ATanh}. */ public static ATanh atanhOf(String fieldReference) { @@ -3177,8 +3228,7 @@ public static ATanh atanhOf(String fieldReference) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. - *
+ * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value.
* * @param expression the {@link AggregationExpression expression} that resolves to a numeric value. * @return new instance of {@link ATanh}. @@ -3188,11 +3238,10 @@ public static ATanh atanhOf(AggregationExpression expression) { } /** - * Creates a new {@link AggregationExpression} that calculates the inverse - * hyperbolic tangent of a value. + * Creates a new {@link AggregationExpression} that calculates the inverse hyperbolic tangent of a value. * - * @param value anything ({@link Field field}, {@link AggregationExpression - * expression}, ...) that resolves to a numeric value. + * @param value anything ({@link Field field}, {@link AggregationExpression expression}, ...) that resolves to a + * numeric value. * @return new instance of {@link ATanh}. */ public static ATanh atanhOf(Object value) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index a8cb58d17c..02b805d5ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -22,13 +22,14 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.AsBuilder; import org.springframework.data.mongodb.core.aggregation.ArrayOperators.Reduce.PropertyExpression; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -159,6 +160,7 @@ public ArrayElemAt elementAt(String fieldReference) { return createArrayElemAt().elementAt(fieldReference); } + @SuppressWarnings("NullAway") private ArrayElemAt createArrayElemAt() { if (usesFieldRef()) { @@ -194,6 +196,7 @@ public ConcatArrays concat(AggregationExpression expression) { return createConcatArrays().concat(expression); } + @SuppressWarnings("NullAway") private ConcatArrays createConcatArrays() { if (usesFieldRef()) { @@ -209,6 +212,7 @@ private ConcatArrays createConcatArrays() { * * @return new instance of {@link AsBuilder} to create a {@link Filter}. */ + @SuppressWarnings("NullAway") public AsBuilder filter() { if (usesFieldRef()) { @@ -228,6 +232,7 @@ public AsBuilder filter() { * * @return new instance of {@link IsArray}. */ + @SuppressWarnings("NullAway") public IsArray isArray() { Assert.state(values == null, "Does it make sense to call isArray on an array; Maybe just skip it"); @@ -240,6 +245,7 @@ public IsArray isArray() { * * @return new instance of {@link Size}. */ + @SuppressWarnings("NullAway") public Size length() { if (usesFieldRef()) { @@ -254,6 +260,7 @@ public Size length() { * * @return new instance of {@link Slice}. */ + @SuppressWarnings("NullAway") public Slice slice() { if (usesFieldRef()) { @@ -270,6 +277,7 @@ public Slice slice() { * @param value must not be {@literal null}. * @return new instance of {@link IndexOfArray}. */ + @SuppressWarnings("NullAway") public IndexOfArray indexOf(Object value) { if (usesFieldRef()) { @@ -285,6 +293,7 @@ public IndexOfArray indexOf(Object value) { * * @return new instance of {@link ReverseArray}. */ + @SuppressWarnings("NullAway") public ReverseArray reverse() { if (usesFieldRef()) { @@ -302,6 +311,7 @@ public ReverseArray reverse() { * @param expression must not be {@literal null}. * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}. */ + @SuppressWarnings("NullAway") public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpression expression) { return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) @@ -315,6 +325,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(AggregationExpressi * @param expressions must not be {@literal null}. * @return new instance of {@link ReduceInitialValueBuilder} to create {@link Reduce}. */ + @SuppressWarnings("NullAway") public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression... expressions) { return initialValue -> (usesFieldRef() ? Reduce.arrayOf(fieldReference) : Reduce.arrayOf(expression)) @@ -328,6 +339,7 @@ public ArrayOperatorFactory.ReduceInitialValueBuilder reduce(PropertyExpression. * @return new instance of {@link SortArray}. * @since 4.0 */ + @SuppressWarnings("NullAway") public SortArray sort(Sort sort) { if (usesFieldRef()) { @@ -361,6 +373,7 @@ public SortArray sort(Direction direction) { * @param arrays must not be {@literal null}. * @return new instance of {@link Zip}. */ + @SuppressWarnings("NullAway") public Zip zipWith(Object... arrays) { if (usesFieldRef()) { @@ -377,6 +390,7 @@ public Zip zipWith(Object... arrays) { * @param value must not be {@literal null}. * @return new instance of {@link In}. */ + @SuppressWarnings("NullAway") public In containsValue(Object value) { if (usesFieldRef()) { @@ -393,6 +407,7 @@ public In containsValue(Object value) { * @return new instance of {@link ArrayToObject}. * @since 2.1 */ + @SuppressWarnings("NullAway") public ArrayToObject toObject() { if (usesFieldRef()) { @@ -409,6 +424,7 @@ public ArrayToObject toObject() { * @return new instance of {@link First}. * @since 3.4 */ + @SuppressWarnings("NullAway") public First first() { if (usesFieldRef()) { @@ -425,6 +441,7 @@ public First first() { * @return new instance of {@link Last}. * @since 3.4 */ + @SuppressWarnings("NullAway") public Last last() { if (usesFieldRef()) { @@ -523,6 +540,7 @@ public static ArrayElemAt arrayOf(Collection values) { * @param index the index number * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(int index) { return new ArrayElemAt(append(index)); } @@ -533,6 +551,7 @@ public ArrayElemAt elementAt(int index) { * @param expression must not be {@literal null}. * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -545,6 +564,7 @@ public ArrayElemAt elementAt(AggregationExpression expression) { * @param arrayFieldReference the field name. * @return new instance of {@link ArrayElemAt}. */ + @Contract("_ -> new") public ArrayElemAt elementAt(String arrayFieldReference) { Assert.notNull(arrayFieldReference, "ArrayReference must not be null"); @@ -611,6 +631,7 @@ public static ConcatArrays arrayOf(Collection values) { * @param arrayFieldReference must not be {@literal null}. * @return new instance of {@link ConcatArrays}. */ + @Contract("_ -> new") public ConcatArrays concat(String arrayFieldReference) { Assert.notNull(arrayFieldReference, "ArrayFieldReference must not be null"); @@ -623,6 +644,7 @@ public ConcatArrays concat(String arrayFieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ConcatArrays}. */ + @Contract("_ -> new") public ConcatArrays concat(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -698,9 +720,12 @@ public static AsBuilder filter(List values) { @Override public Document toDocument(final AggregationOperationContext context) { + + Assert.notNull(as, "As must be set first"); return toFilter(ExposedFields.from(as), context); } + @SuppressWarnings("NullAway") private Document toFilter(ExposedFields exposedFields, AggregationOperationContext context) { Document filterExpression = new Document(); @@ -714,7 +739,7 @@ private Document toFilter(ExposedFields exposedFields, AggregationOperationConte return new Document("$filter", filterExpression); } - private Object getMappedInput(AggregationOperationContext context) { + private @Nullable Object getMappedInput(AggregationOperationContext context) { if (input instanceof Field field) { return context.getReference(field).toString(); @@ -727,7 +752,7 @@ private Object getMappedInput(AggregationOperationContext context) { return input; } - private Object getMappedCondition(AggregationOperationContext context) { + private @Nullable Object getMappedCondition(AggregationOperationContext context) { if (!(condition instanceof AggregationExpression aggregationExpression)) { return condition; @@ -834,6 +859,7 @@ public static InputBuilder newBuilder() { } @Override + @Contract("_ -> this") public AsBuilder filter(List array) { Assert.notNull(array, "Array must not be null"); @@ -842,6 +868,7 @@ public AsBuilder filter(List array) { } @Override + @Contract("_ -> this") public AsBuilder filter(Field field) { Assert.notNull(field, "Field must not be null"); @@ -850,6 +877,7 @@ public AsBuilder filter(Field field) { } @Override + @Contract("_ -> this") public AsBuilder filter(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -858,6 +886,7 @@ public AsBuilder filter(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ConditionBuilder as(String variableName) { Assert.notNull(variableName, "Variable name must not be null"); @@ -1045,6 +1074,7 @@ public static Slice sliceArrayOf(Collection values) { * @param count number of elements to slice. * @return new instance of {@link Slice}. */ + @Contract("_ -> new") public Slice itemCount(int count) { return new Slice(append(count)); } @@ -1057,6 +1087,7 @@ public Slice itemCount(int count) { * @return new instance of {@link Slice}. * @since 4.5 */ + @Contract("_ -> new") public Slice itemCount(AggregationExpression count) { return new Slice(append(count)); } @@ -1175,6 +1206,7 @@ public static IndexOfArrayBuilder arrayOf(Collection values) { * @param range the lookup range. * @return new instance of {@link IndexOfArray}. */ + @Contract("_ -> new") public IndexOfArray within(Range range) { return new IndexOfArray(append(AggregationUtils.toRangeValues(range))); } @@ -1250,6 +1282,7 @@ public static RangeOperatorBuilder rangeStartingAt(long value) { return new RangeOperatorBuilder(value); } + @Contract("_ -> new") public RangeOperator withStepSize(long stepSize) { return new RangeOperator(append(stepSize)); } @@ -1382,6 +1415,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document("$reduce", document); } + @SuppressWarnings("NullAway") private Object getMappedValue(Object value, AggregationOperationContext context) { if (value instanceof Document) { @@ -1701,6 +1735,7 @@ public static ZipBuilder arrayOf(Collection values) { * * @return new instance of {@link Zip}. */ + @Contract("-> new") public Zip useLongestLength() { return new Zip(append("useLongestLength", true)); } @@ -1711,6 +1746,7 @@ public Zip useLongestLength() { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1723,6 +1759,7 @@ public Zip defaultTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1735,6 +1772,7 @@ public Zip defaultTo(AggregationExpression expression) { * @param array must not be {@literal null}. * @return new instance of {@link Zip}. */ + @Contract("_ -> new") public Zip defaultTo(Object[] array) { Assert.notNull(array, "Array must not be null"); @@ -2064,6 +2102,7 @@ public static SortArray sortArrayOf(AggregationExpression expression) { * @param sort must not be {@literal null}. * @return new instance of {@link SortArray}. */ + @Contract("_ -> new") public SortArray by(Sort sort) { return new SortArray(append("sortBy", sort)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java index 69689908c9..f3ffdb7ad1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BooleanOperators.java @@ -19,6 +19,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -77,8 +79,8 @@ public static Not not(AggregationExpression expression) { */ public static class BooleanOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link BooleanOperatorFactory} for given {@literal fieldReference}. @@ -130,6 +132,7 @@ public And and(String fieldReference) { return createAnd().andField(fieldReference); } + @SuppressWarnings("NullAway") private And createAnd() { return usesFieldRef() ? And.and(Fields.field(fieldReference)) : And.and(expression); } @@ -160,6 +163,7 @@ public Or or(String fieldReference) { return createOr().orField(fieldReference); } + @SuppressWarnings("NullAway") private Or createOr() { return usesFieldRef() ? Or.or(Fields.field(fieldReference)) : Or.or(expression); } @@ -169,6 +173,7 @@ private Or createOr() { * * @return new instance of {@link Not}. */ + @SuppressWarnings("NullAway") public Not not() { return usesFieldRef() ? Not.not(fieldReference) : Not.not(expression); } @@ -211,6 +216,7 @@ public static And and(Object... expressions) { * @param expression must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andExpression(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -223,6 +229,7 @@ public And andExpression(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andField(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -235,6 +242,7 @@ public And andField(String fieldReference) { * @param value must not be {@literal null}. * @return new instance of {@link And}. */ + @Contract("_ -> new") public And andValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -277,6 +285,7 @@ public static Or or(Object... expressions) { * @param expression must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orExpression(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -289,6 +298,7 @@ public Or orExpression(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orField(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -301,6 +311,7 @@ public Or orField(String fieldReference) { * @param value must not be {@literal null}. * @return new instance of {@link Or}. */ + @Contract("_ -> new") public Or orValue(Object value) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java index 36492e2a81..16eca4ec22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketAutoOperation.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.BucketAutoOperation.BucketAutoOperationOutputBuilder; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder; import org.springframework.util.Assert; @@ -38,7 +39,7 @@ public class BucketAutoOperation extends BucketOperationSupport boundaries; - private final Object defaultBucket; + private final @Nullable Object defaultBucket; /** * Creates a new {@link BucketOperation} given a {@link Field group-by field}. @@ -76,7 +78,7 @@ private BucketOperation(BucketOperation bucketOperation, Outputs outputs) { this.defaultBucket = bucketOperation.defaultBucket; } - private BucketOperation(BucketOperation bucketOperation, List boundaries, Object defaultBucket) { + private BucketOperation(BucketOperation bucketOperation, List boundaries, @Nullable Object defaultBucket) { super(bucketOperation); @@ -111,6 +113,7 @@ public String getOperator() { * @param literal must not be {@literal null}. * @return new instance of {@link BucketOperation}. */ + @Contract("_ -> new") public BucketOperation withDefaultBucket(Object literal) { Assert.notNull(literal, "Default bucket literal must not be null"); @@ -124,6 +127,7 @@ public BucketOperation withDefaultBucket(Object literal) { * @param boundaries must not be {@literal null}. * @return new instance of {@link BucketOperation}. */ + @Contract("_ -> new") public BucketOperation withBoundaries(Object... boundaries) { Assert.notNull(boundaries, "Boundaries must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java index e19ad59a3f..3d5ded05c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/BucketOperationSupport.java @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.OutputBuilder; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder; @@ -40,8 +41,8 @@ public abstract class BucketOperationSupport, B extends OutputBuilder> implements FieldsExposingAggregationOperation { - private final Field groupByField; - private final AggregationExpression groupByExpression; + private final @Nullable Field groupByField; + private final @Nullable AggregationExpression groupByExpression; private final Outputs outputs; /** @@ -142,12 +143,17 @@ public Document toDocument(AggregationOperationContext context) { } @Override + public Document toDocument(AggregationOperationContext context) { Document document = new Document(); - document.put("groupBy", groupByExpression == null ? context.getReference(groupByField).toString() - : groupByExpression.toDocument(context)); + if(groupByExpression != null) { + document.put("groupBy", groupByExpression.toDocument(context)); + } else if (groupByField != null) { + document.put("groupBy", context.getReference(groupByField).toString()); + + } if (!outputs.isEmpty()) { document.put("output", outputs.toDocument(context)); @@ -625,7 +631,9 @@ public SpelExpressionOutput(String expression, Object[] parameters) { @Override public Document toDocument(AggregationOperationContext context) { - return (Document) TRANSFORMER.transform(expression, context, params); + + Object o = TRANSFORMER.transform(expression, context, params); + return o instanceof Document document ? document : new Document(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java index f27b7f16cb..e2626c3a16 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ComparisonOperators.java @@ -18,6 +18,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -50,8 +52,8 @@ public static ComparisonOperatorFactory valueOf(AggregationExpression expression public static class ComparisonOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link ComparisonOperatorFactory} for given {@literal fieldReference}. @@ -107,6 +109,7 @@ public Cmp compareToValue(Object value) { return createCmp().compareToValue(value); } + @SuppressWarnings("NullAway") private Cmp createCmp() { return usesFieldRef() ? Cmp.valueOf(fieldReference) : Cmp.valueOf(expression); } @@ -144,6 +147,7 @@ public Eq equalToValue(Object value) { return createEq().equalToValue(value); } + @SuppressWarnings("NullAway") private Eq createEq() { return usesFieldRef() ? Eq.valueOf(fieldReference) : Eq.valueOf(expression); } @@ -181,6 +185,7 @@ public Gt greaterThanValue(Object value) { return createGt().greaterThanValue(value); } + @SuppressWarnings("NullAway") private Gt createGt() { return usesFieldRef() ? Gt.valueOf(fieldReference) : Gt.valueOf(expression); } @@ -218,6 +223,7 @@ public Gte greaterThanEqualToValue(Object value) { return createGte().greaterThanEqualToValue(value); } + @SuppressWarnings("NullAway") private Gte createGte() { return usesFieldRef() ? Gte.valueOf(fieldReference) : Gte.valueOf(expression); } @@ -255,6 +261,7 @@ public Lt lessThanValue(Object value) { return createLt().lessThanValue(value); } + @SuppressWarnings("NullAway") private Lt createLt() { return usesFieldRef() ? Lt.valueOf(fieldReference) : Lt.valueOf(expression); } @@ -292,6 +299,7 @@ public Lte lessThanEqualToValue(Object value) { return createLte().lessThanEqualToValue(value); } + @SuppressWarnings("NullAway") private Lte createLte() { return usesFieldRef() ? Lte.valueOf(fieldReference) : Lte.valueOf(expression); } @@ -329,6 +337,7 @@ public Ne notEqualToValue(Object value) { return createNe().notEqualToValue(value); } + @SuppressWarnings("NullAway") private Ne createNe() { return usesFieldRef() ? Ne.valueOf(fieldReference) : Ne.valueOf(expression); } @@ -384,6 +393,7 @@ public static Cmp valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -396,6 +406,7 @@ public Cmp compareTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -408,6 +419,7 @@ public Cmp compareTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Cmp}. */ + @Contract("_ -> new") public Cmp compareToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -461,6 +473,7 @@ public static Eq valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -473,6 +486,7 @@ public Eq equalTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -485,6 +499,7 @@ public Eq equalTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Eq}. */ + @Contract("_ -> new") public Eq equalToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -538,6 +553,7 @@ public static Gt valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThan(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -550,6 +566,7 @@ public Gt greaterThan(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThan(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -562,6 +579,7 @@ public Gt greaterThan(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Gt}. */ + @Contract("_ -> new") public Gt greaterThanValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -615,6 +633,7 @@ public static Lt valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThan(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -627,6 +646,7 @@ public Lt lessThan(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThan(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -639,6 +659,7 @@ public Lt lessThan(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Lt}. */ + @Contract("_ -> new") public Lt lessThanValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -692,6 +713,7 @@ public static Gte valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -704,6 +726,7 @@ public Gte greaterThanEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -716,6 +739,7 @@ public Gte greaterThanEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Gte}. */ + @Contract("_ -> new") public Gte greaterThanEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -769,6 +793,7 @@ public static Lte valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -781,6 +806,7 @@ public Lte lessThanEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -793,6 +819,7 @@ public Lte lessThanEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Lte}. */ + @Contract("_ -> new") public Lte lessThanEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); @@ -846,6 +873,7 @@ public static Ne valueOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualTo(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -858,6 +886,7 @@ public Ne notEqualTo(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -870,6 +899,7 @@ public Ne notEqualTo(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Ne}. */ + @Contract("_ -> new") public Ne notEqualToValue(Object value) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java index 323a11895b..462d94d6f1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConditionalOperators.java @@ -22,12 +22,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.OtherwiseBuilder; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Switch.CaseOperator; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -211,6 +212,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) { return createThenBuilder().thenValueOf(fieldReference); } + @SuppressWarnings("NullAway") private ThenBuilder createThenBuilder() { if (usesFieldRef()) { @@ -303,6 +305,7 @@ private Object mapCondition(Object condition, AggregationOperationContext contex } } + @SuppressWarnings("NullAway") private Object resolve(Object value, AggregationOperationContext context) { if (value instanceof Field field) { @@ -389,7 +392,7 @@ public interface ThenBuilder extends OrBuilder { */ static final class IfNullOperatorBuilder implements IfNullBuilder, ThenBuilder { - private @Nullable List conditions; + private List conditions; private IfNullOperatorBuilder() { conditions = new ArrayList<>(); @@ -404,6 +407,7 @@ public static IfNullOperatorBuilder newBuilder() { return new IfNullOperatorBuilder(); } + @Contract("_ -> this") public ThenBuilder ifNull(String fieldReference) { Assert.hasText(fieldReference, "FieldReference name must not be null or empty"); @@ -412,6 +416,7 @@ public ThenBuilder ifNull(String fieldReference) { } @Override + @Contract("_ -> this") public ThenBuilder ifNull(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression name must not be null or empty"); @@ -420,25 +425,30 @@ public ThenBuilder ifNull(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ThenBuilder orIfNull(String fieldReference) { return ifNull(fieldReference); } @Override + @Contract("_ -> this") public ThenBuilder orIfNull(AggregationExpression expression) { return ifNull(expression); } + @Contract("_ -> new") public IfNull then(Object value) { return new IfNull(conditions, value); } + @Contract("_ -> new") public IfNull thenValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); return new IfNull(conditions, Fields.field(fieldReference)); } + @Contract("_ -> new") public IfNull thenValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -491,6 +501,7 @@ public static Switch switchCases(List conditions) { * @param value must not be {@literal null}. * @return new instance of {@link Switch}. */ + @Contract("_ -> new") public Switch defaultTo(Object value) { return new Switch(append("default", value)); } @@ -623,6 +634,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document("$cond", condObject); } + @SuppressWarnings("NullAway") private Object resolveValue(AggregationOperationContext context, Object value) { if (value instanceof Document || value instanceof Field) { @@ -886,6 +898,7 @@ public static ConditionalExpressionBuilder newBuilder() { } @Override + @Contract("_ -> this") public ConditionalExpressionBuilder when(Document booleanExpression) { Assert.notNull(booleanExpression, "'Boolean expression' must not be null"); @@ -895,6 +908,7 @@ public ConditionalExpressionBuilder when(Document booleanExpression) { } @Override + @Contract("_ -> this") public ThenBuilder when(CriteriaDefinition criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -903,6 +917,7 @@ public ThenBuilder when(CriteriaDefinition criteria) { } @Override + @Contract("_ -> this") public ThenBuilder when(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression field must not be null"); @@ -911,6 +926,7 @@ public ThenBuilder when(AggregationExpression expression) { } @Override + @Contract("_ -> this") public ThenBuilder when(String booleanField) { Assert.hasText(booleanField, "Boolean field name must not be null or empty"); @@ -919,6 +935,7 @@ public ThenBuilder when(String booleanField) { } @Override + @Contract("_ -> this") public OtherwiseBuilder then(Object thenValue) { Assert.notNull(thenValue, "Then-value must not be null"); @@ -927,6 +944,7 @@ public OtherwiseBuilder then(Object thenValue) { } @Override + @Contract("_ -> this") public OtherwiseBuilder thenValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -935,6 +953,7 @@ public OtherwiseBuilder thenValueOf(String fieldReference) { } @Override + @Contract("_ -> this") public OtherwiseBuilder thenValueOf(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); @@ -943,23 +962,32 @@ public OtherwiseBuilder thenValueOf(AggregationExpression expression) { } @Override + @Contract("_ -> new") public Cond otherwise(Object otherwiseValue) { Assert.notNull(otherwiseValue, "Value must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, otherwiseValue); } @Override + @Contract("_ -> new") public Cond otherwiseValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, Fields.field(fieldReference)); } @Override + @Contract("_ -> new") public Cond otherwiseValueOf(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); + Assert.notNull(condition, "Condition value needs to be set first"); + Assert.notNull(thenValue, "Then value needs to be set first"); return new Cond(condition, thenValue, expression); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java index aa085b2a29..35a6ad061c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ConvertOperators.java @@ -17,8 +17,8 @@ import java.util.Collections; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -242,10 +242,12 @@ public DegreesToRadians convertDegreesToRadians() { return DegreesToRadians.degreesToRadians(valueObject()); } + @SuppressWarnings("NullAway") private Convert createConvert() { return usesFieldRef() ? Convert.convertValueOf(fieldReference) : Convert.convertValueOf(expression); } + @SuppressWarnings("NullAway") private Object valueObject() { return usesFieldRef() ? Fields.field(fieldReference) : expression; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java index ff6ed7e983..7bf8a231ff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DateOperators.java @@ -26,7 +26,8 @@ import java.util.TimeZone; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -848,6 +849,7 @@ public TsSecond tsSecond() { return TsSecond.tsSecond(dateReference()); } + @SuppressWarnings("NullAway") private Object dateReference() { if (usesFieldRef()) { @@ -1076,6 +1078,7 @@ public static DayOfYear dayOfYear(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfYear withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1148,6 +1151,7 @@ public static DayOfMonth dayOfMonth(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfMonth withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1220,6 +1224,7 @@ public static DayOfWeek dayOfWeek(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DayOfWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1292,6 +1297,7 @@ public static Year yearOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Year withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1364,6 +1370,7 @@ public static Month monthOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Month withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1436,6 +1443,7 @@ public static Week weekOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Week withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1508,6 +1516,7 @@ public static Hour hourOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Hour withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1580,6 +1589,7 @@ public static Minute minuteOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Minute withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1652,6 +1662,7 @@ public static Second secondOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Second withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1724,6 +1735,7 @@ public static Millisecond millisecondOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public Millisecond withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1810,6 +1822,7 @@ public static FormatBuilder dateOf(final AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public DateToString withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -1824,6 +1837,7 @@ public DateToString withTimezone(Timezone timezone) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturn(Object value) { return new DateToString(append("onNull", value)); } @@ -1836,6 +1850,7 @@ public DateToString onNullReturn(Object value) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturnValueOf(String fieldReference) { return onNullReturn(Fields.field(fieldReference)); } @@ -1848,6 +1863,7 @@ public DateToString onNullReturnValueOf(String fieldReference) { * @return new instance of {@link DateToString}. * @since 2.1 */ + @Contract("_ -> new") public DateToString onNullReturnValueOf(AggregationExpression expression) { return onNullReturn(expression); } @@ -1973,6 +1989,7 @@ public static IsoDayOfWeek isoDayOfWeek(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoDayOfWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2045,6 +2062,7 @@ public static IsoWeek isoWeekOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoWeek withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2117,6 +2135,7 @@ public static IsoWeekYear isoWeekYearOf(AggregationExpression expression) { * @since 2.1 */ @Override + @Contract("_ -> new") public IsoWeekYear withTimezone(Timezone timezone) { Assert.notNull(timezone, "Timezone must not be null"); @@ -2301,6 +2320,7 @@ public static DateFromPartsWithYear dateFromParts() { * @return new instance. * @throws IllegalArgumentException if given {@literal month} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts month(Object month) { return new DateFromParts(append("month", month)); } @@ -2312,6 +2332,7 @@ public DateFromParts month(Object month) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts monthOf(String fieldReference) { return month(Fields.field(fieldReference)); } @@ -2323,6 +2344,7 @@ public DateFromParts monthOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts monthOf(AggregationExpression expression) { return month(expression); } @@ -2335,6 +2357,7 @@ public DateFromParts monthOf(AggregationExpression expression) { * @return new instance. * @throws IllegalArgumentException if given {@literal day} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts day(Object day) { return new DateFromParts(append("day", day)); } @@ -2346,6 +2369,7 @@ public DateFromParts day(Object day) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts dayOf(String fieldReference) { return day(Fields.field(fieldReference)); } @@ -2357,26 +2381,31 @@ public DateFromParts dayOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public DateFromParts dayOf(AggregationExpression expression) { return day(expression); } @Override + @Contract("_ -> new") public DateFromParts hour(Object hour) { return new DateFromParts(append("hour", hour)); } @Override + @Contract("_ -> new") public DateFromParts minute(Object minute) { return new DateFromParts(append("minute", minute)); } @Override + @Contract("_ -> new") public DateFromParts second(Object second) { return new DateFromParts(append("second", second)); } @Override + @Contract("_ -> new") public DateFromParts millisecond(Object millisecond) { return new DateFromParts(append("millisecond", millisecond)); } @@ -2390,6 +2419,7 @@ public DateFromParts millisecond(Object millisecond) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateFromParts withTimezone(Timezone timezone) { return new DateFromParts(appendTimezone(argumentMap(), timezone)); } @@ -2477,6 +2507,7 @@ public static IsoDateFromPartsWithYear dateFromParts() { * @return new instance. * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeek(Object isoWeek) { return new IsoDateFromParts(append("isoWeek", isoWeek)); } @@ -2488,6 +2519,7 @@ public IsoDateFromParts isoWeek(Object isoWeek) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeekOf(String fieldReference) { return isoWeek(Fields.field(fieldReference)); } @@ -2499,6 +2531,7 @@ public IsoDateFromParts isoWeekOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoWeekOf(AggregationExpression expression) { return isoWeek(expression); } @@ -2511,6 +2544,7 @@ public IsoDateFromParts isoWeekOf(AggregationExpression expression) { * @return new instance. * @throws IllegalArgumentException if given {@literal isoWeek} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeek(Object day) { return new IsoDateFromParts(append("isoDayOfWeek", day)); } @@ -2522,6 +2556,7 @@ public IsoDateFromParts isoDayOfWeek(Object day) { * @return new instance. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeekOf(String fieldReference) { return isoDayOfWeek(Fields.field(fieldReference)); } @@ -2533,26 +2568,31 @@ public IsoDateFromParts isoDayOfWeekOf(String fieldReference) { * @return new instance. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public IsoDateFromParts isoDayOfWeekOf(AggregationExpression expression) { return isoDayOfWeek(expression); } @Override + @Contract("_ -> new") public IsoDateFromParts hour(Object hour) { return new IsoDateFromParts(append("hour", hour)); } @Override + @Contract("_ -> new") public IsoDateFromParts minute(Object minute) { return new IsoDateFromParts(append("minute", minute)); } @Override + @Contract("_ -> new") public IsoDateFromParts second(Object second) { return new IsoDateFromParts(append("second", second)); } @Override + @Contract("_ -> new") public IsoDateFromParts millisecond(Object millisecond) { return new IsoDateFromParts(append("millisecond", millisecond)); } @@ -2566,6 +2606,7 @@ public IsoDateFromParts millisecond(Object millisecond) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public IsoDateFromParts withTimezone(Timezone timezone) { return new IsoDateFromParts(appendTimezone(argumentMap(), timezone)); } @@ -2676,6 +2717,7 @@ public static DateToParts datePartsOf(AggregationExpression expression) { * * @return new instance of {@link DateToParts}. */ + @Contract("_ -> new") public DateToParts iso8601() { return new DateToParts(append("iso8601", true)); } @@ -2689,6 +2731,7 @@ public DateToParts iso8601() { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateToParts withTimezone(Timezone timezone) { return new DateToParts(appendTimezone(argumentMap(), timezone)); } @@ -2733,6 +2776,7 @@ public static DateFromString fromString(Object value) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal fieldReference} is {@literal null}. */ + @Contract("_ -> new") public static DateFromString fromStringOf(String fieldReference) { return fromString(Fields.field(fieldReference)); } @@ -2744,6 +2788,7 @@ public static DateFromString fromStringOf(String fieldReference) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal expression} is {@literal null}. */ + @Contract("_ -> new") public static DateFromString fromStringOf(AggregationExpression expression) { return fromString(expression); } @@ -2757,6 +2802,7 @@ public static DateFromString fromStringOf(AggregationExpression expression) { * @throws IllegalArgumentException if given {@literal timezone} is {@literal null}. */ @Override + @Contract("_ -> new") public DateFromString withTimezone(Timezone timezone) { return new DateFromString(appendTimezone(argumentMap(), timezone)); } @@ -2769,6 +2815,7 @@ public DateFromString withTimezone(Timezone timezone) { * @return new instance of {@link DateFromString}. * @throws IllegalArgumentException if given {@literal format} is {@literal null}. */ + @Contract("_ -> new") public DateFromString withFormat(String format) { Assert.notNull(format, "Format must not be null"); @@ -2838,6 +2885,7 @@ public static DateAdd addValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDateOf(AggregationExpression expression) { return toDate(expression); } @@ -2848,6 +2896,7 @@ public DateAdd toDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDateOf(String fieldReference) { return toDate(Fields.field(fieldReference)); } @@ -2858,6 +2907,7 @@ public DateAdd toDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd toDate(Object dateExpression) { return new DateAdd(append("startDate", dateExpression)); } @@ -2868,6 +2918,7 @@ public DateAdd toDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateAdd withTimezone(Timezone timezone) { return new DateAdd(appendTimezone(argumentMap(), timezone)); } @@ -2935,6 +2986,7 @@ public static DateSubtract subtractValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDateOf(AggregationExpression expression) { return fromDate(expression); } @@ -2945,6 +2997,7 @@ public DateSubtract fromDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDateOf(String fieldReference) { return fromDate(Fields.field(fieldReference)); } @@ -2955,6 +3008,7 @@ public DateSubtract fromDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract fromDate(Object dateExpression) { return new DateSubtract(append("startDate", dateExpression)); } @@ -2965,6 +3019,7 @@ public DateSubtract fromDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateSubtract}. */ + @Contract("_ -> new") public DateSubtract withTimezone(Timezone timezone) { return new DateSubtract(appendTimezone(argumentMap(), timezone)); } @@ -3032,6 +3087,7 @@ public static DateDiff diffValue(Object value, String unit) { * @param expression must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDateOf(AggregationExpression expression) { return toDate(expression); } @@ -3042,6 +3098,7 @@ public DateDiff toDateOf(AggregationExpression expression) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDateOf(String fieldReference) { return toDate(Fields.field(fieldReference)); } @@ -3052,6 +3109,7 @@ public DateDiff toDateOf(String fieldReference) { * @param dateExpression anything that evaluates to a valid date. Must not be {@literal null}. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff toDate(Object dateExpression) { return new DateDiff(append("startDate", dateExpression)); } @@ -3062,6 +3120,7 @@ public DateDiff toDate(Object dateExpression) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateAdd}. */ + @Contract("_ -> new") public DateDiff withTimezone(Timezone timezone) { return new DateDiff(appendTimezone(argumentMap(), timezone)); } @@ -3073,6 +3132,7 @@ public DateDiff withTimezone(Timezone timezone) { * @param day must not be {@literal null}. * @return new instance of {@link DateDiff}. */ + @Contract("_ -> new") public DateDiff startOfWeek(Object day) { return new DateDiff(append("startOfWeek", day)); } @@ -3132,6 +3192,7 @@ public static DateTrunc truncateValue(Object value) { * @param unit must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc to(String unit) { return new DateTrunc(append("unit", unit)); } @@ -3142,6 +3203,7 @@ public DateTrunc to(String unit) { * @param unit must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc to(AggregationExpression unit) { return new DateTrunc(append("unit", unit)); } @@ -3152,6 +3214,7 @@ public DateTrunc to(AggregationExpression unit) { * @param day must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc startOfWeek(java.time.DayOfWeek day) { return startOfWeek(day.name().toLowerCase(Locale.US)); } @@ -3162,6 +3225,7 @@ public DateTrunc startOfWeek(java.time.DayOfWeek day) { * @param day must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc startOfWeek(String day) { return new DateTrunc(append("startOfWeek", day)); } @@ -3172,6 +3236,7 @@ public DateTrunc startOfWeek(String day) { * @param binSize must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(int binSize) { return binSize((Object) binSize); } @@ -3182,6 +3247,7 @@ public DateTrunc binSize(int binSize) { * @param expression must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(AggregationExpression expression) { return binSize((Object) expression); } @@ -3192,6 +3258,7 @@ public DateTrunc binSize(AggregationExpression expression) { * @param binSize must not be {@literal null}. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc binSize(Object binSize) { return new DateTrunc(append("binSize", binSize)); } @@ -3202,6 +3269,7 @@ public DateTrunc binSize(Object binSize) { * @param timezone must not be {@literal null}. Consider {@link Timezone#none()} instead. * @return new instance of {@link DateTrunc}. */ + @Contract("_ -> new") public DateTrunc withTimezone(Timezone timezone) { return new DateTrunc(appendTimezone(argumentMap(), timezone)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java index 0da9343ddf..1a559fd26e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DensifyOperation.java @@ -25,7 +25,8 @@ import java.util.stream.Collectors; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -60,6 +61,9 @@ public static DensifyOperationBuilder builder() { @Override public Document toDocument(AggregationOperationContext context) { + Assert.notNull(field, "Field must be set first"); + Assert.notNull(range, "Range must be set first"); + Document densify = new Document(); densify.put("field", context.getReference(field).getRaw()); if (!ObjectUtils.isEmpty(partitionBy)) { @@ -149,9 +153,9 @@ default Document toDocument() { public static abstract class DensifyRange implements Range { private @Nullable DensifyUnit unit; - private Number step; + private @Nullable Number step; - public DensifyRange(DensifyUnit unit) { + public DensifyRange(@Nullable DensifyUnit unit) { this.unit = unit; } @@ -172,6 +176,7 @@ public Document toDocument(AggregationOperationContext ctx) { * @param step must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public DensifyRange incrementBy(Number step) { this.step = step; return this; @@ -183,6 +188,7 @@ public DensifyRange incrementBy(Number step) { * @param step must not be {@literal null}. * @return this. */ + @Contract("_, _ -> this") public DensifyRange incrementBy(Number step, DensifyUnit unit) { this.step = step; return unit(unit); @@ -194,6 +200,7 @@ public DensifyRange incrementBy(Number step, DensifyUnit unit) { * @param unit * @return this. */ + @Contract("_ -> this") public DensifyRange unit(DensifyUnit unit) { this.unit = unit; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java index 7f260c3785..431215e852 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentEnhancingOperation.java @@ -22,6 +22,7 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.util.Assert; @@ -105,7 +106,11 @@ private static Document toSetEntry(Entry entry, AggregationOpera return new Document(field, value); } - private static Object computeValue(Object value, AggregationOperationContext context) { + private static @Nullable Object computeValue(@Nullable Object value, AggregationOperationContext context) { + + if(value == null) { + return value; + } if (value instanceof Field field) { return context.getReference(field).toString(); @@ -154,7 +159,7 @@ static class ExpressionProjection { this.params = parameters.clone(); } - Object toExpression(AggregationOperationContext context) { + @Nullable Object toExpression(AggregationOperationContext context) { return TRANSFORMER.transform(expression, context, params); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java index ff63ad834d..a0cd1d056a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/DocumentOperators.java @@ -18,6 +18,7 @@ import java.util.Collections; import org.bson.Document; +import org.springframework.lang.Contract; /** * Gateway to {@literal document expressions} such as {@literal $rank, $documentNumber, etc.} @@ -190,6 +191,7 @@ public static Shift shift(AggregationExpression expression) { * @param shiftBy value to add to the current position. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift by(int shiftBy) { return new Shift(append("by", shiftBy)); } @@ -200,6 +202,7 @@ public Shift by(int shiftBy) { * @param value must not be {@literal null}. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift defaultTo(Object value) { return new Shift(append("default", value)); } @@ -210,6 +213,7 @@ public Shift defaultTo(Object value) { * @param expression must not be {@literal null}. * @return new instance of {@link Shift}. */ + @Contract("_ -> new") public Shift defaultToValueOf(AggregationExpression expression) { return defaultTo(expression); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java index 56f20dde17..dfdc2d620c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/EvaluationOperators.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -50,8 +51,8 @@ public static EvaluationOperatorFactory valueOf(AggregationExpression expression public static class EvaluationOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link EvaluationOperatorFactory} for given {@literal fieldReference}. @@ -82,6 +83,7 @@ public EvaluationOperatorFactory(AggregationExpression expression) { * * @return new instance of {@link Expr}. */ + @SuppressWarnings("NullAway") public Expr expr() { return usesFieldRef() ? Expr.valueOf(fieldReference) : Expr.valueOf(expression); } @@ -91,6 +93,7 @@ public Expr expr() { * * @return new instance of {@link Expr}. */ + @SuppressWarnings("NullAway") public LastObservationCarriedForward locf() { return usesFieldRef() ? LastObservationCarriedForward.locfValueOf(fieldReference) : LastObservationCarriedForward.locfValueOf(expression); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java index 458bc43437..703d5d5f06 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFields.java @@ -21,8 +21,8 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CompositeIterator; import org.springframework.util.ObjectUtils; @@ -154,8 +154,7 @@ public ExposedFields and(ExposedField field) { * @param name must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - public ExposedField getField(String name) { + public @Nullable ExposedField getField(String name) { for (ExposedField field : this) { if (field.canBeReferredToBy(name)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java index 131fa8a845..1639a54d48 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ExposedFieldsAggregationOperationContext.java @@ -17,11 +17,10 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.DirectFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -119,8 +118,7 @@ private FieldReference getReference(@Nullable Field field, String name) { * @param name must not be {@literal null}. * @return the resolved reference or {@literal null}. */ - @Nullable - protected FieldReference resolveExposedField(@Nullable Field field, String name) { + protected @Nullable FieldReference resolveExposedField(@Nullable Field field, String name) { ExposedField exposedField = exposedFields.getField(name); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java index f5c73dd09c..d1ca95f659 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/FacetOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.BucketOperationSupport.Output; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java index 83fc7c2b87..7f574a850e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/Fields.java @@ -23,8 +23,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -145,14 +146,17 @@ private Fields(Fields existing, Field tail) { * @param name must not be {@literal null}. * @return */ + @Contract("_ -> new") public Fields and(String name) { return and(new AggregationField(name)); } + @Contract("_ -> new") public Fields and(String name, String target) { return and(new AggregationField(name, target)); } + @Contract("_ -> new") public Fields and(Field field) { return new Fields(this, field); } @@ -172,8 +176,7 @@ public int size() { return fields.size(); } - @Nullable - public Field getField(String name) { + public @Nullable Field getField(String name) { for (Field field : fields) { if (field.getName().equals(name)) { @@ -206,7 +209,7 @@ static class AggregationField implements Field { private final String raw; private final String name; - private final String target; + private final @Nullable String target; /** * Creates an aggregation field with the given {@code name}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java index f4a5fb4498..bcfc64f2b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperation.java @@ -19,8 +19,9 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.NearQuery; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -80,6 +81,7 @@ private GeoNearOperation(NearQuery nearQuery, String distanceField, @Nullable St * @return new instance of {@link GeoNearOperation}. * @since 2.1 */ + @Contract("_ -> new") public GeoNearOperation useIndex(String key) { return new GeoNearOperation(nearQuery, distanceField, key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java index 72a917c599..ad1f8ae643 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GraphLookupOperation.java @@ -21,10 +21,11 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.query.CriteriaDefinition; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -35,14 +36,16 @@ * We recommend to use the static factory method {@link Aggregation#graphLookup(String)} instead of creating instances * of this class directly. * - * @see https://docs.mongodb.org/manual/reference/aggregation/graphLookup/ + * @see https://docs.mongodb.org/manual/reference/aggregation/graphLookup/ * @author Mark Paluch * @author Christoph Strobl * @since 1.10 */ public class GraphLookupOperation implements InheritsFieldsAggregationOperation { - private static final Set> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class, Field.class, Document.class); + private static final Set> ALLOWED_START_TYPES = Set.of(AggregationExpression.class, String.class, + Field.class, Document.class); private final String from; private final List startWith; @@ -126,7 +129,7 @@ public ExposedFields getFields() { List fields = new ArrayList<>(2); fields.add(new ExposedField(as, true)); - if(depthField != null) { + if (depthField != null) { fields.add(new ExposedField(depthField, true)); } return ExposedFields.from(fields.toArray(new ExposedField[0])); @@ -217,10 +220,11 @@ static final class GraphLookupOperationFromBuilder implements FromBuilder, StartWithBuilder, ConnectFromBuilder, ConnectToBuilder { private @Nullable String from; - private @Nullable List startWith; + private @Nullable List startWith; private @Nullable String connectFrom; @Override + @Contract("_ -> this") public StartWithBuilder from(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null or empty"); @@ -230,6 +234,7 @@ public StartWithBuilder from(String collectionName) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(String... fieldReferences) { Assert.notNull(fieldReferences, "FieldReferences must not be null"); @@ -246,6 +251,7 @@ public ConnectFromBuilder startWith(String... fieldReferences) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(AggregationExpression... expressions) { Assert.notNull(expressions, "AggregationExpressions must not be null"); @@ -256,6 +262,7 @@ public ConnectFromBuilder startWith(AggregationExpression... expressions) { } @Override + @Contract("_ -> this") public ConnectFromBuilder startWith(Object... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -297,6 +304,7 @@ private void assertStartWithType(Object expression) { } @Override + @Contract("_ -> this") public ConnectToBuilder connectFrom(String fieldName) { Assert.hasText(fieldName, "ConnectFrom must not be null or empty"); @@ -306,10 +314,14 @@ public ConnectToBuilder connectFrom(String fieldName) { } @Override + @Contract("_ -> new") public GraphLookupOperationBuilder connectTo(String fieldName) { Assert.hasText(fieldName, "ConnectTo must not be null or empty"); + Assert.notNull(from, "From must not be null"); + Assert.notNull(startWith, "startWith must ne set first"); + Assert.notNull(connectFrom, "ConnectFrom must be set first"); return new GraphLookupOperationBuilder(from, startWith, connectFrom, fieldName); } } @@ -327,8 +339,7 @@ public static final class GraphLookupOperationBuilder { private @Nullable Field depthField; private @Nullable CriteriaDefinition restrictSearchWithMatch; - private GraphLookupOperationBuilder(String from, List startWith, String connectFrom, - String connectTo) { + private GraphLookupOperationBuilder(String from, List startWith, String connectFrom, String connectTo) { this.from = from; this.startWith = new ArrayList<>(startWith); @@ -342,6 +353,7 @@ private GraphLookupOperationBuilder(String from, List startWit * @param numberOfRecursions must be greater or equal to zero. * @return this. */ + @Contract("_ -> this") public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) { Assert.isTrue(numberOfRecursions >= 0, "Max depth must be >= 0"); @@ -356,6 +368,7 @@ public GraphLookupOperationBuilder maxDepth(long numberOfRecursions) { * @param fieldName must not be {@literal null} or empty. * @return this. */ + @Contract("_ -> this") public GraphLookupOperationBuilder depthField(String fieldName) { Assert.hasText(fieldName, "Depth field name must not be null or empty"); @@ -370,6 +383,7 @@ public GraphLookupOperationBuilder depthField(String fieldName) { * @param criteriaDefinition must not be {@literal null}. * @return */ + @Contract("_ -> this") public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinition) { Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null"); @@ -385,6 +399,7 @@ public GraphLookupOperationBuilder restrict(CriteriaDefinition criteriaDefinitio * @param fieldName must not be {@literal null} or empty. * @return the final {@link GraphLookupOperation}. */ + @Contract("_ -> new") public GraphLookupOperation as(String fieldName) { Assert.hasText(fieldName, "As field name must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java index 10d58a7682..b6d36f1baf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/GroupOperation.java @@ -20,10 +20,10 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -497,6 +497,7 @@ public Operation withAlias(String key) { } public ExposedField asField() { + Assert.notNull(key, "Key must be set first"); return new ExposedField(key, true); } @@ -506,10 +507,12 @@ public Document toDocument(AggregationOperationContext context) { if(op == null && value instanceof Document) { return new Document(key, value); } + + Assert.notNull(op, "Operation keyword must be set"); return new Document(key, new Document(op.toString(), value)); } - public Object getValue(AggregationOperationContext context) { + public @Nullable Object getValue(AggregationOperationContext context) { if (reference == null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java index ca6a2e2754..739b7c52a9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/InheritingExposedFieldsAggregationOperationContext.java @@ -16,8 +16,8 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; /** * {@link ExposedFieldsAggregationOperationContext} that inherits fields from its parent diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java index 282ffbd9e0..a0e0cc03cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java @@ -22,6 +22,7 @@ import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; +import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -281,6 +282,7 @@ public static FromBuilder newBuilder() { } @Override + @Contract("_ -> this") public LocalFieldBuilder from(String name) { Assert.hasText(name, "'From' must not be null or empty"); @@ -289,6 +291,7 @@ public LocalFieldBuilder from(String name) { } @Override + @Contract("_ -> this") public AsBuilder foreignField(String name) { Assert.hasText(name, "'ForeignField' must not be null or empty"); @@ -297,6 +300,7 @@ public AsBuilder foreignField(String name) { } @Override + @Contract("_ -> this") public ForeignFieldBuilder localField(String name) { Assert.hasText(name, "'LocalField' must not be null or empty"); @@ -305,6 +309,7 @@ public ForeignFieldBuilder localField(String name) { } @Override + @Contract("_ -> this") public PipelineBuilder let(Let let) { Assert.notNull(let, "Let must not be null"); @@ -313,6 +318,7 @@ public PipelineBuilder let(Let let) { } @Override + @Contract("_ -> this") public AsBuilder pipeline(AggregationPipeline pipeline) { Assert.notNull(pipeline, "Pipeline must not be null"); @@ -321,9 +327,11 @@ public AsBuilder pipeline(AggregationPipeline pipeline) { } @Override + @Contract("_ -> new") public LookupOperation as(String name) { Assert.hasText(name, "'As' must not be null or empty"); + Assert.notNull(from, "From must be set first"); as = new ExposedField(Fields.field(name), true); return new LookupOperation(from, localField, foreignField, let, pipeline, as); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java index da1dbfc027..5f736b55a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MatchOperation.java @@ -17,6 +17,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.util.Assert; @@ -37,8 +38,8 @@ */ public class MatchOperation implements AggregationOperation { - private final CriteriaDefinition criteriaDefinition; - private final AggregationExpression expression; + private final @Nullable CriteriaDefinition criteriaDefinition; + private final @Nullable AggregationExpression expression; /** * Creates a new {@link MatchOperation} for the given {@link CriteriaDefinition}. @@ -68,6 +69,7 @@ public MatchOperation(AggregationExpression expression) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { return new Document(getOperator(), diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java index 314f83fc7c..bda9a3330d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/MergeOperation.java @@ -22,10 +22,11 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -415,7 +416,7 @@ public Document toDocument(AggregationOperationContext context) { */ public static class MergeOperationBuilder { - private String collection; + private @Nullable String collection; private @Nullable String database; private UniqueMergeId id = UniqueMergeId.id(); private @Nullable Let let; @@ -430,6 +431,7 @@ public MergeOperationBuilder() {} * @param collection must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder intoCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -444,6 +446,7 @@ public MergeOperationBuilder intoCollection(String collection) { * @param database must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder inDatabase(String database) { this.database = database; @@ -456,6 +459,7 @@ public MergeOperationBuilder inDatabase(String database) { * @param into must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder into(MergeOperationTarget into) { this.database = into.database; @@ -469,6 +473,7 @@ public MergeOperationBuilder into(MergeOperationTarget into) { * @param target must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder target(MergeOperationTarget target) { return into(target); } @@ -482,6 +487,7 @@ public MergeOperationBuilder target(MergeOperationTarget target) { * @param fields must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder on(String... fields) { return id(UniqueMergeId.ofIdFields(fields)); } @@ -493,6 +499,7 @@ public MergeOperationBuilder on(String... fields) { * @param id must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder id(UniqueMergeId id) { this.id = id; @@ -506,6 +513,7 @@ public MergeOperationBuilder id(UniqueMergeId id) { * @param let the variable expressions * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder let(Let let) { this.let = let; @@ -519,6 +527,7 @@ public MergeOperationBuilder let(Let let) { * @param let the variable expressions * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder exposeVariablesOf(Let let) { return let(let); } @@ -529,6 +538,7 @@ public MergeOperationBuilder exposeVariablesOf(Let let) { * @param whenMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) { this.whenMatched = whenMatched; @@ -541,6 +551,7 @@ public MergeOperationBuilder whenMatched(WhenDocumentsMatch whenMatched) { * @param whenMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) { return whenMatched(whenMatched); } @@ -551,6 +562,7 @@ public MergeOperationBuilder whenDocumentsMatch(WhenDocumentsMatch whenMatched) * @param aggregation must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) { return whenMatched(WhenDocumentsMatch.updateWith(aggregation)); } @@ -561,6 +573,7 @@ public MergeOperationBuilder whenDocumentsMatchApply(Aggregation aggregation) { * @param whenNotMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatched) { this.whenNotMatched = whenNotMatched; @@ -573,6 +586,7 @@ public MergeOperationBuilder whenNotMatched(WhenDocumentsDontMatch whenNotMatche * @param whenNotMatched must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenNotMatched) { return whenNotMatched(whenNotMatched); } @@ -580,7 +594,10 @@ public MergeOperationBuilder whenDocumentsDontMatch(WhenDocumentsDontMatch whenN /** * @return new instance of {@link MergeOperation}. */ + @Contract("-> new") public MergeOperation build() { + + Assert.notNull(collection, "Collection must not be null"); return new MergeOperation(new MergeOperationTarget(database, collection), id, let, whenMatched, whenNotMatched); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java index c553a7be02..a5124320f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/NestedDelegatingExpressionAggregationOperationContext.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExpressionFieldReference; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; import org.springframework.util.Assert; @@ -57,7 +58,7 @@ public Document getMappedObject(Document document) { } @Override - public Document getMappedObject(Document document, Class type) { + public Document getMappedObject(Document document, @Nullable Class type) { return delegate.getMappedObject(document, type); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java index 25189241b7..4e21ab7bde 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ObjectOperators.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.bson.Document; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -277,7 +278,7 @@ public Document toDocument(Object value, AggregationOperationContext context) { return super.toDocument(potentiallyExtractSingleValue(value), context); } - @SuppressWarnings("unchecked") + @SuppressWarnings("NullAway") private Object potentiallyExtractSingleValue(Object value) { if (value instanceof Collection collection && collection.size() == 1) { @@ -385,6 +386,7 @@ public static GetField getField(Field field) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public GetField of(String fieldRef) { return of(Fields.field(fieldRef)); } @@ -396,6 +398,7 @@ public GetField of(String fieldRef) { * @param expression must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public GetField of(AggregationExpression expression) { return of((Object) expression); } @@ -459,6 +462,7 @@ public static SetField field(Field field) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link GetField}. */ + @Contract("_ -> new") public SetField input(String fieldRef) { return input(Fields.field(fieldRef)); } @@ -470,6 +474,7 @@ public SetField input(String fieldRef) { * @param expression must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField input(AggregationExpression expression) { return input((Object) expression); } @@ -481,6 +486,7 @@ public SetField input(AggregationExpression expression) { * @param fieldRef must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") private SetField input(Object fieldRef) { return new SetField(append("input", fieldRef)); } @@ -491,6 +497,7 @@ private SetField input(Object fieldRef) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValueOf(String fieldReference) { return toValue(Fields.field(fieldReference)); } @@ -502,6 +509,7 @@ public SetField toValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValueOf(AggregationExpression expression) { return toValue(expression); } @@ -512,6 +520,7 @@ public SetField toValueOf(AggregationExpression expression) { * @param value * @return new instance of {@link SetField}. */ + @Contract("_ -> new") public SetField toValue(Object value) { return new SetField(append("value", value)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java index 51520f0868..7dbed3a855 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/OutOperation.java @@ -16,8 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -73,6 +74,7 @@ private OutOperation(@Nullable String databaseName, String collectionName, @Null * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation in(@Nullable String database) { return new OutOperation(database, collectionName, uniqueKey, mode); } @@ -102,6 +104,7 @@ public OutOperation in(@Nullable String database) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation uniqueKey(@Nullable String key) { Document uniqueKey = key == null ? null : BsonUtils.toDocumentOrElse(key, it -> new Document(it, 1)); @@ -126,6 +129,7 @@ public OutOperation uniqueKey(@Nullable String key) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation uniqueKeyOf(Iterable fields) { Assert.notNull(fields, "Fields must not be null"); @@ -144,6 +148,7 @@ public OutOperation uniqueKeyOf(Iterable fields) { * @return new instance of {@link OutOperation}. * @since 2.2 */ + @Contract("_ -> new") public OutOperation mode(OutMode mode) { Assert.notNull(mode, "Mode must not be null"); @@ -158,6 +163,7 @@ public OutOperation mode(OutMode mode) { * @see OutMode#REPLACE_COLLECTION * @since 2.2 */ + @Contract("-> new") public OutOperation replaceCollection() { return mode(OutMode.REPLACE_COLLECTION); } @@ -170,6 +176,7 @@ public OutOperation replaceCollection() { * @see OutMode#REPLACE * @since 2.2 */ + @Contract("-> new") public OutOperation replaceDocuments() { return mode(OutMode.REPLACE); } @@ -182,6 +189,7 @@ public OutOperation replaceDocuments() { * @see OutMode#INSERT * @since 2.2 */ + @Contract("-> new") public OutOperation insertDocuments() { return mode(OutMode.INSERT); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java index 9524171fed..54ed40b035 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/PrefixingDelegatingAggregationOperationContext.java @@ -25,8 +25,8 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; -import org.springframework.lang.Nullable; /** * {@link AggregationOperationContext} implementation prefixing non-command keys on root level with the given prefix. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java index 35db2214f5..af7cf5bfb2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ProjectionOperation.java @@ -23,13 +23,14 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.IfNull; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.Fields.AggregationField; import org.springframework.data.mongodb.core.aggregation.ProjectionOperation.ProjectionOperationBuilder.FieldProjection; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -149,6 +150,7 @@ public ProjectionOperationBuilder and(AggregationExpression expression) { * @param fieldNames must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andExclude(String... fieldNames) { List excludeProjections = FieldProjection.from(Fields.fields(fieldNames), false); @@ -161,6 +163,7 @@ public ProjectionOperation andExclude(String... fieldNames) { * @param fieldNames must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andInclude(String... fieldNames) { List projections = FieldProjection.from(Fields.fields(fieldNames), true); @@ -173,6 +176,7 @@ public ProjectionOperation andInclude(String... fieldNames) { * @param fields must not be {@literal null}. * @return */ + @Contract("_ -> new") public ProjectionOperation andInclude(Fields fields) { return new ProjectionOperation(this.projections, FieldProjection.from(fields, true)); } @@ -185,6 +189,7 @@ public ProjectionOperation andInclude(Fields fields) { * @return new instance of {@link ProjectionOperation}. * @since 2.2 */ + @Contract("_ -> new") public ProjectionOperation asArray(String name) { return new ProjectionOperation(Collections.emptyList(), @@ -402,7 +407,7 @@ public Document toDocument(AggregationOperationContext context) { return new Document(getExposedField().getName(), toMongoExpression(context, expression, params)); } - protected static Object toMongoExpression(AggregationOperationContext context, String expression, + protected static @Nullable Object toMongoExpression(AggregationOperationContext context, String expression, Object[] params) { return TRANSFORMER.transform(expression, context, params); } @@ -1780,6 +1785,7 @@ public ArrayProjectionOperationBuilder(ProjectionOperation target) { * @param expression * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(AggregationExpression expression) { Assert.notNull(expression, "AggregationExpression must not be null"); @@ -1794,6 +1800,7 @@ public ArrayProjectionOperationBuilder and(AggregationExpression expression) { * @param field * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(Field field) { Assert.notNull(field, "Field must not be null"); @@ -1808,6 +1815,7 @@ public ArrayProjectionOperationBuilder and(Field field) { * @param value * @return */ + @Contract("_ -> this") public ArrayProjectionOperationBuilder and(Object value) { this.projections.add(value); @@ -1820,6 +1828,7 @@ public ArrayProjectionOperationBuilder and(Object value) { * @param name The target property name. Must not be {@literal null}. * @return new instance of {@link ArrayProjectionOperationBuilder}. */ + @Contract("_ -> new") public ProjectionOperation as(String name) { return new ProjectionOperation(target.projections, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java index a370016356..5f16fcfc16 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/RedactOperation.java @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ConditionalOperators.Cond.ThenBuilder; import org.springframework.data.mongodb.core.query.CriteriaDefinition; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -33,7 +35,8 @@ * * * @author Christoph Strobl - * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/ + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/redact/ * @since 3.0 */ public class RedactOperation implements AggregationOperation { @@ -94,9 +97,9 @@ public static RedactOperationBuilder builder() { */ public static class RedactOperationBuilder { - private Object when; - private Object then; - private Object otherwise; + private @Nullable Object when; + private @Nullable Object then; + private @Nullable Object otherwise; private RedactOperationBuilder() { @@ -108,6 +111,7 @@ private RedactOperationBuilder() { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(CriteriaDefinition criteria) { this.when = criteria; @@ -120,6 +124,7 @@ public RedactOperationBuilder when(CriteriaDefinition criteria) { * @param condition must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(AggregationExpression condition) { this.when = condition; @@ -132,6 +137,7 @@ public RedactOperationBuilder when(AggregationExpression condition) { * @param condition must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder when(Document condition) { this.when = condition; @@ -143,6 +149,7 @@ public RedactOperationBuilder when(Document condition) { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenDescend() { return then(DESCEND); } @@ -152,6 +159,7 @@ public RedactOperationBuilder thenDescend() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenKeep() { return then(KEEP); } @@ -161,6 +169,7 @@ public RedactOperationBuilder thenKeep() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder thenPrune() { return then(PRUNE); } @@ -172,6 +181,7 @@ public RedactOperationBuilder thenPrune() { * @param then must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder then(Object then) { this.then = then; @@ -183,6 +193,7 @@ public RedactOperationBuilder then(Object then) { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwiseDescend() { return otherwise(DESCEND); } @@ -192,6 +203,7 @@ public RedactOperationBuilder otherwiseDescend() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwiseKeep() { return otherwise(KEEP); } @@ -201,6 +213,7 @@ public RedactOperationBuilder otherwiseKeep() { * * @return this. */ + @Contract("-> this") public RedactOperationBuilder otherwisePrune() { return otherwise(PRUNE); } @@ -212,6 +225,7 @@ public RedactOperationBuilder otherwisePrune() { * @param otherwise must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public RedactOperationBuilder otherwise(Object otherwise) { this.otherwise = otherwise; return this; @@ -220,7 +234,12 @@ public RedactOperationBuilder otherwise(Object otherwise) { /** * @return new instance of {@link RedactOperation}. */ + @Contract("-> new") public RedactOperation build() { + + Assert.notNull(then, "Then must be set first"); + Assert.notNull(otherwise, "Otherwise must be set first"); + return new RedactOperation(when().then(then).otherwise(otherwise)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java index 130182a001..ec306eb6c5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ReplaceRootOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.expression.spel.ast.Projection; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -457,6 +458,7 @@ public DocumentContributor(Object value) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { Document document = new Document("$set", value); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java index 9eab041e88..ed615d9863 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ScriptOperators.java @@ -22,15 +22,15 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorBuilder; import org.springframework.data.mongodb.core.aggregation.ScriptOperators.Accumulator.AccumulatorInitBuilder; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** - * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations. - *
+ * Gateway to {@literal $function} and {@literal $accumulator} aggregation operations.
* Using {@link ScriptOperators} as part of the {@link Aggregation} requires MongoDB server to have * server-side JavaScript execution * enabled. @@ -53,8 +53,8 @@ public static Function function(String body) { } /** - * Create a custom $accumulator operator - * in Javascript. + * Create a custom $accumulator + * operator in Javascript. * * @return new instance of {@link AccumulatorInitBuilder}. */ @@ -74,8 +74,7 @@ public static AccumulatorInitBuilder accumulatorBuilder() { * lang: "js" * } * } - * - *
+ *
* {@link Function} cannot be used as part of {@link org.springframework.data.mongodb.core.schema.MongoJsonSchema * schema} validation query expression.
* NOTE: Server-Side JavaScript @@ -150,10 +149,12 @@ List getArgs() { return get(Fields.ARGS.toString()); } + @Nullable String getBody() { return get(Fields.BODY.toString()); } + @Nullable String getLang() { return get(Fields.LANG.toString()); } @@ -178,8 +179,7 @@ public String toString() { * {@link Accumulator} defines a custom aggregation * $accumulator operator, * one that maintains its state (e.g. totals, maximums, minimums, and related data) as documents progress through the - * pipeline, in JavaScript. - *
+ * pipeline, in JavaScript.
* * { * $accumulator: { @@ -192,8 +192,7 @@ public String toString() { * lang: "js" * } * } - * - *
+ *
* {@link Accumulator} can be used as part of {@link GroupOperation $group}, {@link BucketOperation $bucket} and * {@link BucketAutoOperation $bucketAuto} pipeline stages.
* NOTE: Server-Side JavaScript @@ -240,8 +239,7 @@ public interface AccumulatorInitBuilder { /** * Define the {@code init} {@link Function} for the {@link Accumulator accumulators} initial state. The function - * receives its arguments from the {@link Function#args(Object...) initArgs} array expression. - *
+ * receives its arguments from the {@link Function#args(Object...) initArgs} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -253,13 +251,16 @@ public interface AccumulatorInitBuilder { * @return this. */ default AccumulatorAccumulateBuilder init(Function function) { - return init(function.getBody()).initArgs(function.getArgs()); + + Assert.notNull(function.getBody(), "Function body must not be null"); + + List args = function.getArgs(); + return init(function.getBody()).initArgs(args != null ? args : List.of()); } /** * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives - * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression. - *
+ * its arguments from the {@link AccumulatorInitArgsBuilder#initArgs(Object...)} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -307,8 +308,7 @@ public interface AccumulatorAccumulateBuilder { /** * Set the {@code accumulate} {@link Function} that updates the state for each document. The functions first * argument is the current {@code state}, additional arguments can be defined via {@link Function#args(Object...) - * accumulateArgs}. - *
+ * accumulateArgs}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -320,14 +320,17 @@ public interface AccumulatorAccumulateBuilder { * @return this. */ default AccumulatorMergeBuilder accumulate(Function function) { - return accumulate(function.getBody()).accumulateArgs(function.getArgs()); + + Assert.notNull(function.getBody(), "Function body must not be null"); + + List args = function.getArgs(); + return accumulate(function.getBody()).accumulateArgs(args != null ? args : List.of()); } /** * Set the {@code accumulate} function that updates the state for each document. The functions first argument is * the current {@code state}, additional arguments can be defined via - * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}. - *
+ * {@link AccumulatorAccumulateArgsBuilder#accumulateArgs(Object...)}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -369,8 +372,7 @@ public interface AccumulatorMergeBuilder { /** * Set the {@code merge} function used to merge two internal states.
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its - * memory limit. - *
+ * memory limit.
* * function(state1, state2) { * ... @@ -388,8 +390,7 @@ public interface AccumulatorFinalizeBuilder { /** * Set the {@code finalize} function used to update the result of the accumulation when all documents have been - * processed. - *
+ * processed.
* * function(state) { * ... @@ -414,18 +415,17 @@ static class AccumulatorBuilder implements AccumulatorInitBuilder, AccumulatorInitArgsBuilder, AccumulatorAccumulateBuilder, AccumulatorAccumulateArgsBuilder, AccumulatorMergeBuilder, AccumulatorFinalizeBuilder { - private List initArgs; - private String initFunction; - private List accumulateArgs; - private String accumulateFunction; - private String mergeFunction; - private String finalizeFunction; + private @Nullable List initArgs; + private @Nullable String initFunction; + private @Nullable List accumulateArgs; + private @Nullable String accumulateFunction; + private @Nullable String mergeFunction; + private @Nullable String finalizeFunction; private String lang = "js"; /** * Define the {@code init} function for the {@link Accumulator accumulators} initial state. The function receives - * its arguments from the {@link #initArgs(Object...)} array expression. - *
+ * its arguments from the {@link #initArgs(Object...)} array expression.
* * function(initArg1, initArg2, ...) { * ... @@ -437,6 +437,7 @@ static class AccumulatorBuilder * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder init(String function) { this.initFunction = function; @@ -450,6 +451,7 @@ public AccumulatorBuilder init(String function) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder initArgs(List args) { Assert.notNull(args, "Args must not be null"); @@ -460,8 +462,7 @@ public AccumulatorBuilder initArgs(List args) { /** * Set the {@code accumulate} function that updates the state for each document. The functions first argument is - * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}. - *
+ * the current {@code state}, additional arguments can be defined via {@link #accumulateArgs(Object...)}.
* * function(state, accumArg1, accumArg2, ...) { * ... @@ -473,6 +474,7 @@ public AccumulatorBuilder initArgs(List args) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder accumulate(String function) { Assert.notNull(function, "Accumulate function must not be null"); @@ -488,6 +490,7 @@ public AccumulatorBuilder accumulate(String function) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder accumulateArgs(List args) { Assert.notNull(args, "Args must not be null"); @@ -499,8 +502,7 @@ public AccumulatorBuilder accumulateArgs(List args) { /** * Set the {@code merge} function used to merge two internal states.
* This might be required because the operation is run on a sharded cluster or when the operator exceeds its - * memory limit. - *
+ * memory limit.
* * function(state1, state2) { * ... @@ -512,6 +514,7 @@ public AccumulatorBuilder accumulateArgs(List args) { * @return this. */ @Override + @Contract("_ -> this") public AccumulatorBuilder merge(String function) { Assert.notNull(function, "Merge function must not be null"); @@ -526,6 +529,7 @@ public AccumulatorBuilder merge(String function) { * @param lang must not be {@literal null}. Default is {@literal js}. * @return this. */ + @Contract("_ -> this") public AccumulatorBuilder lang(String lang) { Assert.hasText(lang, "Lang must not be null nor empty; The default would be 'js'"); @@ -536,8 +540,7 @@ public AccumulatorBuilder lang(String lang) { /** * Set the {@code finalize} function used to update the result of the accumulation when all documents have been - * processed. - *
+ * processed.
* * function(state) { * ... @@ -549,6 +552,7 @@ public AccumulatorBuilder lang(String lang) { * @return new instance of {@link Accumulator}. */ @Override + @Contract("_ -> new") public Accumulator finalize(String function) { Assert.notNull(function, "Finalize function must not be null"); @@ -562,6 +566,7 @@ public Accumulator finalize(String function) { } @Override + @Contract("-> new") public Accumulator build() { return new Accumulator(createArgumentMap()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java index 9da80c4668..4db0181d39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SelectionOperators.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.springframework.data.domain.Sort; +import org.springframework.lang.Contract; /** * Gateway to {@literal selection operators} such as {@literal $bottom}. @@ -69,6 +70,7 @@ public static Bottom bottom(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -80,10 +82,12 @@ public Bottom limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom limit(AggregationExpression expression) { return limit((Object) expression); } + @Contract("_ -> new") private Bottom limit(Object value) { return new Bottom(append("n", value)); } @@ -94,6 +98,7 @@ private Bottom limit(Object value) { * @param sort must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom sortBy(Sort sort) { return new Bottom(append("sortBy", sort)); } @@ -104,6 +109,7 @@ public Bottom sortBy(Sort sort) { * @param out must not be {@literal null}. * @return new instance of {@link Bottom}. */ + @Contract("_ -> new") public Bottom output(Fields out) { return new Bottom(append("output", out)); } @@ -115,6 +121,7 @@ public Bottom output(Fields out) { * @return new instance of {@link Bottom}. * @see #output(Fields) */ + @Contract("_ -> new") public Bottom output(String... fieldNames) { return output(Fields.fields(fieldNames)); } @@ -126,6 +133,7 @@ public Bottom output(String... fieldNames) { * @return new instance of {@link Bottom}. * @see #output(Fields) */ + @Contract("_ -> new") public Bottom output(AggregationExpression... out) { return new Bottom(append("output", Arrays.asList(out))); } @@ -172,6 +180,7 @@ public static Top top(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -183,6 +192,7 @@ public Top limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top limit(AggregationExpression expression) { return limit((Object) expression); } @@ -197,6 +207,7 @@ private Top limit(Object value) { * @param sort must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top sortBy(Sort sort) { return new Top(append("sortBy", sort)); } @@ -207,6 +218,7 @@ public Top sortBy(Sort sort) { * @param out must not be {@literal null}. * @return new instance of {@link Top}. */ + @Contract("_ -> new") public Top output(Fields out) { return new Top(append("output", out)); } @@ -218,6 +230,7 @@ public Top output(Fields out) { * @return new instance of {@link Top}. * @see #output(Fields) */ + @Contract("_ -> new") public Top output(String... fieldNames) { return output(Fields.fields(fieldNames)); } @@ -229,6 +242,7 @@ public Top output(String... fieldNames) { * @return new instance of {@link Top}. * @see #output(Fields) */ + @Contract("_ -> new") public Top output(AggregationExpression... out) { return new Top(append("output", Arrays.asList(out))); } @@ -263,6 +277,7 @@ public static First first(int numberOfResults) { * @param numberOfResults * @return new instance of {@link First}. */ + @Contract("_ -> new") public First limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -274,6 +289,7 @@ public First limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First limit(AggregationExpression expression) { return limit((Object) expression); } @@ -288,6 +304,7 @@ private First limit(Object value) { * @param fieldName must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First of(String fieldName) { return input(fieldName); } @@ -298,6 +315,7 @@ public First of(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First of(AggregationExpression expression) { return input(expression); } @@ -308,6 +326,7 @@ public First of(AggregationExpression expression) { * @param fieldName must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First input(String fieldName) { return new First(append("input", Fields.field(fieldName))); } @@ -318,6 +337,7 @@ public First input(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link First}. */ + @Contract("_ -> new") public First input(AggregationExpression expression) { return new First(append("input", expression)); } @@ -357,6 +377,7 @@ public static Last last(int numberOfResults) { * @param numberOfResults * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last limit(int numberOfResults) { return limit((Object) numberOfResults); } @@ -368,6 +389,7 @@ public Last limit(int numberOfResults) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last limit(AggregationExpression expression) { return limit((Object) expression); } @@ -382,6 +404,7 @@ private Last limit(Object value) { * @param fieldName must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last of(String fieldName) { return input(fieldName); } @@ -392,6 +415,7 @@ public Last of(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last of(AggregationExpression expression) { return input(expression); } @@ -402,6 +426,7 @@ public Last of(AggregationExpression expression) { * @param fieldName must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last input(String fieldName) { return new Last(append("input", Fields.field(fieldName))); } @@ -412,6 +437,7 @@ public Last input(String fieldName) { * @param expression must not be {@literal null}. * @return new instance of {@link Last}. */ + @Contract("_ -> new") public Last input(AggregationExpression expression) { return new Last(append("input", expression)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java index b188b16b5f..800d8b37d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperation.java @@ -19,8 +19,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.SetOperation.FieldAppender.ValueAppender; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * Adds new fields to documents. {@code $set} outputs documents that contain all existing fields from the input @@ -82,6 +83,7 @@ public static ValueAppender set(String field) { * @param value the value to assign. * @return new instance of {@link SetOperation}. */ + @Contract("_, _ -> new") public SetOperation set(Object field, Object value) { LinkedHashMap target = new LinkedHashMap<>(getValueMap()); @@ -131,7 +133,7 @@ public ValueAppender set(String field) { return new ValueAppender() { @Override - public SetOperation toValue(Object value) { + public SetOperation toValue(@Nullable Object value) { valueMap.put(field, value); return FieldAppender.this.build(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java index 094ef7365b..a99c0926f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetOperators.java @@ -19,7 +19,9 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -55,8 +57,8 @@ public static SetOperatorFactory arrayAsSet(AggregationExpression expression) { */ public static class SetOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link SetOperatorFactory} for given {@literal fieldReference}. @@ -104,6 +106,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) { return createSetEquals().isEqualTo(expressions); } + @SuppressWarnings("NullAway") private SetEquals createSetEquals() { return usesFieldRef() ? SetEquals.arrayAsSet(fieldReference) : SetEquals.arrayAsSet(expression); } @@ -130,6 +133,7 @@ public SetIntersection intersects(AggregationExpression... expressions) { return createSetIntersection().intersects(expressions); } + @SuppressWarnings("NullAway") private SetIntersection createSetIntersection() { return usesFieldRef() ? SetIntersection.arrayAsSet(fieldReference) : SetIntersection.arrayAsSet(expression); } @@ -156,6 +160,7 @@ public SetUnion union(AggregationExpression... expressions) { return createSetUnion().union(expressions); } + @SuppressWarnings("NullAway") private SetUnion createSetUnion() { return usesFieldRef() ? SetUnion.arrayAsSet(fieldReference) : SetUnion.arrayAsSet(expression); } @@ -182,6 +187,7 @@ public SetDifference differenceTo(AggregationExpression expression) { return createSetDifference().differenceTo(expression); } + @SuppressWarnings("NullAway") private SetDifference createSetDifference() { return usesFieldRef() ? SetDifference.arrayAsSet(fieldReference) : SetDifference.arrayAsSet(expression); } @@ -208,6 +214,7 @@ public SetIsSubset isSubsetOf(AggregationExpression expression) { return createSetIsSubset().isSubsetOf(expression); } + @SuppressWarnings("NullAway") private SetIsSubset createSetIsSubset() { return usesFieldRef() ? SetIsSubset.arrayAsSet(fieldReference) : SetIsSubset.arrayAsSet(expression); } @@ -218,6 +225,7 @@ private SetIsSubset createSetIsSubset() { * * @return new instance of {@link AnyElementTrue}. */ + @SuppressWarnings("NullAway") public AnyElementTrue anyElementTrue() { return usesFieldRef() ? AnyElementTrue.arrayAsSet(fieldReference) : AnyElementTrue.arrayAsSet(expression); } @@ -228,6 +236,7 @@ public AnyElementTrue anyElementTrue() { * * @return new instance of {@link AllElementsTrue}. */ + @SuppressWarnings("NullAway") public AllElementsTrue allElementsTrue() { return usesFieldRef() ? AllElementsTrue.arrayAsSet(fieldReference) : AllElementsTrue.arrayAsSet(expression); } @@ -283,6 +292,7 @@ public static SetEquals arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -295,6 +305,7 @@ public SetEquals isEqualTo(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -307,6 +318,7 @@ public SetEquals isEqualTo(AggregationExpression... expressions) { * @param array must not be {@literal null}. * @return new instance of {@link SetEquals}. */ + @Contract("_ -> new") public SetEquals isEqualTo(Object[] array) { Assert.notNull(array, "Array must not be null"); @@ -360,6 +372,7 @@ public static SetIntersection arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetIntersection}. */ + @Contract("_ -> new") public SetIntersection intersects(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -372,6 +385,7 @@ public SetIntersection intersects(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetIntersection}. */ + @Contract("_ -> new") public SetIntersection intersects(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -425,6 +439,7 @@ public static SetUnion arrayAsSet(AggregationExpression expression) { * @param arrayReferences must not be {@literal null}. * @return new instance of {@link SetUnion}. */ + @Contract("_ -> new") public SetUnion union(String... arrayReferences) { Assert.notNull(arrayReferences, "ArrayReferences must not be null"); @@ -437,6 +452,7 @@ public SetUnion union(String... arrayReferences) { * @param expressions must not be {@literal null}. * @return new instance of {@link SetUnion}. */ + @Contract("_ -> new") public SetUnion union(AggregationExpression... expressions) { Assert.notNull(expressions, "Expressions must not be null"); @@ -490,6 +506,7 @@ public static SetDifference arrayAsSet(AggregationExpression expression) { * @param arrayReference must not be {@literal null}. * @return new instance of {@link SetDifference}. */ + @Contract("_ -> new") public SetDifference differenceTo(String arrayReference) { Assert.notNull(arrayReference, "ArrayReference must not be null"); @@ -502,6 +519,7 @@ public SetDifference differenceTo(String arrayReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetDifference}. */ + @Contract("_ -> new") public SetDifference differenceTo(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -555,6 +573,7 @@ public static SetIsSubset arrayAsSet(AggregationExpression expression) { * @param arrayReference must not be {@literal null}. * @return new instance of {@link SetIsSubset}. */ + @Contract("_ -> new") public SetIsSubset isSubsetOf(String arrayReference) { Assert.notNull(arrayReference, "ArrayReference must not be null"); @@ -567,6 +586,7 @@ public SetIsSubset isSubsetOf(String arrayReference) { * @param expression must not be {@literal null}. * @return new instance of {@link SetIsSubset}. */ + @Contract("_ -> new") public SetIsSubset isSubsetOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -614,6 +634,7 @@ public static AnyElementTrue arrayAsSet(AggregationExpression expression) { return new AnyElementTrue(Collections.singletonList(expression)); } + @Contract("-> this") public AnyElementTrue anyElementTrue() { return this; } @@ -659,6 +680,7 @@ public static AllElementsTrue arrayAsSet(AggregationExpression expression) { return new AllElementsTrue(Collections.singletonList(expression)); } + @Contract("-> this") public AllElementsTrue allElementsTrue() { return this; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java index 2b8df539e1..e1fec17811 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperation.java @@ -22,8 +22,9 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -31,7 +32,8 @@ * * @author Christoph Strobl * @since 3.3 - * @see https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/ + * @see https://docs.mongodb.com/manual/reference/operator/aggregation/setWindowFields/ */ public class SetWindowFieldsOperation implements AggregationOperation, FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation { @@ -137,6 +139,7 @@ public WindowOutput(ComputedField outputField) { * @param field must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WindowOutput append(ComputedField field) { Assert.notNull(field, "Field must not be null"); @@ -152,6 +155,7 @@ public WindowOutput append(ComputedField field) { * @return new instance of {@link ComputedFieldAppender}. * @see #append(ComputedField) */ + @Contract("_ -> new") public ComputedFieldAppender append(AggregationExpression expression) { return new ComputedFieldAppender() { @@ -249,8 +253,7 @@ public AggregationExpression getWindowOperator() { return windowOperator; } - @Nullable - public Window getWindow() { + public @Nullable Window getWindow() { return window; } } @@ -360,6 +363,7 @@ public static class RangeWindowBuilder { * @param lower eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder from(String lower) { this.lower = lower; @@ -372,6 +376,7 @@ public RangeWindowBuilder from(String lower) { * @param upper eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder to(String upper) { this.upper = upper; @@ -386,6 +391,7 @@ public RangeWindowBuilder to(String upper) { * @param lower * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder from(Number lower) { this.lower = lower; @@ -400,6 +406,7 @@ public RangeWindowBuilder from(Number lower) { * @param upper * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder to(Number upper) { this.upper = upper; @@ -411,6 +418,7 @@ public RangeWindowBuilder to(Number upper) { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder fromCurrent() { return from(CURRENT); } @@ -420,6 +428,7 @@ public RangeWindowBuilder fromCurrent() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder fromUnbounded() { return from(UNBOUNDED); } @@ -429,6 +438,7 @@ public RangeWindowBuilder fromUnbounded() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder toCurrent() { return to(CURRENT); } @@ -438,6 +448,7 @@ public RangeWindowBuilder toCurrent() { * * @return this. */ + @Contract("-> this") public RangeWindowBuilder toUnbounded() { return to(UNBOUNDED); } @@ -448,6 +459,7 @@ public RangeWindowBuilder toUnbounded() { * @param windowUnit must not be {@literal null}. Can be on of {@link Windows}. * @return this. */ + @Contract("_ -> this") public RangeWindowBuilder unit(WindowUnit windowUnit) { Assert.notNull(windowUnit, "WindowUnit must not be null"); @@ -460,6 +472,7 @@ public RangeWindowBuilder unit(WindowUnit windowUnit) { * * @return new instance of {@link RangeWindow}. */ + @Contract("-> new") public RangeWindow build() { Assert.notNull(lower, "Lower bound must not be null"); @@ -488,20 +501,24 @@ public static class DocumentWindowBuilder { * @param lower * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder from(Number lower) { this.lower = lower; return this; } + @Contract("-> this") public DocumentWindowBuilder fromCurrent() { return from(CURRENT); } + @Contract("-> this") public DocumentWindowBuilder fromUnbounded() { return from(UNBOUNDED); } + @Contract("-> this") public DocumentWindowBuilder to(String upper) { this.upper = upper; @@ -514,6 +531,7 @@ public DocumentWindowBuilder to(String upper) { * @param lower eg. {@literal current} or {@literal unbounded}. * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder from(String lower) { this.lower = lower; @@ -528,20 +546,24 @@ public DocumentWindowBuilder from(String lower) { * @param upper * @return this. */ + @Contract("_ -> this") public DocumentWindowBuilder to(Number upper) { this.upper = upper; return this; } + @Contract("-> this") public DocumentWindowBuilder toCurrent() { return to(CURRENT); } + @Contract("-> this") public DocumentWindowBuilder toUnbounded() { return to(UNBOUNDED); } + @Contract("-> new") public DocumentWindow build() { Assert.notNull(lower, "Lower bound must not be null"); @@ -689,9 +711,9 @@ public enum WindowUnits implements WindowUnit { */ public static class SetWindowFieldsOperationBuilder { - private Object partitionBy; - private SortOperation sortOperation; - private WindowOutput output; + private @Nullable Object partitionBy; + private @Nullable SortOperation sortOperation; + private @Nullable WindowOutput output; /** * Specify the field to group by. @@ -699,6 +721,7 @@ public static class SetWindowFieldsOperationBuilder { * @param fieldName must not be {@literal null} or null. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionByField(String fieldName) { Assert.hasText(fieldName, "Field name must not be empty or null"); @@ -711,6 +734,7 @@ public SetWindowFieldsOperationBuilder partitionByField(String fieldName) { * @param expression must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpression expression) { return partitionBy(expression); } @@ -721,6 +745,7 @@ public SetWindowFieldsOperationBuilder partitionByExpression(AggregationExpressi * @param fields must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(String... fields) { return sortBy(Sort.by(fields)); } @@ -731,6 +756,7 @@ public SetWindowFieldsOperationBuilder sortBy(String... fields) { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(Sort sort) { return sortBy(new SortOperation(sort)); } @@ -741,6 +767,7 @@ public SetWindowFieldsOperationBuilder sortBy(Sort sort) { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) { Assert.notNull(sort, "SortOperation must not be null"); @@ -755,6 +782,7 @@ public SetWindowFieldsOperationBuilder sortBy(SortOperation sort) { * @param output must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder output(WindowOutput output) { Assert.notNull(output, "WindowOutput must not be null"); @@ -769,6 +797,7 @@ public SetWindowFieldsOperationBuilder output(WindowOutput output) { * @param expression must not be {@literal null}. * @return new instance of {@link WindowChoice}. */ + @Contract("_ -> new") public WindowChoice output(AggregationExpression expression) { return new WindowChoice() { @@ -837,6 +866,7 @@ public interface WindowChoice extends As { * @param value must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public SetWindowFieldsOperationBuilder partitionBy(Object value) { Assert.notNull(value, "Partition By must not be null"); @@ -850,7 +880,10 @@ public SetWindowFieldsOperationBuilder partitionBy(Object value) { * * @return new instance of {@link SetWindowFieldsOperation}. */ + @Contract("-> new") public SetWindowFieldsOperation build() { + + Assert.notNull(output, "Output must be set first"); return new SetWindowFieldsOperation(partitionBy, sortOperation, output); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java index ffc0aa0654..e6a9a23d31 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SortByCountOperation.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -67,6 +67,7 @@ public SortByCountOperation(AggregationExpression groupByExpression) { } @Override + @SuppressWarnings("NullAway") public Document toDocument(AggregationOperationContext context) { return new Document(getOperator(), groupByExpression == null ? context.getReference(groupByField).toString() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java index 3119e2729c..ade4f5328e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SpelExpressionTransformer.java @@ -20,6 +20,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.GenericTypeResolver; import org.springframework.data.mongodb.core.spel.ExpressionNode; import org.springframework.data.mongodb.core.spel.ExpressionTransformationContextSupport; @@ -42,7 +43,6 @@ import org.springframework.expression.spel.standard.SpelExpression; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -83,7 +83,7 @@ class SpelExpressionTransformer implements AggregationExpressionTransformer { * @param params must not be {@literal null} * @return */ - public Object transform(String expression, AggregationOperationContext context, Object... params) { + public @Nullable Object transform(String expression, AggregationOperationContext context, Object... params) { Assert.notNull(expression, "Expression must not be null"); Assert.notNull(context, "AggregationOperationContext must not be null"); @@ -96,7 +96,7 @@ public Object transform(String expression, AggregationOperationContext context, return transform(new AggregationExpressionTransformationContext<>(node, null, null, context)); } - public Object transform(AggregationExpressionTransformationContext context) { + public @Nullable Object transform(AggregationExpressionTransformationContext context) { return lookupConversionFor(context.getCurrentNode()).convert(context); } @@ -137,7 +137,7 @@ private static abstract class ExpressionNodeConversion * * @param transformer must not be {@literal null}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) public ExpressionNodeConversion(AggregationExpressionTransformer transformer) { Assert.notNull(transformer, "Transformer must not be null"); @@ -165,7 +165,7 @@ protected boolean supports(ExpressionNode node) { * @param context must not be {@literal null}. * @return */ - protected Object transform(ExpressionNode node, AggregationExpressionTransformationContext context) { + protected @Nullable Object transform(ExpressionNode node, AggregationExpressionTransformationContext context) { Assert.notNull(node, "ExpressionNode must not be null"); Assert.notNull(context, "AggregationExpressionTransformationContext must not be null"); @@ -183,7 +183,7 @@ protected Object transform(ExpressionNode node, AggregationExpressionTransformat * @param context must not be {@literal null}. * @return */ - protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation, + protected @Nullable Object transform(ExpressionNode node, @Nullable ExpressionNode parent, @Nullable Document operation, AggregationExpressionTransformationContext context) { Assert.notNull(node, "ExpressionNode must not be null"); @@ -194,7 +194,7 @@ protected Object transform(ExpressionNode node, @Nullable ExpressionNode parent, } @Override - public Object transform(AggregationExpressionTransformationContext context) { + public @Nullable Object transform(AggregationExpressionTransformationContext context) { return transformer.transform(context); } @@ -204,7 +204,7 @@ public Object transform(AggregationExpressionTransformationContext context); + protected abstract @Nullable Object convert(AggregationExpressionTransformationContext context); } /** @@ -247,6 +247,7 @@ protected Object convert(AggregationExpressionTransformationContext context, OperatorNode currentNode) { @@ -301,7 +302,7 @@ private static class IndexerNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { return context.addToPreviousOrReturn(context.getCurrentNode().getValue()); } @@ -322,9 +323,8 @@ private static class InlineListNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { ExpressionNode currentNode = context.getCurrentNode(); @@ -355,7 +355,7 @@ private static class PropertyOrFieldReferenceNodeConversion extends ExpressionNo } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { String fieldReference = context.getFieldReference().toString(); return context.addToPreviousOrReturn(fieldReference); @@ -381,14 +381,14 @@ private static class LiteralNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { LiteralNode node = context.getCurrentNode(); Object value = node.getValue(); if (context.hasPreviousOperation()) { - if (node.isUnaryMinus(context.getParentNode())) { + if (node.isUnaryMinus(context.getParentNode()) && value != null) { // unary minus operator return NumberUtils.convertNumberToTargetClass(((Number) value).doubleValue() * -1, (Class) value.getClass()); // retain type, e.g. int to -int @@ -419,7 +419,7 @@ private static class MethodReferenceNodeConversion extends ExpressionNodeConvers } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { MethodReferenceNode node = context.getCurrentNode(); AggregationMethodReference methodReference = node.getMethodReference(); @@ -469,7 +469,7 @@ private static class CompoundExpressionNodeConversion extends ExpressionNodeConv } @Override - protected Object convert(AggregationExpressionTransformationContext context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { ExpressionNode currentNode = context.getCurrentNode(); @@ -503,7 +503,7 @@ static class NotOperatorNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { NotOperatorNode node = context.getCurrentNode(); List args = new ArrayList<>(); @@ -537,7 +537,7 @@ static class ValueRetrievingNodeConversion extends ExpressionNodeConversion context) { + protected @Nullable Object convert(AggregationExpressionTransformationContext context) { Object value = context.getCurrentNode().getValue(); return ObjectUtils.isArray(value) ? Arrays.asList(ObjectUtils.toObjectArray(value)) : value; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java index 9788497601..0f3447a476 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/StringOperators.java @@ -21,8 +21,10 @@ import java.util.Map; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.mongodb.util.RegexFlags; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -60,8 +62,8 @@ public static StringOperatorFactory valueOf(AggregationExpression fieldReference */ public static class StringOperatorFactory { - private final String fieldReference; - private final AggregationExpression expression; + private final @Nullable String fieldReference; + private final @Nullable AggregationExpression expression; /** * Creates new {@link StringOperatorFactory} for given {@literal fieldReference}. @@ -126,6 +128,7 @@ public Concat concat(String value) { return createConcat().concat(value); } + @SuppressWarnings("NullAway") private Concat createConcat() { return usesFieldRef() ? Concat.valueOf(fieldReference) : Concat.valueOf(expression); } @@ -153,6 +156,7 @@ public Substr substring(int start, int nrOfChars) { return createSubstr().substring(start, nrOfChars); } + @SuppressWarnings("NullAway") private Substr createSubstr() { return usesFieldRef() ? Substr.valueOf(fieldReference) : Substr.valueOf(expression); } @@ -162,6 +166,7 @@ private Substr createSubstr() { * * @return new instance of {@link ToLower}. */ + @SuppressWarnings("NullAway") public ToLower toLower() { return usesFieldRef() ? ToLower.lowerValueOf(fieldReference) : ToLower.lowerValueOf(expression); } @@ -171,6 +176,7 @@ public ToLower toLower() { * * @return new instance of {@link ToUpper}. */ + @SuppressWarnings("NullAway") public ToUpper toUpper() { return usesFieldRef() ? ToUpper.upperValueOf(fieldReference) : ToUpper.upperValueOf(expression); } @@ -214,6 +220,7 @@ public StrCaseCmp strCaseCmpValueOf(AggregationExpression expression) { return createStrCaseCmp().strcasecmpValueOf(expression); } + @SuppressWarnings("NullAway") private StrCaseCmp createStrCaseCmp() { return usesFieldRef() ? StrCaseCmp.valueOf(fieldReference) : StrCaseCmp.valueOf(expression); } @@ -260,6 +267,7 @@ public IndexOfBytes indexOf(AggregationExpression expression) { return createIndexOfBytesSubstringBuilder().indexOf(expression); } + @SuppressWarnings("NullAway") private IndexOfBytes.SubstringBuilder createIndexOfBytesSubstringBuilder() { return usesFieldRef() ? IndexOfBytes.valueOf(fieldReference) : IndexOfBytes.valueOf(expression); } @@ -306,6 +314,7 @@ public IndexOfCP indexOfCP(AggregationExpression expression) { return createIndexOfCPSubstringBuilder().indexOf(expression); } + @SuppressWarnings("NullAway") private IndexOfCP.SubstringBuilder createIndexOfCPSubstringBuilder() { return usesFieldRef() ? IndexOfCP.valueOf(fieldReference) : IndexOfCP.valueOf(expression); } @@ -343,6 +352,7 @@ public Split split(AggregationExpression expression) { return createSplit().split(expression); } + @SuppressWarnings("NullAway") private Split createSplit() { return usesFieldRef() ? Split.valueOf(fieldReference) : Split.valueOf(expression); } @@ -353,6 +363,7 @@ private Split createSplit() { * * @return new instance of {@link StrLenBytes}. */ + @SuppressWarnings("NullAway") public StrLenBytes length() { return usesFieldRef() ? StrLenBytes.stringLengthOf(fieldReference) : StrLenBytes.stringLengthOf(expression); } @@ -363,6 +374,7 @@ public StrLenBytes length() { * * @return new instance of {@link StrLenCP}. */ + @SuppressWarnings("NullAway") public StrLenCP lengthCP() { return usesFieldRef() ? StrLenCP.stringLengthOfCP(fieldReference) : StrLenCP.stringLengthOfCP(expression); } @@ -390,6 +402,7 @@ public SubstrCP substringCP(int codePointStart, int nrOfCodePoints) { return createSubstrCP().substringCP(codePointStart, nrOfCodePoints); } + @SuppressWarnings("NullAway") private SubstrCP createSubstrCP() { return usesFieldRef() ? SubstrCP.valueOf(fieldReference) : SubstrCP.valueOf(expression); } @@ -432,6 +445,7 @@ public Trim trim(AggregationExpression expression) { return trim().charsOf(expression); } + @SuppressWarnings("NullAway") private Trim createTrim() { return usesFieldRef() ? Trim.valueOf(fieldReference) : Trim.valueOf(expression); } @@ -474,6 +488,7 @@ public LTrim ltrim(AggregationExpression expression) { return ltrim().charsOf(expression); } + @SuppressWarnings("NullAway") private LTrim createLTrim() { return usesFieldRef() ? LTrim.valueOf(fieldReference) : LTrim.valueOf(expression); } @@ -516,6 +531,7 @@ public RTrim rtrim(AggregationExpression expression) { return rtrim().charsOf(expression); } + @SuppressWarnings("NullAway") private RTrim createRTrim() { return usesFieldRef() ? RTrim.valueOf(fieldReference) : RTrim.valueOf(expression); } @@ -572,6 +588,7 @@ public RegexFind regexFind(String regex, String options) { return createRegexFind().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexFind createRegexFind() { return usesFieldRef() ? RegexFind.valueOf(fieldReference) : RegexFind.valueOf(expression); } @@ -628,6 +645,7 @@ public RegexFindAll regexFindAll(String regex, String options) { return createRegexFindAll().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexFindAll createRegexFindAll() { return usesFieldRef() ? RegexFindAll.valueOf(fieldReference) : RegexFindAll.valueOf(expression); } @@ -683,6 +701,7 @@ public RegexMatch regexMatch(String regex, String options) { return createRegexMatch().regex(regex).options(options); } + @SuppressWarnings("NullAway") private RegexMatch createRegexMatch() { return usesFieldRef() ? RegexMatch.valueOf(fieldReference) : RegexMatch.valueOf(expression); } @@ -713,6 +732,7 @@ public ReplaceOne replaceOne(AggregationExpression search, String replacement) { return createReplaceOne().findValueOf(search).replacement(replacement); } + @SuppressWarnings("NullAway") private ReplaceOne createReplaceOne() { return usesFieldRef() ? ReplaceOne.valueOf(fieldReference) : ReplaceOne.valueOf(expression); } @@ -743,6 +763,7 @@ public ReplaceAll replaceAll(AggregationExpression search, String replacement) { return createReplaceAll().findValueOf(search).replacement(replacement); } + @SuppressWarnings("NullAway") private ReplaceAll createReplaceAll() { return usesFieldRef() ? ReplaceAll.valueOf(fieldReference) : ReplaceAll.valueOf(expression); } @@ -810,6 +831,7 @@ public static Concat stringValue(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concatValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -822,6 +844,7 @@ public Concat concatValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concatValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -834,6 +857,7 @@ public Concat concatValueOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link Concat}. */ + @Contract("_ -> new") public Concat concat(String value) { return new Concat(append(value)); } @@ -883,6 +907,7 @@ public static Substr valueOf(AggregationExpression expression) { * @param start start index (including) * @return new instance of {@link Substr}. */ + @Contract("_ -> new") public Substr substring(int start) { return substring(start, -1); } @@ -892,6 +917,7 @@ public Substr substring(int start) { * @param nrOfChars * @return new instance of {@link Substr}. */ + @Contract("_, _ -> new") public Substr substring(int start, int nrOfChars) { return new Substr(append(Arrays.asList(start, nrOfChars))); } @@ -1055,16 +1081,19 @@ public static StrCaseCmp stringValue(String value) { return new StrCaseCmp(Collections.singletonList(value)); } + @Contract("_ -> new") public StrCaseCmp strcasecmp(String value) { return new StrCaseCmp(append(value)); } + @Contract("_ -> new") public StrCaseCmp strcasecmpValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); return new StrCaseCmp(append(Fields.field(fieldReference))); } + @Contract("_ -> new") public StrCaseCmp strcasecmpValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1118,6 +1147,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) { * @param range must not be {@literal null}. * @return new instance of {@link IndexOfBytes}. */ + @Contract("_ -> new") public IndexOfBytes within(Range range) { return new IndexOfBytes(append(AggregationUtils.toRangeValues(range))); } @@ -1208,6 +1238,7 @@ public static SubstringBuilder valueOf(AggregationExpression expression) { * @param range must not be {@literal null}. * @return new instance of {@link IndexOfCP}. */ + @Contract("_ -> new") public IndexOfCP within(Range range) { return new IndexOfCP(append(AggregationUtils.toRangeValues(range))); } @@ -1298,6 +1329,7 @@ public static Split valueOf(AggregationExpression expression) { * @param delimiter must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(String delimiter) { Assert.notNull(delimiter, "Delimiter must not be null"); @@ -1310,6 +1342,7 @@ public Split split(String delimiter) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(Field fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1322,6 +1355,7 @@ public Split split(Field fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Split}. */ + @Contract("_ -> new") public Split split(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1447,10 +1481,12 @@ public static SubstrCP valueOf(AggregationExpression expression) { return new SubstrCP(Collections.singletonList(expression)); } + @Contract("_ -> new") public SubstrCP substringCP(int start) { return substringCP(start, -1); } + @Contract("_, _ -> new") public SubstrCP substringCP(int start, int nrOfChars) { return new SubstrCP(append(Arrays.asList(start, nrOfChars))); } @@ -1501,6 +1537,7 @@ public static Trim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1514,6 +1551,7 @@ public Trim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim charsOf(String fieldReference) { return new Trim(append("chars", Fields.field(fieldReference))); } @@ -1525,6 +1563,7 @@ public Trim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link Trim}. */ + @Contract("_ -> new") public Trim charsOf(AggregationExpression expression) { return new Trim(append("chars", expression)); } @@ -1598,6 +1637,7 @@ public static LTrim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1611,6 +1651,7 @@ public LTrim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim charsOf(String fieldReference) { return new LTrim(append("chars", Fields.field(fieldReference))); } @@ -1622,6 +1663,7 @@ public LTrim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link LTrim}. */ + @Contract("_ -> new") public LTrim charsOf(AggregationExpression expression) { return new LTrim(append("chars", expression)); } @@ -1677,6 +1719,7 @@ public static RTrim valueOf(AggregationExpression expression) { * @param chars must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim chars(String chars) { Assert.notNull(chars, "Chars must not be null"); @@ -1689,6 +1732,7 @@ public RTrim chars(String chars) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim charsOf(String fieldReference) { return new RTrim(append("chars", Fields.field(fieldReference))); } @@ -1699,6 +1743,7 @@ public RTrim charsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RTrim}. */ + @Contract("_ -> new") public RTrim charsOf(AggregationExpression expression) { return new RTrim(append("chars", expression)); } @@ -1757,6 +1802,7 @@ public static RegexFind valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind options(String options) { Assert.notNull(options, "Options must not be null"); @@ -1771,6 +1817,7 @@ public RegexFind options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind optionsOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -1785,6 +1832,7 @@ public RegexFind optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1798,6 +1846,7 @@ public RegexFind optionsOf(AggregationExpression expression) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -1811,6 +1860,7 @@ public RegexFind regex(String regex) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -1827,6 +1877,7 @@ public RegexFind pattern(Pattern pattern) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regexOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1840,6 +1891,7 @@ public RegexFind regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFind}. */ + @Contract("_ -> new") public RegexFind regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1899,6 +1951,7 @@ public static RegexFindAll valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll options(String options) { Assert.notNull(options, "Options must not be null"); @@ -1913,6 +1966,7 @@ public RegexFindAll options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll optionsOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1927,6 +1981,7 @@ public RegexFindAll optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -1940,6 +1995,7 @@ public RegexFindAll optionsOf(AggregationExpression expression) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -1956,6 +2012,7 @@ public RegexFindAll pattern(Pattern pattern) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -1969,6 +2026,7 @@ public RegexFindAll regex(String regex) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regexOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -1982,6 +2040,7 @@ public RegexFindAll regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexFindAll}. */ + @Contract("_ -> new") public RegexFindAll regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2043,6 +2102,7 @@ public static RegexMatch valueOf(AggregationExpression expression) { * @param options must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch options(String options) { Assert.notNull(options, "Options must not be null"); @@ -2057,6 +2117,7 @@ public RegexMatch options(String options) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch optionsOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2071,6 +2132,7 @@ public RegexMatch optionsOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch optionsOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2084,6 +2146,7 @@ public RegexMatch optionsOf(AggregationExpression expression) { * @param pattern must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch pattern(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -2100,6 +2163,7 @@ public RegexMatch pattern(Pattern pattern) { * @param regex must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regex(String regex) { Assert.notNull(regex, "Regex must not be null"); @@ -2113,6 +2177,7 @@ public RegexMatch regex(String regex) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regexOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2126,6 +2191,7 @@ public RegexMatch regexOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link RegexMatch}. */ + @Contract("_ -> new") public RegexMatch regexOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2201,6 +2267,7 @@ public static ReplaceOne valueOf(AggregationExpression expression) { * @param replacement must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacement(String replacement) { Assert.notNull(replacement, "Replacement must not be null"); @@ -2215,6 +2282,7 @@ public ReplaceOne replacement(String replacement) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacementOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2229,6 +2297,7 @@ public ReplaceOne replacementOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne replacementOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2242,6 +2311,7 @@ public ReplaceOne replacementOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne find(String value) { Assert.notNull(value, "Search string must not be null"); @@ -2255,6 +2325,7 @@ public ReplaceOne find(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne findValueOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2269,6 +2340,7 @@ public ReplaceOne findValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceOne}. */ + @Contract("_ -> new") public ReplaceOne findValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2344,6 +2416,7 @@ public static ReplaceAll valueOf(AggregationExpression expression) { * @param replacement must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacement(String replacement) { Assert.notNull(replacement, "Replacement must not be null"); @@ -2358,6 +2431,7 @@ public ReplaceAll replacement(String replacement) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacementValueOf(String fieldReference) { Assert.notNull(fieldReference, "FieldReference must not be null"); @@ -2372,6 +2446,7 @@ public ReplaceAll replacementValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll replacementValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); @@ -2385,6 +2460,7 @@ public ReplaceAll replacementValueOf(AggregationExpression expression) { * @param value must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll find(String value) { Assert.notNull(value, "Search string must not be null"); @@ -2398,6 +2474,7 @@ public ReplaceAll find(String value) { * @param fieldReference must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll findValueOf(String fieldReference) { Assert.notNull(fieldReference, "fieldReference must not be null"); @@ -2411,6 +2488,7 @@ public ReplaceAll findValueOf(String fieldReference) { * @param expression must not be {@literal null}. * @return new instance of {@link ReplaceAll}. */ + @Contract("_ -> new") public ReplaceAll findValueOf(AggregationExpression expression) { Assert.notNull(expression, "Expression must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java index 1fcf87d2a0..cc0296c900 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/SystemVariable.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.aggregation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Describes the system variables available in MongoDB aggregation framework pipeline expressions. @@ -116,8 +116,7 @@ public String getTarget() { return toString(); } - @Nullable - static String variableNameFrom(@Nullable String fieldRef) { + static @Nullable String variableNameFrom(@Nullable String fieldRef) { if (fieldRef == null || !fieldRef.startsWith(PREFIX) || fieldRef.length() <= 2) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java index f30ebf394b..d2d49abf78 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/TypeBasedAggregationOperationContext.java @@ -22,7 +22,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.PersistentPropertyPath; import org.springframework.data.mapping.context.MappingContext; @@ -33,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java index 057ada12d5..c93c1bad9e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperation.java @@ -19,7 +19,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java index ff765c37f7..0bcc192ded 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnsetOperation.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -67,6 +68,7 @@ public static UnsetOperation unset(String... fields) { * @param fields must not be {@literal null}. * @return new instance of {@link UnsetOperation}. */ + @Contract("_ -> new") public UnsetOperation and(String... fields) { List target = new ArrayList<>(this.fields); @@ -80,6 +82,7 @@ public UnsetOperation and(String... fields) { * @param fields must not be {@literal null}. * @return new instance of {@link UnsetOperation}. */ + @Contract("_ -> new") public UnsetOperation and(Field... fields) { List target = new ArrayList<>(this.fields); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java index d59ae01b12..cc0552cd1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/UnwindOperation.java @@ -16,8 +16,9 @@ package org.springframework.data.mongodb.core.aggregation; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -201,6 +202,8 @@ public static PathBuilder newBuilder() { @Override public UnwindOperation preserveNullAndEmptyArrays() { + Assert.notNull(field, "Path needs to be set first"); + if (arrayIndex != null) { return new UnwindOperation(field, arrayIndex, true); } @@ -211,6 +214,8 @@ public UnwindOperation preserveNullAndEmptyArrays() { @Override public UnwindOperation skipNullAndEmptyArrays() { + Assert.notNull(field, "Path needs to be set first"); + if (arrayIndex != null) { return new UnwindOperation(field, arrayIndex, false); } @@ -219,6 +224,7 @@ public UnwindOperation skipNullAndEmptyArrays() { } @Override + @Contract("_ -> this") public EmptyArraysBuilder arrayIndex(String field) { Assert.hasText(field, "'ArrayIndex' must not be null or empty"); @@ -227,6 +233,7 @@ public EmptyArraysBuilder arrayIndex(String field) { } @Override + @Contract("-> this") public EmptyArraysBuilder noArrayIndex() { arrayIndex = null; @@ -234,6 +241,7 @@ public EmptyArraysBuilder noArrayIndex() { } @Override + @Contract("_ -> this") public UnwindOperationBuilder path(String path) { Assert.hasText(path, "'Path' must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java index 8e676c72bc..b5a9ca0f21 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VariableOperators.java @@ -22,8 +22,8 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -223,8 +223,7 @@ public static class Let implements AggregationExpression { private final List vars; - @Nullable // - private final AggregationExpression expression; + private final @Nullable AggregationExpression expression; private Let(List vars, @Nullable AggregationExpression expression) { @@ -333,6 +332,7 @@ private Document getMappedVariable(ExpressionVariable var, AggregationOperationC return new Document(var.variableName, var.expression); } + @SuppressWarnings("NullAway") private Object getMappedIn(AggregationOperationContext context) { return expression.toDocument(new NestedDelegatingExpressionAggregationOperationContext(context, this.vars.stream().map(var -> Fields.field(var.variableName)).collect(Collectors.toList()))); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java index dd14ef20c9..de266a3064 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/VectorSearchOperation.java @@ -24,13 +24,14 @@ import org.bson.BinaryVector; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Vector; import org.springframework.data.mongodb.core.mapping.MongoVector; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.StringUtils; /** @@ -55,12 +56,12 @@ public class VectorSearchOperation implements AggregationOperation { private final @Nullable Integer numCandidates; private final QueryPaths path; private final Vector vector; - private final String score; - private final Consumer scoreCriteria; + private final @Nullable String score; + private final @Nullable Consumer scoreCriteria; private VectorSearchOperation(SearchType searchType, @Nullable CriteriaDefinition filter, String indexName, Limit limit, @Nullable Integer numCandidates, QueryPaths path, Vector vector, @Nullable String searchScore, - Consumer scoreCriteria) { + @Nullable Consumer scoreCriteria) { this.searchType = searchType; this.filter = filter; @@ -299,9 +300,9 @@ public String getOperator() { */ private static class VectorSearchBuilder implements PathContributor, VectorContributor, LimitContributor { - String index; - QueryPath paths; - Vector vector; + @Nullable String index; + @Nullable QueryPath paths; + @Nullable Vector vector; PathContributor index(String index) { this.index = index; @@ -317,6 +318,11 @@ public VectorContributor path(String path) { @Override public VectorSearchOperation limit(Limit limit) { + + Assert.notNull(index, "Index must be set first"); + Assert.notNull(paths, "Path must be set first"); + Assert.notNull(vector, "Vector must be set first"); + return new VectorSearchOperation(index, QueryPaths.of(paths), limit, vector); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java index 0e30b8b855..2769990ca6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/package-info.java @@ -3,6 +3,6 @@ * * @since 1.3 */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.aggregation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java index 3e08dc1014..9ada2014a0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/annotation/package-info.java @@ -1,6 +1,6 @@ /** * Core Spring Data MongoDB annotations not limited to a special use case (like Query,...). */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.annotation; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java index 7a01677939..9b1c744be2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/AbstractMongoConverter.java @@ -20,6 +20,7 @@ import org.bson.types.Code; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToBigIntegerConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.ObjectIdToStringConverter; import org.springframework.data.mongodb.core.convert.MongoConverters.StringToObjectIdConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java index 40afbb8c10..3b4dd99d4e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefProxyHandler.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java index 0235694030..ee1f568494 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java @@ -18,10 +18,10 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.DBRef; @@ -60,7 +60,7 @@ Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbR * @param id will never be {@literal null}. * @return new instance of {@link DBRef}. */ - default DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation, + default DBRef createDbRef(org.springframework.data.mongodb.core.mapping.@Nullable DBRef annotation, MongoPersistentEntity entity, Object id) { if (annotation != null && StringUtils.hasText(annotation.db())) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java index bf6b882375..fd80029118 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolverCallback.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; /** @@ -31,5 +32,5 @@ public interface DbRefResolverCallback { * @param property will never be {@literal null}. * @return */ - Object resolve(MongoPersistentProperty property); + @Nullable Object resolve(MongoPersistentProperty property); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java index 22b1ce7981..13c0198aa0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefProxyHandler.java @@ -18,13 +18,12 @@ import java.util.function.Function; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index de66c3ea94..c72bc4b886 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; @@ -32,8 +33,8 @@ import org.springframework.data.mongodb.core.mapping.BasicMongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import com.mongodb.DBRef; @@ -71,7 +72,7 @@ public DefaultDbRefResolver(MongoDatabaseFactory mongoDbFactory) { } @Override - public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, + public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler handler) { Assert.notNull(property, "Property must not be null"); @@ -86,7 +87,7 @@ public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbr } @Override - public Document fetch(DBRef dbRef) { + public @Nullable Document fetch(DBRef dbRef) { return getReferenceLoader().fetchOne( DocumentReferenceQuery.forSingleDocument(Filters.eq(FieldName.ID.name(), dbRef.getId())), ReferenceCollection.fromDBRef(dbRef)); @@ -171,7 +172,7 @@ private boolean isLazyDbRef(MongoPersistentProperty property) { private static Stream documentWithId(Object identifier, Collection documents) { return documents.stream() // - .filter(it -> it.get(BasicMongoPersistentProperty.ID_FIELD_NAME).equals(identifier)) // + .filter(it -> ObjectUtils.nullSafeEquals(it.get(BasicMongoPersistentProperty.ID_FIELD_NAME), identifier)) // .limit(1); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java index 82e5c9d0eb..376e0dd8cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolverCallback.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; @@ -53,7 +54,7 @@ class DefaultDbRefResolverCallback implements DbRefResolverCallback { } @Override - public Object resolve(MongoPersistentProperty property) { + public @Nullable Object resolve(MongoPersistentProperty property) { return resolver.getValueInternal(property, surroundingObject, evaluator, path); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java index 2c2b52afd5..f5db41f006 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultMongoTypeMapper.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.DefaultTypeMapper; import org.springframework.data.convert.SimpleTypeInformationMapper; @@ -32,7 +33,6 @@ import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.BasicDBList; @@ -114,7 +114,7 @@ public DefaultMongoTypeMapper(@Nullable String typeKey, List accessor, - MappingContext, ?> mappingContext, + @Nullable MappingContext, ?> mappingContext, List mappers) { super(accessor, mappingContext, mappers); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java index a7b3d6f21f..4df7c02f91 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultReferenceResolver.java @@ -20,6 +20,7 @@ import java.util.Collections; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.data.mongodb.core.mapping.DBRef; import org.springframework.data.mongodb.core.mapping.DocumentReference; @@ -63,7 +64,8 @@ public DefaultReferenceResolver(ReferenceLoader referenceLoader, PersistenceExce } @Override - public Object resolveReference(MongoPersistentProperty property, Object source, + @SuppressWarnings("NullAway") + public @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { LookupFunction lookupFunction = (property.isCollectionLike() || property.isMap()) ? collectionLookupFunction @@ -84,6 +86,7 @@ public Object resolveReference(MongoPersistentProperty property, Object source, * @see DBRef#lazy() * @see DocumentReference#lazy() */ + @SuppressWarnings("NullAway") protected boolean isLazyReference(MongoPersistentProperty property) { if (property.isDocumentReference()) { @@ -106,6 +109,7 @@ LazyLoadingProxyFactory getProxyFactory() { return proxyFactory; } + @SuppressWarnings("NullAway") private Object createLazyLoadingProxy(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, LookupFunction lookupFunction, MongoEntityReader entityReader) { return proxyFactory.createLazyLoadingProxy(property, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java index c795add9c8..ff50dd5df3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentAccessor.java @@ -21,11 +21,11 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.DBObject; @@ -119,8 +119,7 @@ public void put(MongoPersistentProperty prop, @Nullable Object value) { * @param property must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - public Object get(MongoPersistentProperty property) { + public @Nullable Object get(MongoPersistentProperty property) { return BsonUtils.resolveValue(document, getFieldName(property)); } @@ -131,8 +130,7 @@ public Object get(MongoPersistentProperty property) { * @param entity must not be {@literal null}. * @return */ - @Nullable - public Object getRawId(MongoPersistentEntity entity) { + public @Nullable Object getRawId(MongoPersistentEntity entity) { return entity.hasIdProperty() ? get(entity.getRequiredIdProperty()) : BsonUtils.get(document, FieldName.ID.name()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java index 8429584a6f..e03d215088 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPointerFactory.java @@ -70,6 +70,7 @@ class DocumentPointerFactory { this.cache = new WeakHashMap<>(); } + @SuppressWarnings("NullAway") DocumentPointer computePointer( MappingContext, MongoPersistentProperty> mappingContext, MongoPersistentProperty property, Object value, Class typeHint) { @@ -87,7 +88,7 @@ DocumentPointer computePointer( if (usesDefaultLookup(property)) { - MongoPersistentProperty idProperty = persistentEntity.getIdProperty(); + MongoPersistentProperty idProperty = persistentEntity.getRequiredIdProperty(); Object idValue = persistentEntity.getIdentifierAccessor(value).getIdentifier(); if (idProperty.hasExplicitWriteTarget() @@ -114,6 +115,7 @@ DocumentPointer computePointer( .getDocumentPointer(mappingContext, persistentEntity, propertyAccessor); } + @SuppressWarnings("NullAway") private boolean usesDefaultLookup(MongoPersistentProperty property) { if (property.isDocumentReference()) { @@ -216,9 +218,16 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target, MongoPersistentProperty persistentProperty = persistentEntity.getPersistentProperty(entry.getKey()); if (persistentProperty != null && persistentProperty.isEntity()) { - MongoPersistentEntity nestedEntity = mappingContext.getPersistentEntity(persistentProperty.getType()); - target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext, - nestedEntity, nestedEntity.getPropertyAccessor(propertyAccessor.getProperty(persistentProperty)))); + MongoPersistentEntity nestedEntity = mappingContext.getRequiredPersistentEntity(persistentProperty.getType()); + Object propertyValue = propertyAccessor.getProperty(persistentProperty); + + if(propertyValue == null) { + target.put(entry.getKey(), propertyValue); + } else { + PersistentPropertyAccessor nestedAccessor = nestedEntity.getPropertyAccessor(propertyValue); + target.put(entry.getKey(), updatePlaceholders(document, new Document(), mappingContext, + nestedEntity, nestedAccessor)); + } } else { target.put(entry.getKey(), updatePlaceholders((Document) entry.getValue(), new Document(), mappingContext, persistentEntity, propertyAccessor)); @@ -236,7 +245,7 @@ Object updatePlaceholders(org.bson.Document source, org.bson.Document target, String fieldName = entry.getKey().equals(FieldName.ID.name()) ? "id" : entry.getKey(); if (!fieldName.contains(".")) { - Object targetValue = propertyAccessor.getProperty(persistentEntity.getPersistentProperty(fieldName)); + Object targetValue = propertyAccessor.getProperty(persistentEntity.getRequiredPersistentProperty(fieldName)); target.put(attribute, targetValue); continue; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java index ea5ce01b44..a41e17c0ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentPropertyAccessor.java @@ -18,11 +18,11 @@ import java.util.Map; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.expression.MapAccessor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; /** * {@link PropertyAccessor} to allow entity based field access to {@link Document}s. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java index bf21781058..b1e894efe9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DocumentReferenceSource.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; /** * The source object to resolve document references upon. Encapsulates the actual source and the reference specific @@ -56,8 +57,7 @@ public Object getSelf() { * * @return can be {@literal null}. */ - @Nullable - public Object getTargetSource() { + public @Nullable Object getTargetSource() { return targetSource; } @@ -67,8 +67,7 @@ public Object getTargetSource() { * @param source * @return */ - @Nullable - static Object getTargetSource(Object source) { + static @Nullable Object getTargetSource(@Nullable Object source) { return source instanceof DocumentReferenceSource referenceSource ? referenceSource.getTargetSource() : source; } @@ -78,7 +77,8 @@ static Object getTargetSource(Object source) { * @param self * @return */ - static Object getSelf(Object self) { + @Contract("null -> null") + static @Nullable Object getSelf(@Nullable Object self) { return self instanceof DocumentReferenceSource referenceSource ? referenceSource.getSelf() : self; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java index 2bca260b79..ae73ab68bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java @@ -25,6 +25,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.ReadingConverter; import org.springframework.data.convert.WritingConverter; @@ -45,6 +46,7 @@ import org.springframework.data.mongodb.core.geo.GeoJsonPolygon; import org.springframework.data.mongodb.core.geo.Sphere; import org.springframework.data.mongodb.core.query.GeoCommand; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -130,7 +132,8 @@ enum DocumentToPointConverter implements Converter { INSTANCE; @Override - public Point convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Point convert(@Nullable Document source) { if (source == null) { return null; @@ -157,7 +160,8 @@ enum PointToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Point source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Point source) { return source == null ? null : new Document("x", source.getX()).append("y", source.getY()); } } @@ -174,7 +178,8 @@ enum BoxToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Box source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Box source) { if (source == null) { return null; @@ -199,7 +204,9 @@ enum DocumentToBoxConverter implements Converter { INSTANCE; @Override - public Box convert(Document source) { + @Contract("null -> null; !null -> !null") + @SuppressWarnings("NullAway") + public @Nullable Box convert(@Nullable Document source) { if (source == null) { return null; @@ -223,7 +230,8 @@ enum CircleToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Circle source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Circle source) { if (source == null) { return null; @@ -249,7 +257,8 @@ enum DocumentToCircleConverter implements Converter { INSTANCE; @Override - public Circle convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Circle convert(@Nullable Document source) { if (source == null) { return null; @@ -286,7 +295,8 @@ enum SphereToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Sphere source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Sphere source) { if (source == null) { return null; @@ -312,7 +322,8 @@ enum DocumentToSphereConverter implements Converter { INSTANCE; @Override - public Sphere convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Sphere convert(@Nullable Document source) { if (source == null) { return null; @@ -349,7 +360,8 @@ enum PolygonToDocumentConverter implements Converter { INSTANCE; @Override - public Document convert(Polygon source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable Polygon source) { if (source == null) { return null; @@ -381,18 +393,20 @@ enum DocumentToPolygonConverter implements Converter { @Override @SuppressWarnings({ "unchecked" }) - public Polygon convert(Document source) { + @Contract("null -> null; !null -> !null") + public @Nullable Polygon convert(@Nullable Document source) { if (source == null) { return null; } List points = (List) source.get("points"); - List newPoints = new ArrayList<>(points.size()); + Assert.notNull(points, "Points elements of polygon must not be null"); + List newPoints = new ArrayList<>(points.size()); for (Document element : points) { - Assert.notNull(element, "Point elements of polygon must not be null"); + Assert.notNull(element, "Point elements of polygon must not contain null"); newPoints.add(DocumentToPointConverter.INSTANCE.convert(element)); } @@ -412,7 +426,8 @@ enum GeoCommandToDocumentConverter implements Converter { @Override @SuppressWarnings("rawtypes") - public Document convert(GeoCommand source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable GeoCommand source) { if (source == null) { return null; @@ -463,7 +478,8 @@ enum GeoJsonToDocumentConverter implements Converter, Document> { INSTANCE; @Override - public Document convert(GeoJson source) { + @Contract("null -> null; !null -> !null") + public @Nullable Document convert(@Nullable GeoJson source) { if (source == null) { return null; @@ -490,7 +506,7 @@ public Document convert(GeoJson source) { private Object convertIfNecessary(Object candidate) { - if (candidate instanceof GeoJson geoJson) { + if (candidate instanceof GeoJson geoJson) { return convertIfNecessary(geoJson.getCoordinates()); } @@ -551,7 +567,8 @@ enum DocumentToGeoJsonPointConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonPoint convert(@Nullable Document source) { if (source == null) { return null; @@ -560,7 +577,10 @@ public GeoJsonPoint convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "Point"), String.format("Cannot convert type '%s' to Point", source.get("type"))); - List dbl = (List) source.get("coordinates"); + if(!(source.get("coordinates") instanceof List sourceCoordinates)) { + throw new IllegalArgumentException("Coordinates need to be present"); + } + List dbl = (List) sourceCoordinates; return new GeoJsonPoint(toPrimitiveDoubleValue(dbl.get(0)), toPrimitiveDoubleValue(dbl.get(1))); } } @@ -574,7 +594,8 @@ enum DocumentToGeoJsonPolygonConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonPolygon convert(@Nullable Document source) { if (source == null) { return null; @@ -596,7 +617,8 @@ enum DocumentToGeoJsonMultiPolygonConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiPolygon convert(@Nullable Document source) { if (source == null) { return null; @@ -606,8 +628,9 @@ public GeoJsonMultiPolygon convert(Document source) { String.format("Cannot convert type '%s' to MultiPolygon", source.get("type"))); List dbl = (List) source.get("coordinates"); - List polygones = new ArrayList<>(); + Assert.notNull(dbl, "Source needs to contain coordinates"); + List polygones = new ArrayList<>(dbl.size()); for (Object polygon : dbl) { polygones.add(toGeoJsonPolygon((List) polygon)); } @@ -625,7 +648,8 @@ enum DocumentToGeoJsonLineStringConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonLineString convert(@Nullable Document source) { if (source == null) { return null; @@ -649,7 +673,8 @@ enum DocumentToGeoJsonMultiPointConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiPoint convert(@Nullable Document source) { if (source == null) { return null; @@ -673,7 +698,8 @@ enum DocumentToGeoJsonMultiLineStringConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonMultiLineString convert(@Nullable Document source) { if (source == null) { return null; @@ -682,10 +708,13 @@ public GeoJsonMultiLineString convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "MultiLineString"), String.format("Cannot convert type '%s' to MultiLineString", source.get("type"))); - List lines = new ArrayList<>(); - List cords = (List) source.get("coordinates"); + if(!(source.get("coordinates") instanceof List coordinates)) { + throw new IllegalArgumentException("coordinates need to be present"); + } + + List lines = new ArrayList<>(coordinates.size()); - for (Object line : cords) { + for (Object line : coordinates) { lines.add(new GeoJsonLineString(toListOfPoint((List) line))); } return new GeoJsonMultiLineString(lines); @@ -700,9 +729,9 @@ enum DocumentToGeoJsonGeometryCollectionConverter implements Converter null; !null -> !null") + public @Nullable GeoJsonGeometryCollection convert(@Nullable Document source) { if (source == null) { return null; @@ -711,8 +740,12 @@ public GeoJsonGeometryCollection convert(Document source) { Assert.isTrue(ObjectUtils.nullSafeEquals(source.get("type"), "GeometryCollection"), String.format("Cannot convert type '%s' to GeometryCollection", source.get("type"))); - List> geometries = new ArrayList<>(); - for (Object o : (List) source.get("geometries")) { + if(!(source.get("geometries") instanceof List sourceGeometries)) { + throw new IllegalArgumentException("Geometries need to be present"); + } + + List> geometries = new ArrayList<>(sourceGeometries.size()); + for (Object o : sourceGeometries) { geometries.add(toGenericGeoJson((Document) o)); } @@ -732,7 +765,10 @@ static List toList(Point point) { * @since 1.7 */ @SuppressWarnings("unchecked") - static List toListOfPoint(List listOfCoordinatePairs) { + @Contract("null -> fail") + static List toListOfPoint(@Nullable List listOfCoordinatePairs) { + + Assert.notNull(listOfCoordinatePairs, "ListOfCoordinatePairs must not be null"); List points = new ArrayList<>(listOfCoordinatePairs.size()); @@ -755,7 +791,10 @@ static List toListOfPoint(List listOfCoordinatePairs) { * @return never {@literal null}. * @since 1.7 */ - static GeoJsonPolygon toGeoJsonPolygon(List dbList) { + @Contract("null -> fail") + static GeoJsonPolygon toGeoJsonPolygon(@Nullable List dbList) { + + Assert.notNull(dbList, "DbList must not be null"); GeoJsonPolygon polygon = new GeoJsonPolygon(toListOfPoint((List) dbList.get(0))); return dbList.size() > 1 ? polygon.withInnerRing(toListOfPoint((List) dbList.get(1))) : polygon; @@ -794,7 +833,8 @@ private static GeoJson toGenericGeoJson(Document source) { throw new IllegalArgumentException(String.format("No converter found capable of converting GeoJson type %s", type)); } - private static double toPrimitiveDoubleValue(Object value) { + @Contract("null -> fail") + private static double toPrimitiveDoubleValue(@Nullable Object value) { Assert.isInstanceOf(Number.class, value, "Argument must be a Number"); return NumberUtils.convertNumberToTargetClass((Number) value, Double.class); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java index 77aac55813..6329d74d4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxy.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.DBRef; @@ -53,8 +53,7 @@ public interface LazyLoadingProxy { * @return can be {@literal null}. * @since 3.3 */ - @Nullable - default Object getSource() { + default @Nullable Object getSource() { return toDBRef(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java index 76539ea431..eff58e7bd4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/LazyLoadingProxyFactory.java @@ -30,6 +30,8 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; @@ -43,7 +45,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lock; import org.springframework.data.util.Lock.AcquiredLock; -import org.springframework.lang.Nullable; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.ReflectionUtils; @@ -124,7 +125,7 @@ private ProxyFactory prepareProxyFactory(Class propertyType, Supplier propertyType = property.getType(); LazyLoadingInterceptor interceptor = new LazyLoadingInterceptor(property, callback, source, exceptionTranslator); @@ -160,6 +161,7 @@ private Class getEnhancedTypeFor(Class type) { return enhancer.createClass(); } + @NullUnmarked public static class LazyLoadingInterceptor implements MethodInterceptor, org.springframework.cglib.proxy.MethodInterceptor, Serializable { @@ -180,10 +182,10 @@ public static class LazyLoadingInterceptor private final Lock readLock = Lock.of(rwLock.readLock()); private final Lock writeLock = Lock.of(rwLock.writeLock()); - private final MongoPersistentProperty property; - private final DbRefResolverCallback callback; - private final Object source; - private final PersistenceExceptionTranslator exceptionTranslator; + private final @Nullable MongoPersistentProperty property; + private final @Nullable DbRefResolverCallback callback; + private final @Nullable Object source; + private final @Nullable PersistenceExceptionTranslator exceptionTranslator; private volatile boolean resolved; private @Nullable Object result; @@ -191,18 +193,17 @@ public static class LazyLoadingInterceptor * @return a {@link LazyLoadingInterceptor} that just continues with the invocation. * @since 4.0 */ + @SuppressWarnings("NullAway") public static LazyLoadingInterceptor none() { return new LazyLoadingInterceptor(null, null, null, null) { - @Nullable @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } - @Nullable @Override - public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable { + public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable { ReflectionUtils.makeAccessible(method); return method.invoke(o, args); @@ -210,8 +211,8 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox }; } - public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCallback callback, Object source, - PersistenceExceptionTranslator exceptionTranslator) { + public LazyLoadingInterceptor(@Nullable MongoPersistentProperty property, @Nullable DbRefResolverCallback callback, @Nullable Object source, + @Nullable PersistenceExceptionTranslator exceptionTranslator) { this.property = property; this.callback = callback; @@ -219,15 +220,13 @@ public LazyLoadingInterceptor(MongoPersistentProperty property, DbRefResolverCal this.exceptionTranslator = exceptionTranslator; } - @Nullable @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { return intercept(invocation.getThis(), invocation.getMethod(), invocation.getArguments(), null); } - @Nullable @Override - public Object intercept(Object o, Method method, Object[] args, MethodProxy proxy) throws Throwable { + public @Nullable Object intercept(Object o, Method method, @Nullable Object @Nullable[] args, @Nullable MethodProxy proxy) throws Throwable { if (INITIALIZE_METHOD.equals(method)) { return ensureResolved(); @@ -247,7 +246,7 @@ public Object intercept(Object o, Method method, Object[] args, MethodProxy prox return proxyToString(source); } - if (ReflectionUtils.isEqualsMethod(method)) { + if (ReflectionUtils.isEqualsMethod(method) && args != null) { return proxyEquals(o, args[0]); } @@ -347,8 +346,8 @@ private void readObject(ObjectInputStream in) throws IOException { } } - @Nullable - private Object resolve() { + @SuppressWarnings("NullAway") + private @Nullable Object resolve() { try (AcquiredLock l = readLock.lock()) { if (resolved) { @@ -370,7 +369,7 @@ private Object resolve() { return writeLock.execute(() -> callback.resolve(property)); } catch (RuntimeException ex) { - DataAccessException translatedException = exceptionTranslator.translateExceptionIfPossible(ex); + DataAccessException translatedException = exceptionTranslator != null ? exceptionTranslator.translateExceptionIfPossible(ex) : null; if (translatedException instanceof ClientSessionException) { throw new LazyLoadingException("Unable to lazily resolve DBRef; Invalid session state", ex); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 1d40573b81..2d073869ae 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -39,7 +39,7 @@ import org.bson.conversions.Bson; import org.bson.json.JsonReader; import org.bson.types.ObjectId; - +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.context.ApplicationContext; @@ -53,6 +53,7 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.data.annotation.Reference; import org.springframework.data.convert.CustomConversions; +import org.springframework.data.convert.PropertyValueConversions; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.TypeMapper; import org.springframework.data.convert.ValueConversionContext; @@ -94,7 +95,7 @@ import org.springframework.data.util.Predicates; import org.springframework.data.util.TypeInformation; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -157,7 +158,7 @@ public class MappingMongoConverter extends AbstractMongoConverter protected @Nullable ApplicationContext applicationContext; protected @Nullable Environment environment; - protected MongoTypeMapper typeMapper; + protected @Nullable MongoTypeMapper typeMapper; protected @Nullable String mapKeyDotReplacement = null; protected @Nullable CodecRegistryProvider codecRegistryProvider; @@ -307,7 +308,9 @@ public void setApplicationContext(ApplicationContext applicationContext) throws this.environment = applicationContext.getEnvironment(); this.spELContext = new SpELContext(this.spELContext, applicationContext); this.projectionFactory.setBeanFactory(applicationContext); - this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + if(applicationContext.getClassLoader() != null) { + this.projectionFactory.setBeanClassLoader(applicationContext.getClassLoader()); + } if (entityCallbacks == null) { setEntityCallbacks(EntityCallbacks.create(applicationContext)); @@ -418,7 +421,7 @@ FieldName getFieldName(MongoPersistentProperty prop) { return accessor.getBean(); } - private Object doReadOrProject(ConversionContext context, Bson source, TypeInformation typeHint, + private Object doReadOrProject(ConversionContext context, @Nullable Bson source, TypeInformation typeHint, EntityProjection typeDescriptor) { if (typeDescriptor.isProjection()) { @@ -433,12 +436,12 @@ static class MapPersistentPropertyAccessor implements PersistentPropertyAccessor Map map = new LinkedHashMap<>(); @Override - public void setProperty(PersistentProperty persistentProperty, Object o) { + public void setProperty(PersistentProperty persistentProperty, @Nullable Object o) { map.put(persistentProperty.getName(), o); } @Override - public Object getProperty(PersistentProperty persistentProperty) { + public @Nullable Object getProperty(PersistentProperty persistentProperty) { return map.get(persistentProperty.getName()); } @@ -466,10 +469,14 @@ protected S read(TypeInformation type, Bson bson) { * @return the converted object, will never be {@literal null}. * @since 3.2 */ - @SuppressWarnings("unchecked") - protected S readDocument(ConversionContext context, Bson bson, + @SuppressWarnings({"unchecked","NullAway"}) + protected S readDocument(ConversionContext context, @Nullable Bson bson, TypeInformation typeHint) { + if(bson == null) { + bson = new Document(); + } + Document document = bson instanceof BasicDBObject dbObject ? new Document(dbObject) : (Document) bson; TypeInformation typeToRead = getTypeMapper().readType(document, typeHint); Class rawType = typeToRead.getType(); @@ -545,7 +552,7 @@ public EvaluatingDocumentAccessor(Bson document) { } @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { return expressionEvaluatorFactory.create(getDocument()).evaluate(expression); } } @@ -599,8 +606,7 @@ private S populateProperties(ConversionContext context, MongoPersistentEntit * Reads the identifier from either the bean backing the {@link PersistentPropertyAccessor} or the source document in * case the identifier has not be populated yet. In this case the identifier is set on the bean for further reference. */ - @Nullable - private Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor accessor, + private @Nullable Object readAndPopulateIdentifier(ConversionContext context, PersistentPropertyAccessor accessor, DocumentAccessor document, MongoPersistentEntity entity, ValueExpressionEvaluator evaluator) { Object rawId = document.getRawId(entity); @@ -684,8 +690,7 @@ private DbRefResolverCallback getDbRefResolverCallback(ConversionContext context (prop, bson, e, path) -> MappingMongoConverter.this.getValueInternal(context, prop, bson, e)); } - @Nullable - private Object readAssociation(Association association, DocumentAccessor documentAccessor, + private @Nullable Object readAssociation(Association association, DocumentAccessor documentAccessor, DbRefProxyHandler handler, DbRefResolverCallback callback, ConversionContext context) { MongoPersistentProperty property = association.getInverse(); @@ -745,8 +750,8 @@ && peek(collection) instanceof Document) { } } - @Nullable - private Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor, + @SuppressWarnings("NullAway") + private @Nullable Object readUnwrapped(ConversionContext context, DocumentAccessor documentAccessor, MongoPersistentProperty prop, MongoPersistentEntity unwrappedEntity) { if (prop.findAnnotation(Unwrapped.class).onEmpty().equals(OnEmpty.USE_EMPTY)) { @@ -762,13 +767,14 @@ private Object readUnwrapped(ConversionContext context, DocumentAccessor documen } @Override + @SuppressWarnings("NullAway") public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { org.springframework.data.mongodb.core.mapping.DBRef annotation; if (referringProperty != null) { annotation = referringProperty.getDBRef(); - Assert.isTrue(annotation != null, "The referenced property has to be mapped with @DBRef"); + Assert.notNull(annotation, "The referenced property has to be mapped with @DBRef"); } // DATAMONGO-913 @@ -780,6 +786,7 @@ public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringP } @Override + @SuppressWarnings("NullAway") public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (source instanceof LazyLoadingProxy proxy) { @@ -799,6 +806,7 @@ public DocumentPointer toDocumentPointer(Object source, @Nullable MongoPersisten throw new IllegalArgumentException("The referringProperty is neither a DBRef nor a document reference"); } + @SuppressWarnings("NullAway") DocumentPointer createDocumentPointer(Object source, @Nullable MongoPersistentProperty referringProperty) { if (referringProperty == null) { @@ -863,7 +871,7 @@ private boolean requiresTypeHint(Class type) { /** * Internal write conversion method which should be used for nested invocations. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked","NullAway"}) protected void writeInternal(@Nullable Object obj, Bson bson, @Nullable TypeInformation typeHint) { if (null == obj) { @@ -1267,6 +1275,7 @@ protected String potentiallyEscapeMapKey(String source) { * * @param key */ + @SuppressWarnings("NullAway") private String potentiallyConvertMapKey(Object key) { if (key instanceof String stringValue) { @@ -1332,20 +1341,23 @@ private void writeSimpleInternal(@Nullable Object value, Bson bson, MongoPersist property.hasExplicitWriteTarget() ? property.getFieldType() : Object.class)); } - @Nullable @SuppressWarnings("unchecked") - private Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property, + private @Nullable Object applyPropertyConversion(@Nullable Object value, MongoPersistentProperty property, PersistentPropertyAccessor persistentPropertyAccessor) { MongoConversionContext context = new MongoConversionContext(new PropertyValueProvider<>() { - @Nullable @Override - public T getPropertyValue(MongoPersistentProperty property) { + public @Nullable T getPropertyValue(MongoPersistentProperty property) { return (T) persistentPropertyAccessor.getProperty(property); } }, property, this, spELContext); - PropertyValueConverter> valueConverter = conversions - .getPropertyValueConversions().getValueConverter(property); + + PropertyValueConversions propertyValueConversions = conversions.getPropertyValueConversions(); + if(propertyValueConversions == null) { + return value; + } + + PropertyValueConverter> valueConverter = propertyValueConversions.getValueConverter(property); return value != null ? valueConverter.write(value, context) : valueConverter.writeNull(context); } @@ -1353,8 +1365,9 @@ public T getPropertyValue(MongoPersistentProperty property) { * Checks whether we have a custom conversion registered for the given value into an arbitrary simple Mongo type. * Returns the converted value if so. If not, we perform special enum handling or simply return the value as is. */ - @Nullable - private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { + @Contract("null, _-> null") + @SuppressWarnings("NullAway") + private @Nullable Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nullable Class typeHint) { if (value == null) { return null; @@ -1390,7 +1403,7 @@ private Object getPotentiallyConvertedSimpleWrite(@Nullable Object value, @Nulla * * @since 3.2 */ - protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation target) { + protected @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, TypeInformation target) { return getPotentiallyConvertedSimpleRead(value, target.getType()); } @@ -1398,10 +1411,11 @@ protected Object getPotentiallyConvertedSimpleRead(Object value, TypeInformation * Checks whether we have a custom conversion for the given simple object. Converts the given value if so, applies * {@link Enum} handling or returns the value as is. */ + @Contract("null, _ -> null; _, null -> param1") @SuppressWarnings({ "rawtypes", "unchecked" }) - private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class target) { + private @Nullable Object getPotentiallyConvertedSimpleRead(@Nullable Object value, @Nullable Class target) { - if (target == null) { + if (target == null || value == null) { return value; } @@ -1420,6 +1434,7 @@ private Object getPotentiallyConvertedSimpleRead(Object value, @Nullable Class source, + @SuppressWarnings({"unchecked","NullAway"}) + protected @Nullable Object readCollectionOrArray(ConversionContext context, @Nullable Collection source, TypeInformation targetType) { Assert.notNull(targetType, "Target type must not be null"); + Assert.notNull(source, "Source must not be null"); Class collectionType = targetType.isSubTypeOf(Collection.class) // ? targetType.getType() // @@ -1517,7 +1532,7 @@ protected Object readCollectionOrArray(ConversionContext context, Collection * @return the converted {@link Map}, will never be {@literal null}. * @since 3.2 */ - protected Map readMap(ConversionContext context, Bson bson, TypeInformation targetType) { + protected @Nullable Map readMap(ConversionContext context, @Nullable Bson bson, TypeInformation targetType) { Assert.notNull(bson, "Document must not be null"); Assert.notNull(targetType, "TypeInformation must not be null"); @@ -1714,9 +1729,8 @@ private Object removeTypeInfo(Object object, boolean recursively) { return document; } - @Nullable @SuppressWarnings("unchecked") - T readValue(ConversionContext context, @Nullable Object value, TypeInformation type) { + @Nullable T readValue(ConversionContext context, @Nullable Object value, TypeInformation type) { if (value == null) { return null; @@ -1735,8 +1749,7 @@ T readValue(ConversionContext context, @Nullable Object value, TypeInformati return (T) context.convert(value, type); } - @Nullable - private Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation type) { + private @Nullable Object readDBRef(ConversionContext context, @Nullable DBRef dbref, TypeInformation type) { if (type.getType().equals(DBRef.class)) { return dbref; @@ -1801,6 +1814,7 @@ private List bulkReadAndConvertDBRefs(ConversionContext context, List event) { if (canPublishEvent()) { @@ -1880,12 +1894,12 @@ public MappingMongoConverter with(MongoDatabaseFactory dbFactory) { return target; } - private T doConvert(Object value, Class target) { + private @Nullable T doConvert(Object value, Class target) { return doConvert(value, target, null); } @SuppressWarnings("ConstantConditions") - private T doConvert(Object value, Class target, + private @Nullable T doConvert(Object value, Class target, @Nullable Class fallback) { if (conversionService.canConvert(value.getClass(), target) || fallback == null) { @@ -1939,7 +1953,7 @@ static class MongoDbPropertyValueProvider implements PropertyValueProvider T getPropertyValue(MongoPersistentProperty property) { + @SuppressWarnings({"unchecked", "NullAway"}) + public @Nullable T getPropertyValue(MongoPersistentProperty property) { String expression = property.getSpelExpression(); Object value = expression != null ? evaluator.evaluate(expression) : accessor.get(property); @@ -2109,7 +2122,7 @@ enum NoOpParameterValueProvider implements ParameterValueProvider T getParameterValue(Parameter parameter) { + public @Nullable T getParameterValue(Parameter parameter) { return null; } } @@ -2137,7 +2150,7 @@ public List> getParameterTypes( } @Override - public org.springframework.data.util.TypeInformation getProperty(String property) { + public org.springframework.data.util.@Nullable TypeInformation getProperty(String property) { return delegate.getProperty(property); } @@ -2172,7 +2185,7 @@ public TypeInformation getRawTypeInformation() { } @Override - public org.springframework.data.util.TypeInformation getActualType() { + public org.springframework.data.util.@Nullable TypeInformation getActualType() { return delegate.getActualType(); } @@ -2187,7 +2200,7 @@ public List> getParameterTypes( } @Override - public org.springframework.data.util.TypeInformation getSuperTypeInformation(Class superType) { + public org.springframework.data.util.@Nullable TypeInformation getSuperTypeInformation(Class superType) { return delegate.getSuperTypeInformation(superType); } @@ -2279,8 +2292,7 @@ default ConversionContext forProperty(MongoPersistentProperty property) { * @return * @param */ - @Nullable - default S findContextualEntity(MongoPersistentEntity entity, Document document) { + default @Nullable S findContextualEntity(MongoPersistentEntity entity, Document document) { return null; } @@ -2314,7 +2326,7 @@ public ConversionContext withPath(ObjectPath currentPath) { } @Override - public S findContextualEntity(MongoPersistentEntity entity, Document document) { + public @Nullable S findContextualEntity(MongoPersistentEntity entity, Document document) { Object identifier = document.get(BasicMongoPersistentProperty.ID_FIELD_NAME); @@ -2351,15 +2363,15 @@ protected static class DefaultConversionContext implements ConversionContext { final ObjectPath path; final ContainerValueConverter documentConverter; final ContainerValueConverter> collectionConverter; - final ContainerValueConverter mapConverter; - final ContainerValueConverter dbRefConverter; - final ValueConverter elementConverter; + final ContainerValueConverter<@Nullable Bson> mapConverter; + final ContainerValueConverter<@Nullable DBRef> dbRefConverter; + final ValueConverter<@Nullable Object> elementConverter; DefaultConversionContext(MongoConverter sourceConverter, org.springframework.data.convert.CustomConversions customConversions, ObjectPath path, - ContainerValueConverter documentConverter, ContainerValueConverter> collectionConverter, - ContainerValueConverter mapConverter, ContainerValueConverter dbRefConverter, - ValueConverter elementConverter) { + ContainerValueConverter<@Nullable Bson> documentConverter, ContainerValueConverter<@Nullable Collection> collectionConverter, + ContainerValueConverter<@Nullable Bson> mapConverter, ContainerValueConverter<@Nullable DBRef> dbRefConverter, + ValueConverter<@Nullable Object> elementConverter) { this.sourceConverter = sourceConverter; this.conversions = customConversions; @@ -2371,8 +2383,8 @@ protected static class DefaultConversionContext implements ConversionContext { this.elementConverter = elementConverter; } - @SuppressWarnings("unchecked") @Override + @SuppressWarnings({"unchecked", "NullAway"}) public S convert(Object source, TypeInformation typeHint, ConversionContext context) { @@ -2453,7 +2465,7 @@ public ObjectPath getPath() { */ interface ValueConverter { - Object convert(T source, TypeInformation typeHint); + @Nullable Object convert(@Nullable T source, TypeInformation typeHint); } @@ -2465,7 +2477,7 @@ interface ValueConverter { */ interface ContainerValueConverter { - Object convert(ConversionContext context, T source, TypeInformation typeHint); + @Nullable Object convert(ConversionContext context, @Nullable T source, TypeInformation typeHint); } @@ -2480,7 +2492,7 @@ class ProjectingConversionContext extends DefaultConversionContext { ProjectingConversionContext(MongoConverter sourceConverter, CustomConversions customConversions, ObjectPath path, ContainerValueConverter> collectionConverter, ContainerValueConverter mapConverter, - ContainerValueConverter dbRefConverter, ValueConverter elementConverter, + ContainerValueConverter<@Nullable DBRef> dbRefConverter, ValueConverter<@Nullable Object> elementConverter, EntityProjection projection) { super(sourceConverter, customConversions, path, (context, source, typeHint) -> doReadOrProject(context, source, typeHint, projection), @@ -2532,7 +2544,7 @@ public void setProperty(PersistentProperty property, @Nullable Object value) } @Override - public Object getProperty(PersistentProperty property) { + public @Nullable Object getProperty(PersistentProperty property) { return delegate.getProperty(translate(property)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java index da106715d4..0cc687c815 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConversionContext.java @@ -16,13 +16,13 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.ValueConversionContext; import org.springframework.data.mapping.model.PropertyValueProvider; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.CheckReturnValue; -import org.springframework.lang.Nullable; /** * {@link ValueConversionContext} that allows to delegate read/write to an underlying {@link MongoConverter}. @@ -95,12 +95,12 @@ public Object getValue(String propertyPath) { @Override @SuppressWarnings("unchecked") - public T write(@Nullable Object value, TypeInformation target) { + public @Nullable T write(@Nullable Object value, TypeInformation target) { return (T) mongoConverter.convertToMongoType(value, target); } @Override - public T read(@Nullable Object value, TypeInformation target) { + public @Nullable T read(@Nullable Object value, TypeInformation target) { return value instanceof Bson bson ? mongoConverter.read(target.getType(), bson) : ValueConversionContext.super.read(value, target); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java index 3676e74c8b..e147d64cc5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverter.java @@ -21,6 +21,7 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionException; import org.springframework.data.convert.CustomConversions; import org.springframework.data.convert.EntityConverter; @@ -33,7 +34,6 @@ import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -101,9 +101,8 @@ public interface MongoConverter * @throws IllegalArgumentException if {@literal targetType} is {@literal null}. * @since 2.1 */ - @SuppressWarnings("unchecked") - @Nullable - default T mapValueToTargetType(S source, Class targetType, DbRefResolver dbRefResolver) { + @SuppressWarnings({"unchecked","NullAway"}) + default @Nullable T mapValueToTargetType(S source, Class targetType, DbRefResolver dbRefResolver) { Assert.notNull(targetType, "TargetType must not be null"); Assert.notNull(dbRefResolver, "DbRefResolver must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java index f9a67d73a0..1fd45e1960 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoConverters.java @@ -47,6 +47,7 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; @@ -142,7 +143,7 @@ public String convert(ObjectId id) { enum StringToObjectIdConverter implements Converter { INSTANCE; - public ObjectId convert(String source) { + public @Nullable ObjectId convert(String source) { return StringUtils.hasText(source) ? new ObjectId(source) : null; } } @@ -206,7 +207,7 @@ public Decimal128 convert(BigInteger source) { enum StringToBigDecimalConverter implements Converter { INSTANCE; - public BigDecimal convert(String source) { + public @Nullable BigDecimal convert(String source) { return StringUtils.hasText(source) ? new BigDecimal(source) : null; } } @@ -235,7 +236,7 @@ public String convert(BigInteger source) { enum StringToBigIntegerConverter implements Converter { INSTANCE; - public BigInteger convert(String source) { + public @Nullable BigInteger convert(String source) { return StringUtils.hasText(source) ? new BigInteger(source) : null; } } @@ -312,20 +313,25 @@ public String convert(Term source) { * @author Christoph Strobl * @since 1.7 */ + @SuppressWarnings("NullAway") enum DocumentToNamedMongoScriptConverter implements Converter { INSTANCE; @Override - public NamedMongoScript convert(Document source) { + public @Nullable NamedMongoScript convert(Document source) { if (source.isEmpty()) { return null; } String id = source.get(FieldName.ID.name()).toString(); + Assert.notNull(id, "Script id must not be null"); + Object rawValue = source.get("value"); + Assert.isInstanceOf(Code.class, rawValue); + return new NamedMongoScript(id, ((Code) rawValue).getCode()); } } @@ -379,7 +385,7 @@ enum StringToCurrencyConverter implements Converter { INSTANCE; @Override - public Currency convert(String source) { + public @Nullable Currency convert(String source) { return StringUtils.hasText(source) ? Currency.getInstance(source) : null; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java index 050c3bd27d..8dccced380 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoCustomConversions.java @@ -32,6 +32,7 @@ import java.util.Set; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; @@ -51,7 +52,7 @@ import org.springframework.data.mongodb.core.convert.MongoConverters.StringToBigIntegerConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -137,7 +138,7 @@ public Set getConvertibleTypes() { return new HashSet<>(Arrays.asList(localeToString, booleanToString)); } - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return source != null ? source.toString() : null; } } @@ -188,6 +189,7 @@ public static MongoConverterConfigurationAdapter from(List converters) { * @param converter must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverter(Converter converter) { Assert.notNull(converter, "Converter must not be null"); @@ -202,6 +204,7 @@ public MongoConverterConfigurationAdapter registerConverter(Converter conv * @param converters must not be {@literal null} nor contain {@literal null} values. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverters(Collection converters) { Assert.notNull(converters, "Converters must not be null"); @@ -217,6 +220,7 @@ public MongoConverterConfigurationAdapter registerConverters(Collection conve * @param converterFactory must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFactory converterFactory) { Assert.notNull(converterFactory, "ConverterFactory must not be null"); @@ -232,6 +236,7 @@ public MongoConverterConfigurationAdapter registerConverterFactory(ConverterFact * @return this. * @since 3.4 */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory( PropertyValueConverterFactory converterFactory) { @@ -249,6 +254,7 @@ public MongoConverterConfigurationAdapter registerPropertyValueConverterFactory( * @return this. * @since 3.4 */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter configurePropertyConversions( Consumer> configurationAdapter) { @@ -271,6 +277,7 @@ public MongoConverterConfigurationAdapter configurePropertyConversions( * @param useNativeDriverJavaTimeCodecs * @return this. */ + @Contract("_ -> this") public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean useNativeDriverJavaTimeCodecs) { this.useNativeDriverJavaTimeCodecs = useNativeDriverJavaTimeCodecs; @@ -285,6 +292,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs(boolean * @return this. * @see #useNativeDriverJavaTimeCodecs(boolean) */ + @Contract("-> this") public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(true); } @@ -299,6 +307,7 @@ public MongoConverterConfigurationAdapter useNativeDriverJavaTimeCodecs() { * @return this. * @see #useNativeDriverJavaTimeCodecs(boolean) */ + @Contract("-> this") public MongoConverterConfigurationAdapter useSpringDataJavaTimeCodecs() { return useNativeDriverJavaTimeCodecs(false); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java index 0316251dc1..67f9d5ec46 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoExampleMapper.java @@ -28,6 +28,7 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.domain.ExampleMatcher.NullHandler; import org.springframework.data.domain.ExampleMatcher.PropertyValueTransformer; @@ -98,13 +99,17 @@ public Document getMappedExample(Example example) { * @param entity must not be {@literal null}. * @return */ - public Document getMappedExample(Example example, MongoPersistentEntity entity) { + @SuppressWarnings("NullAway") + public Document getMappedExample(Example example, @Nullable MongoPersistentEntity entity) { Assert.notNull(example, "Example must not be null"); - Assert.notNull(entity, "MongoPersistentEntity must not be null"); Document reference = (Document) converter.convertToMongoType(example.getProbe()); + if(entity != null) { + entity = mappingContext.getRequiredPersistentEntity(example.getProbeType()); + } + if (entity.getIdProperty() != null && ClassUtils.isAssignable(entity.getType(), example.getProbeType())) { Object identifier = entity.getIdentifierAccessor(example.getProbe()).getIdentifier(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java index 8d199083e7..a5d329045e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoJsonSchemaMapper.java @@ -21,12 +21,12 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.schema.JsonSchemaObject.Type; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java index 867a6213d2..8aeb576c67 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MongoWriter.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.core.convert; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.convert.EntityWriter; import org.springframework.data.mongodb.core.mapping.DocumentPointer; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -43,8 +43,7 @@ public interface MongoWriter extends EntityWriter { * @param obj can be {@literal null}. * @return */ - @Nullable - default Object convertToMongoType(@Nullable Object obj) { + default @Nullable Object convertToMongoType(@Nullable Object obj) { return convertToMongoType(obj, (TypeInformation) null); } @@ -59,7 +58,7 @@ default Object convertToMongoType(@Nullable Object obj) { @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation); - default Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { + default @Nullable Object convertToMongoType(@Nullable Object obj, MongoPersistentEntity entity) { return convertToMongoType(obj, entity.getTypeInformation()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java index 265257af5c..68578f32b9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/NoOpDbRefResolver.java @@ -18,9 +18,8 @@ import java.util.List; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; @@ -37,16 +36,14 @@ public enum NoOpDbRefResolver implements DbRefResolver { INSTANCE; @Override - @Nullable - public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, + public @Nullable Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler proxyHandler) { return handle(); } @Override - @Nullable - public Document fetch(DBRef dbRef) { + public @Nullable Document fetch(DBRef dbRef) { return handle(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java index 5fefd472c4..d5f034eb1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ObjectPath.java @@ -18,9 +18,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -99,8 +99,7 @@ ObjectPath push(Object object, MongoPersistentEntity entity, @Nullable Object * @return {@literal null} when no match found. * @since 2.0 */ - @Nullable - T getPathItem(Object id, String collection, Class type) { + @Nullable T getPathItem(Object id, String collection, Class type) { Assert.notNull(id, "Id must not be null"); Assert.hasText(collection, "Collection name must not be null"); @@ -133,13 +132,11 @@ Object getCurrentObject() { return getObject(); } - @Nullable - private Object getObject() { + private @Nullable Object getObject() { return object; } - @Nullable - private Object getIdValue() { + private @Nullable Object getIdValue() { return idValue; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java index debaf2f127..11ed30aedd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/QueryMapper.java @@ -37,6 +37,8 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.Converter; import org.springframework.data.annotation.Reference; @@ -67,7 +69,7 @@ import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -137,6 +139,7 @@ public Document getMappedObject(Bson query, Optional entity) { if (isNestedKeyword(query)) { @@ -277,6 +280,7 @@ public Document addMetaAttributes(Document source, @Nullable MongoPersistentEnti return mapMetaAttributes(source, entity, MetaMapping.FORCE); } + @SuppressWarnings("NullAway") private Document mapMetaAttributes(Document source, @Nullable MongoPersistentEntity entity, MetaMapping metaMapping) { @@ -349,7 +353,7 @@ private Document getMappedTextScoreField(MongoPersistentProperty property) { * @param rawValue * @return */ - protected Entry getMappedObjectForField(Field field, Object rawValue) { + protected Entry getMappedObjectForField(Field field, @Nullable Object rawValue) { String key = field.getMappedKey(); Object value; @@ -413,7 +417,9 @@ protected Document getMappedKeyword(Keyword keyword, @Nullable MongoPersistentEn } if (keyword.isSample()) { - return exampleMapper.getMappedExample(keyword.getValue(), entity); + + Example example = keyword.getValue(); + return exampleMapper.getMappedExample(example, entity != null ? entity : mappingContext.getRequiredPersistentEntity(example.getProbeType())); } if (keyword.isJsonSchema()) { @@ -455,8 +461,8 @@ protected Document getMappedKeyword(Field property, Keyword keyword) { * @return */ @Nullable - @SuppressWarnings("unchecked") - protected Object getMappedValue(Field documentField, Object sourceValue) { + @SuppressWarnings("NullAway") + protected Object getMappedValue(Field documentField, @Nullable Object sourceValue) { Object value = applyFieldTargetTypeHintToValue(documentField, sourceValue); @@ -493,6 +499,7 @@ private boolean isIdField(Field documentField) { && documentField.getProperty().getOwner().isIdProperty(documentField.getProperty()); } + @SuppressWarnings("NullAway") private Class getIdTypeForField(Field documentField) { return isIdField(documentField) ? documentField.getProperty().getFieldType() : ObjectId.class; } @@ -531,7 +538,7 @@ protected boolean isAssociationConversionNecessary(Field documentField, @Nullabl } MongoPersistentEntity entity = documentField.getPropertyEntity(); - return entity.hasIdProperty() + return entity != null && entity.hasIdProperty() && (type.equals(DBRef.class) || entity.getRequiredIdProperty().getActualType().isAssignableFrom(type)); } @@ -667,14 +674,14 @@ protected Object convertAssociation(@Nullable Object source, @Nullable MongoPers return createReferenceFor(source, property); } - @Nullable - private Object convertValue(Field documentField, Object sourceValue, Object value, + private @Nullable Object convertValue(Field documentField, @Nullable Object sourceValue, @Nullable Object value, PropertyValueConverter> valueConverter) { MongoPersistentProperty property = documentField.getProperty(); OperatorContext criteriaContext = new QueryOperatorContext( - isKeyword(documentField.name) ? documentField.name : "$eq", property.getFieldName()); + isKeyword(documentField.name) ? documentField.name : "$eq", property != null ? property.getFieldName() : documentField.name); + MongoConversionContext conversionContext; if (valueConverter instanceof MongoConversionContext mcc) { conversionContext = mcc.forOperator(criteriaContext); @@ -686,8 +693,8 @@ private Object convertValue(Field documentField, Object sourceValue, Object valu return convertValueWithConversionContext(documentField, sourceValue, value, valueConverter, conversionContext); } - @Nullable - protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, + @SuppressWarnings("NullAway") + protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value, PropertyValueConverter> valueConverter, MongoConversionContext conversionContext) { @@ -707,7 +714,7 @@ protected Object convertValueWithConversionContext(Field documentField, Object s return converted; } - if (property != null && !documentField.getProperty().isMap() && sourceValue instanceof Document document) { + if (property != null && !property.isMap() && sourceValue instanceof Document document) { return BsonUtils.mapValues(document, (key, val) -> { if (isKeyword(key)) { @@ -718,11 +725,11 @@ protected Object convertValueWithConversionContext(Field documentField, Object s }); } - return valueConverter.write(value, conversionContext); + return value != null ? valueConverter.write(value, conversionContext) : value; } @Nullable - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) private Object convertIdField(Field documentField, Object source) { Object value = source; @@ -856,6 +863,7 @@ public Object convertId(@Nullable Object id, Class targetType) { * @param candidate * @return */ + @Contract("null -> false") protected boolean isNestedKeyword(@Nullable Object candidate) { if (!(candidate instanceof Document)) { @@ -890,8 +898,8 @@ protected boolean isTypeKey(String key) { * @param candidate * @return */ - protected boolean isKeyword(String candidate) { - return candidate.startsWith("$"); + protected boolean isKeyword(@Nullable String candidate) { + return candidate != null && candidate.startsWith("$"); } /** @@ -936,6 +944,7 @@ private Object applyFieldTargetTypeHintToValue(Field documentField, @Nullable Ob * @author Oliver Gierke * @author Christoph Strobl */ + @SuppressWarnings("NullAway") static class Keyword { private static final Set NON_DBREF_CONVERTING_KEYWORDS = Set.of("$", "$size", "$slice", "$gt", "$lt"); @@ -1184,6 +1193,7 @@ public MetadataBackedField(String name, MongoPersistentEntity entity, * @param context must not be {@literal null}. * @param property may be {@literal null}. */ + @SuppressWarnings("NullAway") public MetadataBackedField(String name, MongoPersistentEntity entity, MappingContext, MongoPersistentProperty> context, @Nullable MongoPersistentProperty property) { @@ -1227,7 +1237,7 @@ public MongoPersistentProperty getProperty() { } @Override - public MongoPersistentEntity getPropertyEntity() { + public @Nullable MongoPersistentEntity getPropertyEntity() { MongoPersistentProperty property = getProperty(); return property == null ? null : mappingContext.getPersistentEntity(property); } @@ -1244,7 +1254,7 @@ public boolean isAssociation() { } @Override - public Association getAssociation() { + public @Nullable Association getAssociation() { return association; } @@ -1447,6 +1457,7 @@ protected Converter getPropertyConverter() { * @return * @since 1.7 */ + @SuppressWarnings("NullAway") protected Converter getAssociationConverter() { return new AssociationConverter(name, getAssociation()); } @@ -1598,7 +1609,7 @@ public AssociationConverter(String name, Association as } @Override - public String convert(MongoPersistentProperty source) { + public @Nullable String convert(MongoPersistentProperty source) { if (associationFound) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java index 5a1adf9114..cd7d55311d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLoader.java @@ -20,9 +20,9 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.ReferenceResolver.ReferenceCollection; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; import com.mongodb.client.MongoCollection; @@ -42,8 +42,7 @@ public interface ReferenceLoader { * @param context must not be {@literal null}. * @return the matching {@link Document} or {@literal null} if none found. */ - @Nullable - default Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { + default @Nullable Document fetchOne(DocumentReferenceQuery referenceQuery, ReferenceCollection context) { Iterator it = fetchMany(referenceQuery, context).iterator(); return it.hasNext() ? it.next() : null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java index b912cfb540..a0e5a6f2bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceLookupDelegate.java @@ -31,6 +31,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mapping.model.SpELContext; import org.springframework.data.mongodb.core.convert.ReferenceLoader.DocumentReferenceQuery; @@ -47,7 +48,6 @@ import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.data.util.Streamable; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -98,8 +98,7 @@ public ReferenceLookupDelegate( * {@literal null}. * @return can be {@literal null}. */ - @Nullable - public Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, + public @Nullable Object readReference(MongoPersistentProperty property, Object source, LookupFunction lookupFunction, MongoEntityReader entityReader) { Object value = source instanceof DocumentReferenceSource documentReferenceSource @@ -126,7 +125,7 @@ public Object readReference(MongoPersistentProperty property, Object source, Loo @Nullable private Iterable retrieveRawDocuments(MongoPersistentProperty property, Object source, - LookupFunction lookupFunction, Object value) { + LookupFunction lookupFunction, @Nullable Object value) { DocumentReferenceQuery filter = computeFilter(property, source, spELContext); if (filter instanceof NoResultsFilter) { @@ -137,7 +136,8 @@ private Iterable retrieveRawDocuments(MongoPersistentProperty property return lookupFunction.apply(filter, referenceCollection); } - private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, Object value, + @SuppressWarnings("NullAway") + private ReferenceCollection computeReferenceContext(MongoPersistentProperty property, @Nullable Object value, SpELContext spELContext) { // Use the first value as a reference for others in case of collection like @@ -195,7 +195,7 @@ private ReferenceCollection computeReferenceContext(MongoPersistentProperty prop * @return can be {@literal null}. */ @SuppressWarnings("unchecked") - private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier defaultValue) { + private T parseValueOrGet(String value, ParameterBindingContext bindingContext, Supplier<@Nullable T> defaultValue) { if (!StringUtils.hasText(value)) { return defaultValue.get(); @@ -220,7 +220,7 @@ private T parseValueOrGet(String value, ParameterBindingContext bindingConte return evaluated != null ? evaluated : defaultValue.get(); } - ParameterBindingContext bindingContext(MongoPersistentProperty property, Object source, SpELContext spELContext) { + ParameterBindingContext bindingContext(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) { ValueProvider valueProvider = valueProviderFor(DocumentReferenceSource.getTargetSource(source)); @@ -228,7 +228,7 @@ ParameterBindingContext bindingContext(MongoPersistentProperty property, Object () -> evaluationContextFor(property, source, spELContext)); } - ValueProvider valueProviderFor(Object source) { + ValueProvider valueProviderFor(@Nullable Object source) { return index -> { if (source instanceof Document document) { @@ -238,7 +238,7 @@ ValueProvider valueProviderFor(Object source) { }; } - EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object source, SpELContext spELContext) { + EvaluationContext evaluationContextFor(MongoPersistentProperty property, @Nullable Object source, SpELContext spELContext) { Object target = source instanceof DocumentReferenceSource documentReferenceSource ? documentReferenceSource.getTargetSource() @@ -264,7 +264,7 @@ EvaluationContext evaluationContextFor(MongoPersistentProperty property, Object * @param spELContext must not be {@literal null}. * @return never {@literal null}. */ - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked","NullAway"}) DocumentReferenceQuery computeFilter(MongoPersistentProperty property, Object source, SpELContext spELContext) { DocumentReference documentReference = property.isDocumentReference() ? property.getDocumentReference() diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java index 715327d18e..0698b08bf8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ReferenceResolver.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.core.convert; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.DBRef; @@ -54,8 +54,7 @@ Object resolveReference(MongoPersistentProperty property, Object source, */ class ReferenceCollection { - @Nullable // - private final String database; + private final @Nullable String database; private final String collection; /** @@ -95,8 +94,7 @@ public String getCollection() { * * @return can be {@literal null}. */ - @Nullable - public String getDatabase() { + public @Nullable String getDatabase() { return database; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java index 805bafe974..bff72427f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/UpdateMapper.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.bson.conversions.Bson; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.PropertyValueConverter; import org.springframework.data.convert.ValueConversionContext; @@ -37,7 +38,6 @@ import org.springframework.data.mongodb.core.query.Update.Modifier; import org.springframework.data.mongodb.core.query.Update.Modifiers; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -132,7 +132,7 @@ public static boolean isUpdateObject(@Nullable Document updateObj) { * org.springframework.data.mongodb.core.mapping.MongoPersistentEntity) */ @Override - protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { + protected @Nullable Object delegateConvertToMongoType(Object source, @Nullable MongoPersistentEntity entity) { if (entity != null && entity.isUnwrapped()) { return converter.convertToMongoType(source, entity); @@ -143,7 +143,8 @@ protected Object delegateConvertToMongoType(Object source, @Nullable MongoPersis } @Override - protected Entry getMappedObjectForField(Field field, Object rawValue) { + @SuppressWarnings("NullAway") + protected Entry getMappedObjectForField(Field field, @Nullable Object rawValue) { if (isDocument(rawValue)) { @@ -163,7 +164,7 @@ protected Entry getMappedObjectForField(Field field, Object rawV return super.getMappedObjectForField(field, rawValue); } - protected Object convertValueWithConversionContext(Field documentField, Object sourceValue, Object value, + protected @Nullable Object convertValueWithConversionContext(Field documentField, @Nullable Object sourceValue, @Nullable Object value, PropertyValueConverter> valueConverter, MongoConversionContext conversionContext) { @@ -206,11 +207,12 @@ private boolean isQuery(@Nullable Object value) { return value instanceof Query; } - private Document getMappedValue(@Nullable Field field, Modifier modifier) { + private @Nullable Document getMappedValue(@Nullable Field field, Modifier modifier) { return new Document(modifier.getKey(), getMappedModifier(field, modifier)); } - private Object getMappedModifier(@Nullable Field field, Modifier modifier) { + @SuppressWarnings("NullAway") + private @Nullable Object getMappedModifier(@Nullable Field field, Modifier modifier) { Object value = modifier.getValue(); @@ -221,7 +223,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) { : getMappedSort(sortObject, field.getPropertyEntity()); } - if (isAssociationConversionNecessary(field, value)) { + if (field != null && isAssociationConversionNecessary(field, value)) { if (ObjectUtils.isArray(value) || value instanceof Collection) { List targetPointers = new ArrayList<>(); for (Object val : converter.getConversionService().convert(value, List.class)) { @@ -239,7 +241,7 @@ private Object getMappedModifier(@Nullable Field field, Modifier modifier) { private TypeInformation getTypeHintForEntity(@Nullable Object source, MongoPersistentEntity entity) { TypeInformation info = entity.getTypeInformation(); - Class type = info.getActualType().getType(); + Class type = info.getRequiredActualType().getType(); if (source == null || type.isInterface() || java.lang.reflect.Modifier.isAbstract(type.getModifiers())) { return info; @@ -257,7 +259,7 @@ private TypeInformation getTypeHintForEntity(@Nullable Object source, MongoPe } @Override - protected Field createPropertyField(MongoPersistentEntity entity, String key, + protected Field createPropertyField(@Nullable MongoPersistentEntity entity, String key, MappingContext, MongoPersistentProperty> mappingContext) { return entity == null ? super.createPropertyField(entity, key, mappingContext) @@ -316,6 +318,7 @@ protected Converter getPropertyConverter() { } @Override + @SuppressWarnings("NullAway") protected Converter getAssociationConverter() { return new UpdateAssociationConverter(getMappingContext(), getAssociation(), key); } @@ -343,7 +346,7 @@ public UpdateAssociationConverter( } @Override - public String convert(MongoPersistentProperty source) { + public @Nullable String convert(MongoPersistentProperty source) { return super.convert(source) == null ? null : mapper.mapPropertyName(source); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java index 0a96cc867a..8eed053a09 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/ValueResolver.java @@ -17,10 +17,9 @@ import org.bson.Document; import org.bson.conversions.Bson; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; -import org.springframework.lang.Nullable; /** * Internal API to trigger the resolution of properties. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java index 4097be7704..b31d8a2b7c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/EncryptingConverter.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoValueConverter; import org.springframework.data.mongodb.core.encryption.EncryptionContext; @@ -28,7 +29,7 @@ public interface EncryptingConverter extends MongoValueConverter { @Override - default S read(Object value, MongoConversionContext context) { + default @Nullable S read(Object value, MongoConversionContext context) { return decrypt(value, buildEncryptionContext(context)); } @@ -39,10 +40,10 @@ default S read(Object value, MongoConversionContext context) { * @param context the context to operate in. * @return never {@literal null}. */ - S decrypt(Object encryptedValue, EncryptionContext context); + @Nullable S decrypt(Object encryptedValue, EncryptionContext context); @Override - default T write(Object value, MongoConversionContext context) { + default @Nullable T write(Object value, MongoConversionContext context) { return encrypt(value, buildEncryptionContext(context)); } @@ -53,7 +54,7 @@ default T write(Object value, MongoConversionContext context) { * @param context the context to operate in. * @return never {@literal null}. */ - T encrypt(Object value, EncryptionContext context); + T encrypt(@Nullable Object value, EncryptionContext context); /** * Obtain the {@link EncryptionContext} for a given {@link MongoConversionContext value conversion context}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java index 67c30fcf94..0431cf11dd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/ExplicitEncryptionContext.java @@ -15,13 +15,13 @@ */ package org.springframework.data.mongodb.core.convert.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.encryption.EncryptionContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; /** * Default {@link EncryptionContext} implementation. @@ -43,29 +43,33 @@ public MongoPersistentProperty getProperty() { return conversionContext.getProperty(); } - @Nullable @Override - public Object lookupValue(String path) { + public @Nullable Object lookupValue(String path) { return conversionContext.getValue(path); } @Override - public Object convertToMongoType(Object value) { + public @Nullable Object convertToMongoType(Object value) { return conversionContext.write(value); } @Override - public EvaluationContext getEvaluationContext(Object source) { - return conversionContext.getSpELContext().getEvaluationContext(source); + public EvaluationContext getEvaluationContext(@Nullable Object source) { + + if(conversionContext.getSpELContext() != null) { + return conversionContext.getSpELContext().getEvaluationContext(source); + } + + throw new IllegalStateException("SpEL context not present"); } @Override - public T read(@Nullable Object value, TypeInformation target) { + public @Nullable T read(@Nullable Object value, TypeInformation target) { return conversionContext.read(value, target); } @Override - public T write(@Nullable Object value, TypeInformation target) { + public @Nullable T write(@Nullable Object value, TypeInformation target) { return conversionContext.write(value, target); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 8d29847aae..5bc100c48a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -81,7 +81,7 @@ public Object read(Object value, MongoConversionContext context) { } @Override - public Object decrypt(Object encryptedValue, EncryptionContext context) { + public @Nullable Object decrypt(Object encryptedValue, EncryptionContext context) { Object decryptedValue = encryptedValue; if (encryptedValue instanceof Binary || encryptedValue instanceof BsonBinary) { @@ -155,7 +155,8 @@ public Object decrypt(Object encryptedValue, EncryptionContext context) { } @Override - public Object encrypt(Object value, EncryptionContext context) { + @SuppressWarnings("NullAway") + public Object encrypt(@Nullable Object value, EncryptionContext context) { if (LOGGER.isDebugEnabled()) { LOGGER.debug(String.format("Encrypting %s.%s.", getProperty(context).getOwner().getName(), diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java index 4a6f78357a..a0e8ea27f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/package-info.java @@ -3,5 +3,5 @@ * explicit encryption * mechanism of Client-Side Field Level Encryption. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.convert.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java index cfa07fa8f9..dbef5cbb90 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/package-info.java @@ -1,6 +1,6 @@ /** * Spring Data MongoDB specific converter infrastructure. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.convert; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java index 5f5e29578d..45e83ed7ca 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionContext.java @@ -15,12 +15,12 @@ */ package org.springframework.data.mongodb.core.encryption; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentProperty; import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; -import org.springframework.lang.Nullable; /** * Context to encapsulate encryption for a specific {@link MongoPersistentProperty}. @@ -45,7 +45,7 @@ public interface EncryptionContext { * @param value * @return */ - Object convertToMongoType(Object value); + @Nullable Object convertToMongoType(Object value); /** * Reads the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. @@ -54,7 +54,7 @@ public interface EncryptionContext { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - default T read(@Nullable Object value) { + default @Nullable T read(@Nullable Object value) { return (T) read(value, getProperty().getTypeInformation()); } @@ -66,7 +66,7 @@ default T read(@Nullable Object value) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - default T read(@Nullable Object value, Class target) { + default @Nullable T read(@Nullable Object value, Class target) { return read(value, TypeInformation.of(target)); } @@ -78,7 +78,7 @@ default T read(@Nullable Object value, Class target) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be read as an instance of {@link Class type}. */ - T read(@Nullable Object value, TypeInformation target); + @Nullable T read(@Nullable Object value, TypeInformation target); /** * Write the value as an instance of the {@link PersistentProperty#getTypeInformation() property type}. @@ -90,8 +90,7 @@ default T read(@Nullable Object value, Class target) { * @see PersistentProperty#getTypeInformation() * @see #write(Object, TypeInformation) */ - @Nullable - default T write(@Nullable Object value) { + default @Nullable T write(@Nullable Object value) { return (T) write(value, getProperty().getTypeInformation()); } @@ -103,8 +102,7 @@ default T write(@Nullable Object value) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. */ - @Nullable - default T write(@Nullable Object value, Class target) { + default @Nullable T write(@Nullable Object value, Class target) { return write(value, TypeInformation.of(target)); } @@ -116,8 +114,7 @@ default T write(@Nullable Object value, Class target) { * @return can be {@literal null}. * @throws IllegalStateException if value cannot be written as an instance of {@link Class type}. */ - @Nullable - T write(@Nullable Object value, TypeInformation target); + @Nullable T write(@Nullable Object value, TypeInformation target); /** * Lookup the value for a given path within the current context. @@ -128,7 +125,7 @@ default T write(@Nullable Object value, Class target) { @Nullable Object lookupValue(String path); - EvaluationContext getEvaluationContext(Object source); + EvaluationContext getEvaluationContext(@Nullable Object source); /** * The field name and field query operator diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java index f3906d89dd..90a3ab8720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/package-info.java @@ -2,5 +2,5 @@ * Infrastructure for explicit * encryption mechanism of Client-Side Field Level Encryption. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.encryption; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java index 2372700aec..74f36e3198 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonGeometryCollection.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java index bc74a56df3..cfae6761a8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java @@ -19,9 +19,8 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; - +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import com.fasterxml.jackson.core.JsonParser; import com.fasterxml.jackson.core.Version; @@ -139,9 +138,8 @@ private static void registerDeserializersIn(SimpleModule module) { */ private static abstract class GeoJsonDeserializer> extends JsonDeserializer { - @Nullable @Override - public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException { + public @Nullable T deserialize(JsonParser jp, @Nullable DeserializationContext ctxt) throws IOException { JsonNode node = jp.readValueAsTree(); JsonNode coordinates = node.get("coordinates"); @@ -158,8 +156,7 @@ public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext c * @param coordinates * @return */ - @Nullable - protected abstract T doDeserialize(ArrayNode coordinates); + protected abstract @Nullable T doDeserialize(ArrayNode coordinates); /** * Get the {@link GeoJsonPoint} representation of given {@link ArrayNode} assuming {@code node.[0]} represents @@ -168,8 +165,7 @@ public T deserialize(@Nullable JsonParser jp, @Nullable DeserializationContext c * @param node can be {@literal null}. * @return {@literal null} when given a {@code null} value. */ - @Nullable - protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { + protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { if (node == null) { return null; @@ -185,8 +181,7 @@ protected GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { * @param node can be {@literal null}. * @return {@literal null} when given a {@code null} value. */ - @Nullable - protected Point toPoint(@Nullable ArrayNode node) { + protected @Nullable Point toPoint(@Nullable ArrayNode node) { if (node == null) { return null; @@ -236,9 +231,8 @@ protected GeoJsonLineString toLineString(ArrayNode node) { */ private static class GeoJsonPointDeserializer extends GeoJsonDeserializer { - @Nullable @Override - protected GeoJsonPoint doDeserialize(ArrayNode coordinates) { + protected @Nullable GeoJsonPoint doDeserialize(ArrayNode coordinates) { return toGeoJsonPoint(coordinates); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java index 8dafe9ea00..833a1dd9f6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiLineString.java @@ -19,8 +19,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java index bcb4c3e79e..e30ed5d6ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPoint.java @@ -20,8 +20,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java index 12b9de9da4..a7e6306b49 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonMultiPolygon.java @@ -19,7 +19,7 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java index 166a10df08..990be290cd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonPolygon.java @@ -21,9 +21,10 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -78,6 +79,7 @@ public GeoJsonPolygon(List points) { * @return new {@link GeoJsonPolygon}. * @since 1.10 */ + @Contract("_, _, _, _, _ -> new") public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Point fourth, Point... others) { return withInnerRing(asList(first, second, third, fourth, others)); } @@ -88,6 +90,7 @@ public GeoJsonPolygon withInnerRing(Point first, Point second, Point third, Poin * @param points must not be {@literal null}. * @return new {@link GeoJsonPolygon}. */ + @Contract("_ -> new") public GeoJsonPolygon withInnerRing(List points) { return withInnerRing(new GeoJsonLineString(points)); } @@ -99,6 +102,7 @@ public GeoJsonPolygon withInnerRing(List points) { * @return new {@link GeoJsonPolygon}. * @since 1.10 */ + @Contract("_ -> new") public GeoJsonPolygon withInnerRing(GeoJsonLineString lineString) { Assert.notNull(lineString, "LineString must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java index a482c136e7..47be645869 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java @@ -18,12 +18,12 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.PersistenceCreator; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.geo.Shape; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java index 6cc77f832b..e5adfb26f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB geo-spatial queries. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.geo; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java index 225bb41ac8..b4b7b8430a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/DefaultSearchIndexOperations.java @@ -20,12 +20,12 @@ import org.bson.BsonString; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.client.model.SearchIndexModel; @@ -40,7 +40,7 @@ public class DefaultSearchIndexOperations implements SearchIndexOperations { private final MongoOperations mongoOperations; private final String collectionName; - private final TypeInformation entityTypeInformation; + private final @Nullable TypeInformation entityTypeInformation; public DefaultSearchIndexOperations(MongoOperations mongoOperations, Class type) { this(mongoOperations, mongoOperations.getCollectionName(type), type); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index 1c5a8ca6bc..b88acb06a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -20,6 +20,7 @@ import org.bson.Document; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -60,6 +61,7 @@ public GeospatialIndex(String field) { * @param name must not be {@literal null} or empty. * @return this. */ + @Contract("_ -> this") public GeospatialIndex named(String name) { this.name = name; @@ -70,6 +72,7 @@ public GeospatialIndex named(String name) { * @param min * @return this. */ + @Contract("_ -> this") public GeospatialIndex withMin(int min) { this.min = min; return this; @@ -79,6 +82,7 @@ public GeospatialIndex withMin(int min) { * @param max * @return this. */ + @Contract("_ -> this") public GeospatialIndex withMax(int max) { this.max = max; return this; @@ -88,6 +92,7 @@ public GeospatialIndex withMax(int max) { * @param bits * @return this. */ + @Contract("_ -> this") public GeospatialIndex withBits(int bits) { this.bits = bits; return this; @@ -97,6 +102,7 @@ public GeospatialIndex withBits(int bits) { * @param type must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GeospatialIndex typed(GeoSpatialIndexType type) { Assert.notNull(type, "Type must not be null"); @@ -109,6 +115,7 @@ public GeospatialIndex typed(GeoSpatialIndexType type) { * @param fieldName * @return this. */ + @Contract("_ -> this") public GeospatialIndex withAdditionalField(String fieldName) { this.additionalField = fieldName; return this; @@ -123,6 +130,7 @@ public GeospatialIndex withAdditionalField(String fieldName) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public GeospatialIndex partial(@Nullable IndexFilter filter) { this.filter = Optional.ofNullable(filter); @@ -139,6 +147,7 @@ public GeospatialIndex partial(@Nullable IndexFilter filter) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public GeospatialIndex collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java index 95f4226e28..91195a40f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/Index.java @@ -23,10 +23,11 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.mongodb.core.index.IndexOptions.Unique; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -52,11 +53,13 @@ public Index(String key, Direction direction) { fieldSpec.put(key, direction); } + @Contract("_, _ -> this") public Index on(String key, Direction direction) { fieldSpec.put(key, direction); return this; } + @Contract("_ -> this") public Index named(String name) { this.name = name; return this; @@ -69,6 +72,7 @@ public Index named(String name) { * @see https://docs.mongodb.org/manual/core/index-unique/ */ + @Contract("-> this") public Index unique() { this.options.setUnique(Unique.YES); @@ -82,6 +86,7 @@ public Index unique() { * @see https://docs.mongodb.org/manual/core/index-sparse/ */ + @Contract("-> this") public Index sparse() { this.sparse = true; return this; @@ -92,7 +97,7 @@ public Index sparse() { * * @return this. * @since 1.5 - */ + */@Contract("-> this") public Index background() { this.background = true; @@ -107,6 +112,7 @@ public Index background() { * "https://www.mongodb.com/docs/manual/core/index-hidden/">https://www.mongodb.com/docs/manual/core/index-hidden/ * @since 4.1 */ + @Contract("-> this") public Index hidden() { options.setHidden(true); @@ -120,6 +126,7 @@ public Index hidden() { * @return this. * @since 1.5 */ + @Contract("_ -> this") public Index expire(long value) { return expire(value, TimeUnit.SECONDS); } @@ -132,6 +139,7 @@ public Index expire(long value) { * @throws IllegalArgumentException if given {@literal timeout} is {@literal null}. * @since 2.2 */ + @Contract("_ -> this") public Index expire(Duration timeout) { Assert.notNull(timeout, "Timeout must not be null"); @@ -146,6 +154,7 @@ public Index expire(Duration timeout) { * @return this. * @since 1.5 */ + @Contract("_, _ -> this") public Index expire(long value, TimeUnit unit) { Assert.notNull(unit, "TimeUnit for expiration must not be null"); @@ -162,6 +171,7 @@ public Index expire(long value, TimeUnit unit) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public Index partial(@Nullable IndexFilter filter) { this.filter = Optional.ofNullable(filter); @@ -178,6 +188,7 @@ public Index partial(@Nullable IndexFilter filter) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Index collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java index a5cbf6c896..2e7268699c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexField.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.core.index; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Sort.Direction; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -139,8 +139,7 @@ public String getKey() { * * @return the direction */ - @Nullable - public Direction getDirection() { + public @Nullable Direction getDirection() { return direction; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java index de7153bfb5..e9817746c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexInfo.java @@ -27,11 +27,12 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; /** * Index information for a MongoDB index. @@ -89,7 +90,7 @@ public IndexInfo(List indexFields, String name, boolean unique, bool */ public static IndexInfo indexInfoOf(Document sourceDocument) { - Document keyDbObject = (Document) sourceDocument.get("key"); + Document keyDbObject = sourceDocument.get("key", new Document()); int numberOfElements = keyDbObject.keySet().size(); List indexFields = new ArrayList(numberOfElements); @@ -105,9 +106,10 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { } else if ("text".equals(value)) { Document weights = (Document) sourceDocument.get("weights"); - - for (String fieldName : weights.keySet()) { - indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + if(weights != null) { + for (String fieldName : weights.keySet()) { + indexFields.add(IndexField.text(fieldName, Float.valueOf(weights.get(fieldName).toString()))); + } } } else { @@ -129,7 +131,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { } } - String name = sourceDocument.get("name").toString(); + String name = ObjectUtils.nullSafeToString(sourceDocument.get("name")); boolean unique = sourceDocument.get("unique", false); boolean sparse = sourceDocument.get("sparse", false); @@ -161,8 +163,7 @@ public static IndexInfo indexInfoOf(Document sourceDocument) { * @return the {@link String} representation of the partial filter {@link Document}. * @since 2.1.11 */ - @Nullable - private static String extractPartialFilterString(Document sourceDocument) { + private static @Nullable String extractPartialFilterString(Document sourceDocument) { if (!sourceDocument.containsKey("partialFilterExpression")) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java index ca3d951c94..aec1ba817d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOperationsProvider.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.index; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Provider interface to obtain {@link IndexOperations} by MongoDB collection name or entity type. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java index 887542cb0c..a390d1eb3e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexOptions.java @@ -18,7 +18,7 @@ import java.time.Duration; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Changeable properties of an index. Can be used for index creation and modification. @@ -28,14 +28,11 @@ */ public class IndexOptions { - @Nullable - private Duration expire; + private @Nullable Duration expire; - @Nullable - private Boolean hidden; + private @Nullable Boolean hidden; - @Nullable - private Unique unique; + private @Nullable Unique unique; public enum Unique { @@ -108,8 +105,7 @@ public void setExpire(Duration expire) { /** * @return {@literal true} if hidden, {@literal null} if not set. */ - @Nullable - public Boolean isHidden() { + public @Nullable Boolean isHidden() { return hidden; } @@ -123,8 +119,7 @@ public void setHidden(boolean hidden) { /** * @return the unique property value, {@literal null} if not set. */ - @Nullable - public Unique getUnique() { + public @Nullable Unique getUnique() { return unique; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java index 362247725f..3bb3fdbd0f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/IndexPredicate.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.index; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Jon Brisbin @@ -26,8 +26,7 @@ public abstract class IndexPredicate { private IndexDirection direction = IndexDirection.ASCENDING; private boolean unique = false; - @Nullable - public String getName() { + public @Nullable String getName() { return name; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java index e20b0704cc..f1550d1501 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreator.java @@ -21,7 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationListener; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.mapping.PersistentEntity; @@ -33,7 +33,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.util.MongoDbErrorCodes; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -183,8 +182,7 @@ public boolean isIndexCreatorFor(MappingContext context) { return this.mappingContext.equals(context); } - @Nullable - private IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) { + private @Nullable IndexInfo fetchIndexInformation(@Nullable IndexDefinitionHolder indexDefinition) { if (indexDefinition == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 37008ad76e..3f63009ae2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -32,6 +32,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; @@ -59,7 +60,6 @@ import org.springframework.data.util.TypeInformation; import org.springframework.expression.EvaluationContext; 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.ObjectUtils; @@ -163,7 +163,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo } if (persistentProperty.isEntity()) { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty), persistentProperty.isUnwrapped() ? "" : persistentProperty.getFieldName(), Path.of(persistentProperty), root.getCollection(), guard)); } @@ -230,7 +230,7 @@ private void guardAndPotentiallyAddIndexForProperty(MongoPersistentProperty pers if (persistentProperty.isEntity()) { try { - indexes.addAll(resolveIndexForEntity(mappingContext.getPersistentEntity(persistentProperty), + indexes.addAll(resolveIndexForEntity(mappingContext.getRequiredPersistentEntity(persistentProperty), propertyDotPath.toString(), propertyPath, collection, guard)); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); @@ -383,7 +383,7 @@ public void doWithPersistentProperty(MongoPersistentProperty persistentProperty) try { appendTextIndexInformation(propertyDotPath, propertyPath, indexDefinitionBuilder, - mappingContext.getPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard); + mappingContext.getRequiredPersistentEntity(persistentProperty.getActualType()), optionsForNestedType, guard); } catch (CyclicPropertyReferenceException e) { LOGGER.info(e.getMessage()); } catch (InvalidDataAccessApiUsageException e) { @@ -518,8 +518,7 @@ private org.bson.Document resolveCompoundIndexKeyFromStringDefinition(String dot * @param persistentProperty * @return */ - @Nullable - protected IndexDefinitionHolder createIndexDefinition(String dotPath, String collection, + protected @Nullable IndexDefinitionHolder createIndexDefinition(String dotPath, String collection, MongoPersistentProperty persistentProperty) { Indexed index = persistentProperty.findAnnotation(Indexed.class); @@ -575,7 +574,7 @@ protected IndexDefinitionHolder createIndexDefinition(String dotPath, String col return new IndexDefinitionHolder(dotPath, indexDefinition, collection); } - private PartialIndexFilter evaluatePartialFilter(String filterExpression, PersistentEntity entity) { + private PartialIndexFilter evaluatePartialFilter(String filterExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(filterExpression, () -> getEvaluationContextForProperty(entity)); @@ -586,7 +585,7 @@ private PartialIndexFilter evaluatePartialFilter(String filterExpression, Persis return PartialIndexFilter.of(BsonUtils.parse(filterExpression, null)); } - private org.bson.Document evaluateWildcardProjection(String projectionExpression, PersistentEntity entity) { + private org.bson.Document evaluateWildcardProjection(String projectionExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(projectionExpression, () -> getEvaluationContextForProperty(entity)); @@ -597,7 +596,7 @@ private org.bson.Document evaluateWildcardProjection(String projectionExpression return BsonUtils.parse(projectionExpression, null); } - private Collation evaluateCollation(String collationExpression, PersistentEntity entity) { + private Collation evaluateCollation(String collationExpression, @Nullable PersistentEntity entity) { Object result = ExpressionUtils.evaluate(collationExpression, () -> getEvaluationContextForProperty(entity)); if (result instanceof org.bson.Document document) { @@ -690,8 +689,7 @@ public void setEvaluationContextProvider(EvaluationContextProvider evaluationCon * @param persistentProperty * @return */ - @Nullable - protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection, + protected @Nullable IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, String collection, MongoPersistentProperty persistentProperty) { GeoSpatialIndexed index = persistentProperty.findAnnotation(GeoSpatialIndexed.class); @@ -793,8 +791,7 @@ private static Duration computeIndexTimeout(String timeoutValue, Supplier entity) { + private @Nullable Collation resolveCollation(Annotation annotation, @Nullable PersistentEntity entity) { return MergedAnnotation.from(annotation).getValue("collation", String.class).filter(StringUtils::hasText) .map(it -> evaluateCollation(it, entity)).orElseGet(() -> { @@ -1119,8 +1116,7 @@ public IncludeStrategy getStrategy() { return strategy; } - @Nullable - public TextIndexedFieldSpec getParentFieldSpec() { + public @Nullable TextIndexedFieldSpec getParentFieldSpec() { return parentFieldSpec; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java index a87b15de45..0b473388fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/TextIndexDefinition.java @@ -20,9 +20,10 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -235,6 +236,7 @@ public TextIndexDefinitionBuilder() { * @param name * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder named(String name) { this.instance.name = name; return this; @@ -246,6 +248,7 @@ public TextIndexDefinitionBuilder named(String name) { * * @return */ + @Contract("-> this") public TextIndexDefinitionBuilder onAllFields() { if (!instance.fieldSpecs.isEmpty()) { @@ -262,6 +265,7 @@ public TextIndexDefinitionBuilder onAllFields() { * @param fieldnames * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder onFields(String... fieldnames) { for (String fieldname : fieldnames) { @@ -276,6 +280,7 @@ public TextIndexDefinitionBuilder onFields(String... fieldnames) { * @param fieldname * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder onField(String fieldname) { return onField(fieldname, 1F); } @@ -286,6 +291,7 @@ public TextIndexDefinitionBuilder onField(String fieldname) { * @param fieldname * @return */ + @Contract("_, _ -> this") public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { if (this.instance.fieldSpecs.contains(ALL_FIELDS)) { @@ -305,6 +311,7 @@ public TextIndexDefinitionBuilder onField(String fieldname, Float weight) { * @see https://docs.mongodb.org/manual/tutorial/specify-language-for-text-index/#specify-default-language-text-index */ + @Contract("_ -> this") public TextIndexDefinitionBuilder withDefaultLanguage(String language) { this.instance.defaultLanguage = language; @@ -317,6 +324,7 @@ public TextIndexDefinitionBuilder withDefaultLanguage(String language) { * @param fieldname * @return */ + @Contract("_ -> this") public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { if (StringUtils.hasText(this.instance.languageOverride)) { @@ -338,6 +346,7 @@ public TextIndexDefinitionBuilder withLanguageOverride(String fieldname) { * "https://docs.mongodb.com/manual/core/index-partial/">https://docs.mongodb.com/manual/core/index-partial/ * @since 1.10 */ + @Contract("_ -> this") public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) { this.instance.filter = filter; @@ -349,12 +358,14 @@ public TextIndexDefinitionBuilder partial(@Nullable IndexFilter filter) { * * @since 2.2 */ + @Contract("-> this") public TextIndexDefinitionBuilder withSimpleCollation() { this.instance.collation = Collation.simple(); return this; } + @Contract("-> new") public TextIndexDefinition build() { return this.instance; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java index b46dbf4d0c..d56801c528 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java @@ -28,6 +28,7 @@ import org.springframework.lang.Contract; import org.springframework.lang.Nullable; import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -149,7 +150,8 @@ static VectorIndex of(Document document) { for (Object entry : definition.get("fields", List.class)) { if (entry instanceof Document field) { - if (field.get("type").equals("vector")) { + Object fieldType = field.get("type"); + if (ObjectUtils.nullSafeEquals(fieldType, "vector")) { index.addField(new VectorIndexField(field.getString("path"), "vector", field.getInteger("numDimensions"), field.getString("similarity"), field.getString("quantization"))); } else { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java index dcd2b7c022..ff0a92ada1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/WildcardIndex.java @@ -21,8 +21,9 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -76,6 +77,7 @@ public WildcardIndex(@Nullable String path) { * * @return this. */ + @Contract("-> this") public WildcardIndex includeId() { wildcardProjection.put(FieldName.ID.name(), 1); @@ -89,6 +91,7 @@ public WildcardIndex includeId() { * @return this. */ @Override + @Contract("_ -> this") public WildcardIndex named(String name) { super.named(name); @@ -101,6 +104,7 @@ public WildcardIndex named(String name) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("-> fail") public Index unique() { throw new UnsupportedOperationException("Wildcard Index does not support 'unique'"); } @@ -111,6 +115,7 @@ public Index unique() { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("-> fail") public Index expire(long seconds) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -121,6 +126,7 @@ public Index expire(long seconds) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("_, _ -> fail") public Index expire(long value, TimeUnit timeUnit) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -131,6 +137,7 @@ public Index expire(long value, TimeUnit timeUnit) { * @throws UnsupportedOperationException not supported for wildcard indexes. */ @Override + @Contract("_ -> fail") public Index expire(Duration duration) { throw new UnsupportedOperationException("Wildcard Index does not support 'ttl'"); } @@ -142,6 +149,7 @@ public Index expire(Duration duration) { * @param paths must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjectionInclude(String... paths) { for (String path : paths) { @@ -157,6 +165,7 @@ public WildcardIndex wildcardProjectionInclude(String... paths) { * @param paths must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjectionExclude(String... paths) { for (String path : paths) { @@ -172,6 +181,7 @@ public WildcardIndex wildcardProjectionExclude(String... paths) { * @param includeExclude must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public WildcardIndex wildcardProjection(Map includeExclude) { wildcardProjection.putAll(includeExclude); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java index c49f501d8d..8524ee62f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB document indexing. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.index; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java index 3d68dbaac2..e865009319 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentEntity.java @@ -25,6 +25,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; @@ -35,6 +36,7 @@ import org.springframework.data.mapping.PropertyHandler; import org.springframework.data.mapping.model.BasicPersistentEntity; import org.springframework.data.mongodb.MongoCollectionUtils; +import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.encryption.EncryptionUtils; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.data.util.Lazy; @@ -42,7 +44,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; 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.ObjectUtils; @@ -139,9 +140,8 @@ public String getLanguage() { return this.language; } - @Nullable @Override - public MongoPersistentProperty getTextScoreProperty() { + public @Nullable MongoPersistentProperty getTextScoreProperty() { return getPersistentProperty(TextScore.class); } @@ -151,7 +151,7 @@ public boolean hasTextScoreProperty() { } @Override - public org.springframework.data.mongodb.core.query.Collation getCollation() { + public @Nullable Collation getCollation() { Object collationValue = collationExpression != null ? collationExpression.evaluate(getValueEvaluationContext(null)) @@ -189,22 +189,22 @@ public void verify() { } @Override - public EvaluationContext getEvaluationContext(Object rootObject) { + public EvaluationContext getEvaluationContext(@Nullable Object rootObject) { return super.getEvaluationContext(rootObject); } @Override - public EvaluationContext getEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + public EvaluationContext getEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) { return super.getEvaluationContext(rootObject, dependencies); } @Override - public ValueEvaluationContext getValueEvaluationContext(Object rootObject) { + public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject) { return super.getValueEvaluationContext(rootObject); } @Override - public ValueEvaluationContext getValueEvaluationContext(Object rootObject, ExpressionDependencies dependencies) { + public ValueEvaluationContext getValueEvaluationContext(@Nullable Object rootObject, ExpressionDependencies dependencies) { return super.getValueEvaluationContext(rootObject, dependencies); } @@ -243,7 +243,11 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste return -1; } - return o1.getFieldOrder() - o2.getFieldOrder(); + if(o1 != null && o2 != null) { + return o1.getFieldOrder() - o2.getFieldOrder(); + } + + return o1 != null ? o1.getFieldOrder() : -1; } } @@ -257,7 +261,7 @@ public int compare(@Nullable MongoPersistentProperty o1, @Nullable MongoPersiste * @return can be {@literal null}. */ @Override - protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) { + protected @Nullable MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNull(MongoPersistentProperty property) { Assert.notNull(property, "MongoPersistentProperty must not be null"); @@ -268,7 +272,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul MongoPersistentProperty currentIdProperty = getIdProperty(); boolean currentIdPropertyIsSet = currentIdProperty != null; - @SuppressWarnings("null") + @SuppressWarnings("NullAway") boolean currentIdPropertyIsExplicit = currentIdPropertyIsSet && currentIdProperty.isExplicitIdProperty(); boolean newIdPropertyIsExplicit = property.isExplicitIdProperty(); @@ -277,7 +281,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul } - @SuppressWarnings("null") + @SuppressWarnings("NullAway") Field currentIdPropertyField = currentIdProperty.getField(); if (newIdPropertyIsExplicit && currentIdPropertyIsExplicit) { @@ -308,8 +312,7 @@ protected MongoPersistentProperty returnPropertyIfBetterIdPropertyCandidateOrNul * @param potentialExpression can be {@literal null} * @return can be {@literal null}. */ - @Nullable - private static ValueExpression detectExpression(@Nullable String potentialExpression) { + private static @Nullable ValueExpression detectExpression(@Nullable String potentialExpression) { if (!StringUtils.hasText(potentialExpression)) { return null; @@ -352,7 +355,7 @@ private void assertUniqueness(MongoPersistentProperty property) { } @Override - public Collection getEncryptionKeyIds() { + public @Nullable Collection getEncryptionKeyIds() { Encrypted encrypted = findAnnotation(Encrypted.class); if (encrypted == null) { @@ -405,6 +408,7 @@ private static void potentiallyAssertTextScoreType(MongoPersistentProperty persi } } + @SuppressWarnings("NullAway") private static void potentiallyAssertDBRefTargetType(MongoPersistentProperty persistentProperty) { if (persistentProperty.isDbReference() && persistentProperty.getDBRef().lazy()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java index 5c3b4e6532..027a570fa3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/BasicMongoPersistentProperty.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - +import org.jspecify.annotations.Nullable; import org.springframework.core.env.StandardEnvironment; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.mapping.Association; @@ -39,7 +39,6 @@ import org.springframework.data.util.Lazy; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -153,8 +152,7 @@ public boolean hasExplicitFieldName() { return StringUtils.hasText(getAnnotatedFieldName()); } - @Nullable - private String getAnnotatedFieldName() { + private @Nullable String getAnnotatedFieldName() { org.springframework.data.mongodb.core.mapping.Field annotation = findAnnotation( org.springframework.data.mongodb.core.mapping.Field.class); @@ -197,9 +195,8 @@ public DBRef getDBRef() { return findAnnotation(DBRef.class); } - @Nullable @Override - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return findAnnotation(DocumentReference.class); } @@ -258,6 +255,7 @@ public MongoField getMongoField() { } @Override + @SuppressWarnings("NullAway") public Collection getEncryptionKeyIds() { Encrypted encrypted = findAnnotation(Encrypted.class); @@ -282,6 +280,7 @@ public Collection getEncryptionKeyIds() { return target; } + @SuppressWarnings("NullAway") protected MongoField doGetMongoField() { MongoFieldBuilder builder = MongoField.builder(); @@ -295,6 +294,7 @@ protected MongoField doGetMongoField() { return builder.build(); } + @SuppressWarnings("NullAway") private String doGetFieldName() { if (isIdProperty()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java index 105c38b288..eb8d08db64 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/CachingMongoPersistentProperty.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.core.mapping; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.FieldNamingStrategy; import org.springframework.data.mapping.model.Property; import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; /** * {@link MongoPersistentProperty} caching access to {@link #isIdProperty()} and {@link #getFieldName()}. @@ -121,12 +121,12 @@ public boolean isDbReference() { } @Override - public DBRef getDBRef() { + public @Nullable DBRef getDBRef() { return dbref.getNullable(); } @Override - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return documentReference.getNullable(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java index 6f0e1ae4c3..881d741ee4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoField.java @@ -15,7 +15,9 @@ */ package org.springframework.data.mongodb.core.mapping; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.FieldName.Type; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -139,7 +141,7 @@ public String toString() { */ public static class MongoFieldBuilder { - private String name; + private @Nullable String name; private Type nameType = Type.PATH; private FieldType type = FieldType.IMPLICIT; private int order = Integer.MAX_VALUE; @@ -150,6 +152,7 @@ public static class MongoFieldBuilder { * @param fieldType * @return */ + @Contract("_ -> this") public MongoFieldBuilder fieldType(FieldType fieldType) { this.type = fieldType; @@ -163,6 +166,7 @@ public MongoFieldBuilder fieldType(FieldType fieldType) { * @param fieldName * @return */ + @Contract("_ -> this") public MongoFieldBuilder name(String fieldName) { Assert.hasText(fieldName, "Field name must not be empty"); @@ -178,6 +182,7 @@ public MongoFieldBuilder name(String fieldName) { * @param path * @return */ + @Contract("_ -> this") public MongoFieldBuilder path(String path) { Assert.hasText(path, "Field path (name) must not be empty"); @@ -193,6 +198,7 @@ public MongoFieldBuilder path(String path) { * @param order * @return */ + @Contract("_ -> this") public MongoFieldBuilder order(int order) { this.order = order; @@ -204,7 +210,10 @@ public MongoFieldBuilder order(int order) { * * @return a new {@link MongoField}. */ + @Contract("-> new") public MongoField build() { + + Assert.notNull(name, "Name of Field must not be null"); return new MongoField(new FieldName(name, nameType), type, order); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java index 76c0269861..4540493124 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoMappingContext.java @@ -17,6 +17,7 @@ import java.util.AbstractMap; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -28,7 +29,6 @@ import org.springframework.data.mapping.model.SimpleTypeHolder; import org.springframework.data.util.NullableWrapperConverters; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Default implementation of a {@link MappingContext} for MongoDB using {@link BasicMongoPersistentEntity} and @@ -46,8 +46,7 @@ public class MongoMappingContext extends AbstractMappingContext getPersistentEntity(MongoPersistentProperty persistentProperty) { + public @Nullable MongoPersistentEntity getPersistentEntity(MongoPersistentProperty persistentProperty) { MongoPersistentEntity entity = super.getPersistentEntity(persistentProperty); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java index e02bd00c8d..f1d67e4ae8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoPersistentEntity.java @@ -17,9 +17,10 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.model.MutablePersistentEntity; -import org.springframework.lang.Nullable; +import org.springframework.data.mongodb.core.query.Collation; /** * MongoDB specific {@link PersistentEntity} abstraction. @@ -68,8 +69,7 @@ public interface MongoPersistentEntity extends MutablePersistentEntity property) { } @Override - @Nullable - public MongoPersistentProperty getIdProperty() { + public @Nullable MongoPersistentProperty getIdProperty() { return delegate.getIdProperty(); } @@ -131,8 +129,7 @@ public MongoPersistentProperty getRequiredIdProperty() { } @Override - @Nullable - public MongoPersistentProperty getVersionProperty() { + public @Nullable MongoPersistentProperty getVersionProperty() { return delegate.getVersionProperty(); } @@ -142,8 +139,7 @@ public MongoPersistentProperty getRequiredVersionProperty() { } @Override - @Nullable - public MongoPersistentProperty getPersistentProperty(String name) { + public @Nullable MongoPersistentProperty getPersistentProperty(String name) { return wrap(delegate.getPersistentProperty(name)); } @@ -159,8 +155,7 @@ public MongoPersistentProperty getRequiredPersistentProperty(String name) { } @Override - @Nullable - public MongoPersistentProperty getPersistentProperty(Class annotationType) { + public @Nullable MongoPersistentProperty getPersistentProperty(Class annotationType) { return wrap(delegate.getPersistentProperty(annotationType)); } @@ -226,8 +221,7 @@ public void doWithAssociations(SimpleAssociationHandler handler) { } @Override - @Nullable - public A findAnnotation(Class annotationType) { + public @Nullable A findAnnotation(Class annotationType) { return delegate.findAnnotation(annotationType); } @@ -289,7 +283,9 @@ public Spliterator spliterator() { return delegate.spliterator(); } - private MongoPersistentProperty wrap(MongoPersistentProperty source) { + @Contract("null -> null; !null -> !null") + private @Nullable MongoPersistentProperty wrap(@Nullable MongoPersistentProperty source) { + if (source == null) { return source; } @@ -332,7 +328,7 @@ public boolean isUnwrapped() { } @Override - public Collection getEncryptionKeyIds() { + public @Nullable Collection getEncryptionKeyIds() { return delegate.getEncryptionKeyIds(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java index 1d4877478f..ac7f24a555 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/UnwrappedMongoPersistentProperty.java @@ -20,11 +20,11 @@ import java.lang.reflect.Method; import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.Association; import org.springframework.data.mapping.PersistentEntity; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -47,6 +47,7 @@ public UnwrappedMongoPersistentProperty(MongoPersistentProperty delegate, Unwrap } @Override + @SuppressWarnings("NullAway") public String getFieldName() { if (!context.getProperty().isUnwrapped()) { @@ -57,6 +58,7 @@ public String getFieldName() { } @Override + @SuppressWarnings("NullAway") public boolean hasExplicitFieldName() { return delegate.hasExplicitFieldName() || !ObjectUtils.isEmpty(context.getProperty().findAnnotation(Unwrapped.class).prefix()); @@ -108,14 +110,12 @@ public boolean isTextScoreProperty() { } @Override - @Nullable - public DBRef getDBRef() { + public @Nullable DBRef getDBRef() { return delegate.getDBRef(); } @Override - @Nullable - public DocumentReference getDocumentReference() { + public @Nullable DocumentReference getDocumentReference() { return delegate.getDocumentReference(); } @@ -145,6 +145,7 @@ public Class getType() { } @Override + @SuppressWarnings("NullAway") public MongoField getMongoField() { if (!context.getProperty().isUnwrapped()) { @@ -165,8 +166,7 @@ public Iterable> getPersistentEntityTypeInformation } @Override - @Nullable - public Method getGetter() { + public @Nullable Method getGetter() { return delegate.getGetter(); } @@ -176,8 +176,7 @@ public Method getRequiredGetter() { } @Override - @Nullable - public Method getSetter() { + public @Nullable Method getSetter() { return delegate.getSetter(); } @@ -187,8 +186,7 @@ public Method getRequiredSetter() { } @Override - @Nullable - public Method getWither() { + public @Nullable Method getWither() { return delegate.getWither(); } @@ -198,8 +196,7 @@ public Method getRequiredWither() { } @Override - @Nullable - public Field getField() { + public @Nullable Field getField() { return delegate.getField(); } @@ -209,14 +206,12 @@ public Field getRequiredField() { } @Override - @Nullable - public String getSpelExpression() { + public @Nullable String getSpelExpression() { return delegate.getSpelExpression(); } @Override - @Nullable - public Association getAssociation() { + public @Nullable Association getAssociation() { return delegate.getAssociation(); } @@ -291,8 +286,7 @@ public Collection getEncryptionKeyIds() { } @Override - @Nullable - public Class getComponentType() { + public @Nullable Class getComponentType() { return delegate.getComponentType(); } @@ -302,8 +296,7 @@ public Class getRawType() { } @Override - @Nullable - public Class getMapValueType() { + public @Nullable Class getMapValueType() { return delegate.getMapValueType(); } @@ -313,8 +306,7 @@ public Class getActualType() { } @Override - @Nullable - public A findAnnotation(Class annotationType) { + public @Nullable A findAnnotation(Class annotationType) { return delegate.findAnnotation(annotationType); } @@ -324,8 +316,7 @@ public A getRequiredAnnotation(Class annotationType) t } @Override - @Nullable - public A findPropertyOrOwnerAnnotation(Class annotationType) { + public @Nullable A findPropertyOrOwnerAnnotation(Class annotationType) { return delegate.findPropertyOrOwnerAnnotation(annotationType); } @@ -340,13 +331,12 @@ public boolean hasActualTypeAnnotation(Class annotationTyp } @Override - @Nullable - public Class getAssociationTargetType() { + public @Nullable Class getAssociationTargetType() { return delegate.getAssociationTargetType(); } @Override - public TypeInformation getAssociationTargetTypeInformation() { + public @Nullable TypeInformation getAssociationTargetTypeInformation() { return delegate.getAssociationTargetTypeInformation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java index 73f4890dec..a8e2c93773 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AbstractDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Base class for delete events. @@ -49,8 +49,7 @@ public AbstractDeleteEvent(Document document, @Nullable Class type, String co * * @return can be {@literal null}. */ - @Nullable - public Class getType() { + public @Nullable Class getType() { return type; } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java index 55ccaa5f3f..10f4cdbbb7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/AfterDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Event being thrown after a single or a set of documents has/have been deleted. The {@link Document} held in the event diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java index 49d509fb43..c826cadb4e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/BeforeDeleteEvent.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.mapping.event; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Event being thrown before a document is deleted. The {@link Document} held in the event will represent the query diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java index eec9a3edf1..bec1986720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/MongoMappingEvent.java @@ -18,8 +18,8 @@ import java.util.function.Function; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEvent; -import org.springframework.lang.Nullable; /** * Base {@link ApplicationEvent} triggered by Spring Data MongoDB. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java index 0cc9d071a3..71ed503b20 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/event/package-info.java @@ -1,6 +1,6 @@ /** * Mapping event callback infrastructure for the MongoDB document-to-object mapping subsystem. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapping.event; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java index 0a513f1a18..f5c917d7d7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/package-info.java @@ -1,6 +1,6 @@ /** * Infrastructure for the MongoDB document-to-object mapping subsystem. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapping; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java index 32a9ed5118..ed9c148a1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceCounts.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.mapreduce; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Value object to encapsulate results of a map-reduce count. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java index e9ee146be6..2b8c9d1eb3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceOptions.java @@ -20,10 +20,11 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; import com.mongodb.client.model.MapReduceAction; +import org.springframework.lang.Contract; /** * @author Mark Pollack @@ -64,6 +65,7 @@ public static MapReduceOptions options() { * @param limit Limit the number of objects to process * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions limit(int limit) { this.limit = limit; @@ -77,6 +79,7 @@ public MapReduceOptions limit(int limit) { * @param collectionName The name of the collection where the results of the map-reduce operation will be stored. * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions outputCollection(String collectionName) { this.outputCollection = collectionName; @@ -90,6 +93,7 @@ public MapReduceOptions outputCollection(String collectionName) { * @param outputDatabase The name of the database where the results of the map-reduce operation will be stored. * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions outputDatabase(@Nullable String outputDatabase) { this.outputDatabase = Optional.ofNullable(outputDatabase); @@ -104,6 +108,7 @@ public MapReduceOptions outputDatabase(@Nullable String outputDatabase) { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionInline() { this.mapReduceAction = null; @@ -118,6 +123,7 @@ public MapReduceOptions actionInline() { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionMerge() { this.mapReduceAction = MapReduceAction.MERGE; @@ -132,6 +138,7 @@ public MapReduceOptions actionMerge() { * @return this. * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionReduce() { this.mapReduceAction = MapReduceAction.REDUCE; @@ -145,6 +152,7 @@ public MapReduceOptions actionReduce() { * @return MapReduceOptions so that methods can be chained in a fluent API style * @since 3.0 */ + @Contract("-> this") public MapReduceOptions actionReplace() { this.mapReduceAction = MapReduceAction.REPLACE; @@ -157,6 +165,7 @@ public MapReduceOptions actionReplace() { * @param finalizeFunction The finalize function. Can be a JSON string or a Spring Resource URL * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) { this.finalizeFunction = Optional.ofNullable(finalizeFunction); @@ -170,6 +179,7 @@ public MapReduceOptions finalizeFunction(@Nullable String finalizeFunction) { * @param scopeVariables variables that can be accessed from map, reduce, and finalize scripts * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions scopeVariables(Map scopeVariables) { this.scopeVariables = scopeVariables; @@ -183,6 +193,7 @@ public MapReduceOptions scopeVariables(Map scopeVariables) { * @param javaScriptMode if true, have the execution of map-reduce stay in JavaScript * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions javaScriptMode(boolean javaScriptMode) { this.jsMode = javaScriptMode; @@ -194,6 +205,7 @@ public MapReduceOptions javaScriptMode(boolean javaScriptMode) { * * @return MapReduceOptions so that methods can be chained in a fluent API style */ + @Contract("_ -> this") public MapReduceOptions verbose(boolean verbose) { this.verbose = verbose; @@ -207,6 +219,7 @@ public MapReduceOptions verbose(boolean verbose) { * @return * @since 2.0 */ + @Contract("_ -> this") public MapReduceOptions collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); @@ -217,13 +230,11 @@ public Optional getFinalizeFunction() { return this.finalizeFunction; } - @Nullable - public Boolean getJavaScriptMode() { + public @Nullable Boolean getJavaScriptMode() { return this.jsMode; } - @Nullable - public String getOutputCollection() { + public @Nullable String getOutputCollection() { return this.outputCollection; } @@ -261,8 +272,7 @@ public Optional getCollation() { * @return the mapped action or {@literal null} if the action maps to inline output. * @since 2.0.10 */ - @Nullable - public MapReduceAction getMapReduceAction() { + public @Nullable MapReduceAction getMapReduceAction() { return mapReduceAction; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java index 865a4e9438..1d4f644bd1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceResults.java @@ -19,7 +19,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -71,13 +71,11 @@ public MapReduceCounts getCounts() { return mapReduceCounts; } - @Nullable - public String getOutputCollection() { + public @Nullable String getOutputCollection() { return outputCollection; } - @Nullable - public Document getRawResults() { + public @Nullable Document getRawResults() { return rawResults; } @@ -147,7 +145,8 @@ private static String parseOutputCollection(Document rawResults) { return null; } - return resultField instanceof Document document ? document.get("collection").toString() + return resultField instanceof Document document && document.containsKey("collection") + ? document.get("collection").toString() : resultField.toString(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java index 28de7fe850..d99f6d9237 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/MapReduceTiming.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.mapreduce; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @deprecated since 3.4 in favor of {@link org.springframework.data.mongodb.core.aggregation}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java index 65522d8613..c5f5840e6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapreduce/package-info.java @@ -3,6 +3,6 @@ * @deprecated since MongoDB server version 5.0 */ @Deprecated -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.mapreduce; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java index fec7fa60ef..e1da0b33ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamRequest.java @@ -20,12 +20,13 @@ import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamOptions; import org.springframework.data.mongodb.core.ChangeStreamOptions.ChangeStreamOptionsBuilder; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.messaging.ChangeStreamRequest.ChangeStreamRequestOptions; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.model.changestream.ChangeStreamDocument; @@ -215,12 +216,12 @@ public ChangeStreamOptions getChangeStreamOptions() { } @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @Override - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return databaseName; } @@ -253,6 +254,7 @@ private ChangeStreamRequestBuilder() {} * @param databaseName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder database(String databaseName) { Assert.hasText(databaseName, "DatabaseName must not be null"); @@ -267,6 +269,7 @@ public ChangeStreamRequestBuilder database(String databaseName) { * @param collectionName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder collection(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null"); @@ -281,6 +284,7 @@ public ChangeStreamRequestBuilder collection(String collectionName) { * @param messageListener must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ChangeStreamRequestBuilder publishTo( MessageListener, ? super T> messageListener) { @@ -308,6 +312,7 @@ public ChangeStreamRequestBuilder publishTo( * @see ChangeStreamOptions#getFilter() * @see ChangeStreamOptionsBuilder#filter(Aggregation) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder filter(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); @@ -323,6 +328,7 @@ public ChangeStreamRequestBuilder filter(Aggregation aggregation) { * @return this. * @see ChangeStreamOptions#getFilter() */ + @Contract("_ -> this") public ChangeStreamRequestBuilder filter(Document... pipeline) { Assert.notNull(pipeline, "Aggregation pipeline must not be null"); @@ -340,6 +346,7 @@ public ChangeStreamRequestBuilder filter(Document... pipeline) { * @see ChangeStreamOptions#getCollation() * @see ChangeStreamOptionsBuilder#collation(Collation) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder collation(Collation collation) { Assert.notNull(collation, "Collation must not be null"); @@ -357,6 +364,7 @@ public ChangeStreamRequestBuilder collation(Collation collation) { * @see ChangeStreamOptions#getResumeToken() * @see ChangeStreamOptionsBuilder#resumeToken(org.bson.BsonValue) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeToken(BsonValue resumeToken) { Assert.notNull(resumeToken, "Resume token not be null"); @@ -373,6 +381,7 @@ public ChangeStreamRequestBuilder resumeToken(BsonValue resumeToken) { * @see ChangeStreamOptions#getResumeTimestamp() * @see ChangeStreamOptionsBuilder#resumeAt(java.time.Instant) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeAt(Instant clusterTime) { Assert.notNull(clusterTime, "ClusterTime must not be null"); @@ -388,6 +397,7 @@ public ChangeStreamRequestBuilder resumeAt(Instant clusterTime) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder resumeAfter(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -403,6 +413,7 @@ public ChangeStreamRequestBuilder resumeAfter(BsonValue resumeToken) { * @return this. * @since 2.2 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder startAfter(BsonValue resumeToken) { Assert.notNull(resumeToken, "ResumeToken must not be null"); @@ -418,6 +429,7 @@ public ChangeStreamRequestBuilder startAfter(BsonValue resumeToken) { * @see ChangeStreamOptions#getFullDocumentLookup() * @see ChangeStreamOptionsBuilder#fullDocumentLookup(FullDocument) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder fullDocumentLookup(FullDocument lookup) { Assert.notNull(lookup, "FullDocument not be null"); @@ -434,6 +446,7 @@ public ChangeStreamRequestBuilder fullDocumentLookup(FullDocument lookup) { * @see ChangeStreamOptions#getFullDocumentBeforeChangeLookup() * @see ChangeStreamOptionsBuilder#fullDocumentBeforeChangeLookup(FullDocumentBeforeChange) */ + @Contract("_ -> this") public ChangeStreamRequestBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) { Assert.notNull(lookup, "FullDocumentBeforeChange not be null"); @@ -448,6 +461,7 @@ public ChangeStreamRequestBuilder fullDocumentBeforeChangeLookup(FullDocument * @param timeout must not be {@literal null}. * @since 3.0 */ + @Contract("_ -> this") public ChangeStreamRequestBuilder maxAwaitTime(Duration timeout) { Assert.notNull(timeout, "timeout not be null"); @@ -459,6 +473,7 @@ public ChangeStreamRequestBuilder maxAwaitTime(Duration timeout) { /** * @return the build {@link ChangeStreamRequest}. */ + @Contract("-> new") public ChangeStreamRequest build() { Assert.notNull(listener, "MessageListener must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java index fc8372613b..cc4d3f0bdb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/ChangeStreamTask.java @@ -27,6 +27,7 @@ import org.bson.BsonTimestamp; import org.bson.BsonValue; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.ChangeStreamEvent; import org.springframework.data.mongodb.core.ChangeStreamOptions; import org.springframework.data.mongodb.core.MongoTemplate; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.messaging.Message.MessageProperties; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; import org.springframework.util.StringUtils; @@ -224,21 +224,18 @@ static class ChangeStreamEventMessage implements Message getRaw() { + public @Nullable ChangeStreamDocument getRaw() { return delegate.getRaw(); } - @Nullable @Override - public T getBody() { + public @Nullable T getBody() { return delegate.getBody(); } - @Nullable @Override - public T getBodyBeforeChange() { + public @Nullable T getBodyBeforeChange() { return delegate.getBodyBeforeChange(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java index 41b5fed4f5..662960284d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/CursorReadingTask.java @@ -21,12 +21,12 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.messaging.Message.MessageProperties; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; @@ -51,7 +51,7 @@ abstract class CursorReadingTask implements Task { private State state = State.CREATED; - private MongoCursor cursor; + private @Nullable MongoCursor cursor; /** * @param template must not be {@literal null}. @@ -109,6 +109,7 @@ public void run() { * is immediately {@link MongoCursor#close() closed} and a new {@link MongoCursor} is requested until a valid one is * retrieved or the {@link #state} changes. */ + @SuppressWarnings("NullAway") private void start() { lock.executeWithoutResult(() -> { @@ -188,6 +189,7 @@ public boolean awaitStart(Duration timeout) throws InterruptedException { return awaitStart.await(timeout.toNanos(), TimeUnit.NANOSECONDS); } + @SuppressWarnings("NullAway") protected Message createMessage(T source, Class targetType, RequestOptions options) { SimpleMessage message = new SimpleMessage<>(source, source, MessageProperties.builder() @@ -209,11 +211,10 @@ private void emitMessage(Message message) { } } - @Nullable - private T getNext() { + private @Nullable T getNext() { return lock.execute(() -> { - if (State.RUNNING.equals(state)) { + if (cursor != null && State.RUNNING.equals(state)) { return cursor.tryNext(); } throw new IllegalStateException(String.format("Cursor %s is not longer open", cursor)); @@ -239,8 +240,7 @@ private static boolean isValidCursor(@Nullable MongoCursor cursor) { * @return can be {@literal null}. * @throws RuntimeException The potentially translated exception. */ - @Nullable - private V execute(Supplier callback) { + private @Nullable V execute(Supplier callback) { try { return callback.get(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java index 546f3fdd33..1b24e67e07 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/DefaultMessageListenerContainer.java @@ -25,12 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.util.Lock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ErrorHandler; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java index 1c934e8302..f9a9c4131d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/LazyMappingDelegatingMessage.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.core.messaging; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.util.ClassUtils; @@ -38,12 +39,12 @@ class LazyMappingDelegatingMessage implements Message { } @Override - public S getRaw() { + public @Nullable S getRaw() { return delegate.getRaw(); } @Override - public T getBody() { + public @Nullable T getBody() { if (delegate.getBody() == null || targetType.equals(delegate.getBody().getClass())) { return targetType.cast(delegate.getBody()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java index 46db068096..e7aa5b036d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/Message.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -59,8 +60,7 @@ public interface Message { * @return can be {@literal null}. * @since 4.0 */ - @Nullable - default T getBodyBeforeChange() { + default @Nullable T getBodyBeforeChange() { return null; } @@ -87,8 +87,7 @@ class MessageProperties { * * @return can be {@literal null}. */ - @Nullable - public String getDatabaseName() { + public @Nullable String getDatabaseName() { return databaseName; } @@ -97,8 +96,7 @@ public String getDatabaseName() { * * @return can be {@literal null}. */ - @Nullable - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @@ -162,6 +160,7 @@ public static class MessagePropertiesBuilder { * @param dbName must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public MessagePropertiesBuilder databaseName(String dbName) { Assert.notNull(dbName, "Database name must not be null"); @@ -174,6 +173,7 @@ public MessagePropertiesBuilder databaseName(String dbName) { * @param collectionName must not be {@literal null}. * @return this */ + @Contract("_ -> this") public MessagePropertiesBuilder collectionName(String collectionName) { Assert.notNull(collectionName, "Collection name must not be null"); @@ -185,6 +185,7 @@ public MessagePropertiesBuilder collectionName(String collectionName) { /** * @return the built {@link MessageProperties}. */ + @Contract("-> new") public MessageProperties build() { MessageProperties properties = new MessageProperties(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java index be5308e3cf..acb7bfd8a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SimpleMessage.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -46,12 +46,12 @@ class SimpleMessage implements Message { } @Override - public S getRaw() { + public @Nullable S getRaw() { return raw; } @Override - public T getBody() { + public @Nullable T getBody() { return body; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java index 287ba293b6..7b914f16f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/SubscriptionRequest.java @@ -17,9 +17,9 @@ import java.time.Duration; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,8 +61,7 @@ interface RequestOptions { * @return the name of the database to subscribe to. Can be {@literal null} in which case the default * {@link MongoDatabaseFactory#getMongoDatabase() database} is used. */ - @Nullable - default String getDatabaseName() { + default @Nullable String getDatabaseName() { return null; } @@ -106,7 +105,7 @@ static RequestOptions justDatabase(String database) { return new RequestOptions() { @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java index c6caef12fb..92e23ff847 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/TailableCursorRequest.java @@ -18,10 +18,11 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.messaging.SubscriptionRequest.RequestOptions; import org.springframework.data.mongodb.core.messaging.TailableCursorRequest.TailableCursorRequestOptions.TailableCursorRequestOptionsBuilder; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -121,6 +122,7 @@ public static class TailableCursorRequestOptions implements SubscriptionRequest. TailableCursorRequestOptions() {} + @SuppressWarnings("NullAway") public static TailableCursorRequestOptions of(RequestOptions options) { return builder().collection(options.getCollectionName()).build(); } @@ -136,7 +138,7 @@ public static TailableCursorRequestOptionsBuilder builder() { } @Override - public String getCollectionName() { + public @Nullable String getCollectionName() { return collectionName; } @@ -163,6 +165,7 @@ private TailableCursorRequestOptionsBuilder() {} * @param collection must not be {@literal null} nor {@literal empty}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestOptionsBuilder collection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); @@ -177,6 +180,7 @@ public TailableCursorRequestOptionsBuilder collection(String collection) { * @param filter the {@link Query } to apply for filtering events. Must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestOptionsBuilder filter(Query filter) { Assert.notNull(filter, "Filter must not be null"); @@ -188,6 +192,7 @@ public TailableCursorRequestOptionsBuilder filter(Query filter) { /** * @return the built {@link TailableCursorRequestOptions}. */ + @Contract("-> new") public TailableCursorRequestOptions build() { TailableCursorRequestOptions options = new TailableCursorRequestOptions(); @@ -220,6 +225,7 @@ private TailableCursorRequestBuilder() {} * @param collectionName must not be {@literal null} nor empty. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestBuilder collection(String collectionName) { Assert.hasText(collectionName, "CollectionName must not be null"); @@ -234,6 +240,7 @@ public TailableCursorRequestBuilder collection(String collectionName) { * @param messageListener must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TailableCursorRequestBuilder publishTo(MessageListener messageListener) { Assert.notNull(messageListener, "MessageListener must not be null"); @@ -248,6 +255,7 @@ public TailableCursorRequestBuilder publishTo(MessageListener this") public TailableCursorRequestBuilder filter(Query filter) { Assert.notNull(filter, "Filter must not be null"); @@ -259,6 +267,7 @@ public TailableCursorRequestBuilder filter(Query filter) { /** * @return the build {@link ChangeStreamRequest}. */ + @Contract("_ -> new") public TailableCursorRequest build() { Assert.notNull(listener, "MessageListener must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java index 35be8f2ef8..aa879cc3c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/messaging/package-info.java @@ -2,5 +2,5 @@ * MongoDB specific messaging support for listening to eg. * Change Streams. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.messaging; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java index e2f9169d0d..cae1d3df48 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB core support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java index 8b1620b320..fd81030275 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicQuery.java @@ -18,7 +18,8 @@ import static org.springframework.util.ObjectUtils.*; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -91,6 +92,7 @@ public BasicQuery(Document queryObject, Document fieldsObject) { * @param query the query to copy. * @since 4.4 */ + @SuppressWarnings("NullAway") public BasicQuery(Query query) { super(query); @@ -101,6 +103,7 @@ public BasicQuery(Query query) { } @Override + @Contract("_ -> this") public Query addCriteria(CriteriaDefinition criteria) { this.queryObject.putAll(criteria.getCriteriaObject()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java index 12843ce622..3d89f1e1b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/BasicUpdate.java @@ -23,13 +23,11 @@ import java.util.function.BiFunction; import org.bson.Document; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ClassUtils; /** - * {@link Document}-based {@link Update} variant. - * * @author Thomas Risberg * @author John Brisbin * @author Oliver Gierke @@ -49,48 +47,56 @@ public BasicUpdate(Document updateObject) { } @Override + @Contract("_, _ -> this") public Update set(String key, @Nullable Object value) { setOperationValue("$set", key, value); return this; } @Override + @Contract("_ -> this") public Update unset(String key) { setOperationValue("$unset", key, 1); return this; } @Override + @Contract("_, _ -> this") public Update inc(String key, Number inc) { setOperationValue("$inc", key, inc); return this; } @Override + @Contract("_, _ -> this") public Update push(String key, @Nullable Object value) { setOperationValue("$push", key, value); return this; } @Override + @Contract("_, _ -> this") public Update addToSet(String key, @Nullable Object value) { setOperationValue("$addToSet", key, value); return this; } @Override + @Contract("_, _ -> this") public Update pop(String key, Position pos) { setOperationValue("$pop", key, (pos == Position.FIRST ? -1 : 1)); return this; } @Override + @Contract("_, _ -> this") public Update pull(String key, @Nullable Object value) { setOperationValue("$pull", key, value); return this; } @Override + @Contract("_, _ -> this") public Update pullAll(String key, Object[] values) { setOperationValue("$pullAll", key, List.of(values), (o, o2) -> { @@ -107,6 +113,7 @@ public Update pullAll(String key, Object[] values) { } @Override + @Contract("_, _ -> this") public Update rename(String oldName, String newName) { setOperationValue("$rename", oldName, newName); return this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java index de24c0511d..217e669883 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Collation.java @@ -19,8 +19,9 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -180,6 +181,7 @@ public static Collation from(Document source) { * @param strength comparison level. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation strength(int strength) { ComparisonLevel current = this.strength.orElseGet(() -> new ICUComparisonLevel(strength)); @@ -192,6 +194,7 @@ public Collation strength(int strength) { * @param comparisonLevel must not be {@literal null}. * @return new {@link Collation} */ + @Contract("_ -> new") public Collation strength(ComparisonLevel comparisonLevel) { Collation newInstance = copy(); @@ -205,6 +208,7 @@ public Collation strength(ComparisonLevel comparisonLevel) { * @param caseLevel use {@literal true} to enable {@code caseLevel} comparison. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation caseLevel(boolean caseLevel) { ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::primary); @@ -218,6 +222,7 @@ public Collation caseLevel(boolean caseLevel) { * @param caseFirst must not be {@literal null}. * @return new instance of {@link Collation}. */ + @Contract("_ -> new") public Collation caseFirst(String caseFirst) { return caseFirst(new CaseFirst(caseFirst)); } @@ -228,6 +233,7 @@ public Collation caseFirst(String caseFirst) { * @param sort must not be {@literal null}. * @return new instance of {@link Collation}. */ + @Contract("_ -> new") public Collation caseFirst(CaseFirst sort) { ComparisonLevel strengthValue = strength.orElseGet(ComparisonLevel::tertiary); @@ -239,6 +245,7 @@ public Collation caseFirst(CaseFirst sort) { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation numericOrderingEnabled() { return numericOrdering(true); } @@ -248,6 +255,7 @@ public Collation numericOrderingEnabled() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation numericOrderingDisabled() { return numericOrdering(false); } @@ -257,6 +265,7 @@ public Collation numericOrderingDisabled() { * * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation numericOrdering(boolean flag) { Collation newInstance = copy(); @@ -271,6 +280,7 @@ public Collation numericOrdering(boolean flag) { * @param alternate must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation alternate(String alternate) { Alternate instance = this.alternate.orElseGet(() -> new Alternate(alternate, Optional.empty())); @@ -284,6 +294,7 @@ public Collation alternate(String alternate) { * @param alternate must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation alternate(Alternate alternate) { Collation newInstance = copy(); @@ -296,6 +307,7 @@ public Collation alternate(Alternate alternate) { * * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation backwardDiacriticSort() { return backwards(true); } @@ -305,6 +317,7 @@ public Collation backwardDiacriticSort() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation forwardDiacriticSort() { return backwards(false); } @@ -315,6 +328,7 @@ public Collation forwardDiacriticSort() { * @param backwards must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation backwards(boolean backwards) { Collation newInstance = copy(); @@ -327,6 +341,7 @@ public Collation backwards(boolean backwards) { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation normalizationEnabled() { return normalization(true); } @@ -336,6 +351,7 @@ public Collation normalizationEnabled() { * * @return new {@link Collation}. */ + @Contract("-> new") public Collation normalizationDisabled() { return normalization(false); } @@ -346,6 +362,7 @@ public Collation normalizationDisabled() { * @param normalization must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation normalization(boolean normalization) { Collation newInstance = copy(); @@ -359,6 +376,7 @@ public Collation normalization(boolean normalization) { * @param maxVariable must not be {@literal null}. * @return new {@link Collation}. */ + @Contract("_ -> new") public Collation maxVariable(String maxVariable) { Alternate alternateValue = alternate.orElseGet(Alternate::shifted); @@ -370,6 +388,7 @@ public Collation maxVariable(String maxVariable) { * * @return the native MongoDB {@link Document} representation of the {@link Collation}. */ + @SuppressWarnings("NullAway") public Document toDocument() { return map(toMongoDocumentConverter()); } @@ -379,7 +398,7 @@ public Document toDocument() { * * @return he native MongoDB representation of the {@link Collation}. */ - public com.mongodb.client.model.Collation toMongoCollation() { + public com.mongodb.client.model.@Nullable Collation toMongoCollation() { return map(toMongoCollationConverter()); } @@ -390,7 +409,7 @@ public com.mongodb.client.model.Collation toMongoCollation() { * @param * @return the converted result. */ - public R map(Converter mapper) { + public @Nullable R map(Converter mapper) { return mapper.convert(this); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 8d4cb703bb..547c6965ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -33,6 +33,7 @@ import org.bson.BsonType; import org.bson.Document; import org.bson.types.Binary; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Point; @@ -45,7 +46,7 @@ import org.springframework.data.mongodb.core.schema.JsonSchemaProperty; import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.util.RegexFlags; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -184,6 +185,7 @@ public static Criteria expr(MongoExpression expression) { * * @return new instance of {@link Criteria}. */ + @Contract("_ -> new") public Criteria and(String key) { return new Criteria(this.criteriaChain, key); } @@ -194,6 +196,7 @@ public Criteria and(String key) { * @param value can be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria is(@Nullable Object value) { if (!NOT_SET.equals(isValue)) { @@ -221,6 +224,7 @@ public Criteria is(@Nullable Object value) { * Missing Fields: Equality Filter * @since 3.3 */ + @Contract("_ -> this") public Criteria isNull() { return is(null); } @@ -237,6 +241,7 @@ public Criteria isNull() { * Fields: Type Check * @since 3.3 */ + @Contract("_ -> this") public Criteria isNullValue() { criteria.put("$type", BsonType.NULL.getValue()); @@ -254,6 +259,7 @@ private boolean lastOperatorWasNot() { * @return this. * @see MongoDB Query operator: $ne */ + @Contract("_ -> this") public Criteria ne(@Nullable Object value) { criteria.put("$ne", value); return this; @@ -266,6 +272,7 @@ public Criteria ne(@Nullable Object value) { * @return this. * @see MongoDB Query operator: $lt */ + @Contract("_ -> this") public Criteria lt(Object value) { criteria.put("$lt", value); return this; @@ -278,6 +285,7 @@ public Criteria lt(Object value) { * @return this. * @see MongoDB Query operator: $lte */ + @Contract("_ -> this") public Criteria lte(Object value) { criteria.put("$lte", value); return this; @@ -290,6 +298,7 @@ public Criteria lte(Object value) { * @return this. * @see MongoDB Query operator: $gt */ + @Contract("_ -> this") public Criteria gt(Object value) { criteria.put("$gt", value); return this; @@ -302,6 +311,7 @@ public Criteria gt(Object value) { * @return this. * @see MongoDB Query operator: $gte */ + @Contract("_ -> this") public Criteria gte(Object value) { criteria.put("$gte", value); return this; @@ -314,7 +324,8 @@ public Criteria gte(Object value) { * @return this. * @see MongoDB Query operator: $in */ - public Criteria in(Object... values) { + @Contract("_ -> this") + public Criteria in(@Nullable Object ... values) { if (values.length > 1 && values[1] instanceof Collection) { throw new InvalidMongoDbApiUsageException( "You can only pass in one argument of type " + values[1].getClass().getName()); @@ -330,6 +341,7 @@ public Criteria in(Object... values) { * @return this. * @see MongoDB Query operator: $in */ + @Contract("_ -> this") public Criteria in(Collection values) { criteria.put("$in", values); return this; @@ -342,6 +354,7 @@ public Criteria in(Collection values) { * @return this. * @see MongoDB Query operator: $nin */ + @Contract("_ -> this") public Criteria nin(Object... values) { return nin(Arrays.asList(values)); } @@ -353,6 +366,7 @@ public Criteria nin(Object... values) { * @return this. * @see MongoDB Query operator: $nin */ + @Contract("_ -> this") public Criteria nin(Collection values) { criteria.put("$nin", values); return this; @@ -366,6 +380,7 @@ public Criteria nin(Collection values) { * @return this. * @see MongoDB Query operator: $mod */ + @Contract("_ -> this") public Criteria mod(Number value, Number remainder) { List l = new ArrayList<>(2); l.add(value); @@ -381,6 +396,7 @@ public Criteria mod(Number value, Number remainder) { * @return this. * @see MongoDB Query operator: $all */ + @Contract("_ -> this") public Criteria all(Object... values) { return all(Arrays.asList(values)); } @@ -392,6 +408,7 @@ public Criteria all(Object... values) { * @return this. * @see MongoDB Query operator: $all */ + @Contract("_ -> this") public Criteria all(Collection values) { criteria.put("$all", values); return this; @@ -404,6 +421,7 @@ public Criteria all(Collection values) { * @return this. * @see MongoDB Query operator: $size */ + @Contract("_ -> this") public Criteria size(int size) { criteria.put("$size", size); return this; @@ -416,6 +434,7 @@ public Criteria size(int size) { * @return this. * @see MongoDB Query operator: $exists */ + @Contract("_ -> this") public Criteria exists(boolean value) { criteria.put("$exists", value); return this; @@ -431,6 +450,7 @@ public Criteria exists(boolean value) { * $sampleRate * @since 3.3 */ + @Contract("_ -> this") public Criteria sampleRate(double sampleRate) { Assert.isTrue(sampleRate >= 0, "The sample rate must be greater than zero"); @@ -447,6 +467,7 @@ public Criteria sampleRate(double sampleRate) { * @return this. * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(int typeNumber) { criteria.put("$type", typeNumber); return this; @@ -460,6 +481,7 @@ public Criteria type(int typeNumber) { * @since 2.1 * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(Type... types) { Assert.notNull(types, "Types must not be null"); @@ -476,6 +498,7 @@ public Criteria type(Type... types) { * @since 3.2 * @see MongoDB Query operator: $type */ + @Contract("_ -> this") public Criteria type(Collection types) { Assert.notNull(types, "Types must not be null"); @@ -490,6 +513,7 @@ public Criteria type(Collection types) { * @return this. * @see MongoDB Query operator: $not */ + @Contract("-> this") public Criteria not() { return not(null); } @@ -501,6 +525,7 @@ public Criteria not() { * @return this. * @see MongoDB Query operator: $not */ + @Contract("_ -> this") private Criteria not(@Nullable Object value) { criteria.put("$not", value); return this; @@ -513,6 +538,7 @@ private Criteria not(@Nullable Object value) { * @return this. * @see MongoDB Query operator: $regex */ + @Contract("_ -> this") public Criteria regex(String regex) { return regex(regex, null); } @@ -525,6 +551,7 @@ public Criteria regex(String regex) { * @return this. * @see MongoDB Query operator: $regex */ + @Contract("_, _ -> this") public Criteria regex(String regex, @Nullable String options) { return regex(toPattern(regex, options)); } @@ -535,6 +562,7 @@ public Criteria regex(String regex, @Nullable String options) { * @param pattern must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria regex(Pattern pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -553,6 +581,7 @@ public Criteria regex(Pattern pattern) { * @param regex must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria regex(BsonRegularExpression regex) { if (lastOperatorWasNot()) { @@ -581,6 +610,7 @@ private Pattern toPattern(String regex, @Nullable String options) { * @see MongoDB Query operator: * $centerSphere */ + @Contract("_ -> this") public Criteria withinSphere(Circle circle) { Assert.notNull(circle, "Circle must not be null"); @@ -597,6 +627,7 @@ public Criteria withinSphere(Circle circle) { * @see MongoDB Query operator: * $geoWithin */ + @Contract("_ -> this") public Criteria within(Shape shape) { Assert.notNull(shape, "Shape must not be null"); @@ -612,6 +643,7 @@ public Criteria within(Shape shape) { * @return this. * @see MongoDB Query operator: $near */ + @Contract("_ -> this") public Criteria near(Point point) { Assert.notNull(point, "Point must not be null"); @@ -629,6 +661,7 @@ public Criteria near(Point point) { * @see MongoDB Query operator: * $nearSphere */ + @Contract("_ -> this") public Criteria nearSphere(Point point) { Assert.notNull(point, "Point must not be null"); @@ -646,6 +679,7 @@ public Criteria nearSphere(Point point) { * @since 1.8 */ @SuppressWarnings("rawtypes") + @Contract("_ -> this") public Criteria intersects(GeoJson geoJson) { Assert.notNull(geoJson, "GeoJson must not be null"); @@ -665,6 +699,7 @@ public Criteria intersects(GeoJson geoJson) { * @see MongoDB Query operator: * $maxDistance */ + @Contract("_ -> this") public Criteria maxDistance(double maxDistance) { if (createNearCriteriaForCommand("$near", "$maxDistance", maxDistance) @@ -687,6 +722,7 @@ public Criteria maxDistance(double maxDistance) { * @return this. * @since 1.7 */ + @Contract("_ -> this") public Criteria minDistance(double minDistance) { if (createNearCriteriaForCommand("$near", "$minDistance", minDistance) @@ -706,6 +742,7 @@ public Criteria minDistance(double minDistance) { * @see MongoDB Query operator: * $elemMatch */ + @Contract("_ -> this") public Criteria elemMatch(Criteria criteria) { this.criteria.put("$elemMatch", criteria.getCriteriaObject()); return this; @@ -718,6 +755,7 @@ public Criteria elemMatch(Criteria criteria) { * @return this. * @since 1.8 */ + @Contract("_ -> this") public Criteria alike(Example sample) { if (StringUtils.hasText(this.getKey())) { @@ -745,6 +783,7 @@ public Criteria alike(Example sample) { * @see MongoDB Query operator: * $jsonSchema */ + @Contract("_ -> this") public Criteria andDocumentStructureMatches(MongoJsonSchema schema) { Assert.notNull(schema, "Schema must not be null"); @@ -776,6 +815,7 @@ public BitwiseCriteriaOperators bits() { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria orOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -793,6 +833,7 @@ public Criteria orOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria orOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -810,6 +851,7 @@ public Criteria orOperator(Collection criteria) { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria norOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -827,6 +869,7 @@ public Criteria norOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria norOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -844,6 +887,7 @@ public Criteria norOperator(Collection criteria) { * @param criteria must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Criteria andOperator(Criteria... criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -861,6 +905,7 @@ public Criteria andOperator(Criteria... criteria) { * @return this. * @since 3.2 */ + @Contract("_ -> this") public Criteria andOperator(Collection criteria) { Assert.notNull(criteria, "Criteria must not be null"); @@ -884,8 +929,7 @@ private Criteria registerCriteriaChainElement(Criteria criteria) { * @see org.springframework.data.mongodb.core.query.CriteriaDefinition#getKey() */ @Override - @Nullable - public String getKey() { + public @Nullable String getKey() { return this.key; } @@ -1095,7 +1139,7 @@ private boolean isEqual(@Nullable Object left, @Nullable Object right) { if (Collection.class.isAssignableFrom(left.getClass())) { - if (!Collection.class.isAssignableFrom(right.getClass())) { + if (right == null || !Collection.class.isAssignableFrom(right.getClass())) { return false; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java index c00b1d4b82..c75f709ab9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.query; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Oliver Gierke diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java index 3540a5a836..9775fefdb0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Field.java @@ -22,8 +22,9 @@ import java.util.Map.Entry; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.MongoExpression; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -52,6 +53,7 @@ public class Field { * @param field the document field name to be included. * @return {@code this} field projection instance. */ + @Contract("_ -> this") public Field include(String field) { Assert.notNull(field, "Key must not be null"); @@ -111,6 +113,7 @@ public FieldProjectionExpression project(MongoExpression expression) { * @return new instance of {@link FieldProjectionExpression}. * @since 3.2 */ + @Contract("_, _ -> this") public Field projectAs(MongoExpression expression, String field) { criteria.put(field, expression); @@ -124,6 +127,7 @@ public Field projectAs(MongoExpression expression, String field) { * @return {@code this} field projection instance. * @since 3.1 */ + @Contract("_ -> this") public Field include(String... fields) { return include(Arrays.asList(fields)); } @@ -135,6 +139,7 @@ public Field include(String... fields) { * @return {@code this} field projection instance. * @since 4.4 */ + @Contract("_ -> this") public Field include(Collection fields) { Assert.notNull(fields, "Keys must not be null"); @@ -149,6 +154,7 @@ public Field include(Collection fields) { * @param field the document field name to be excluded. * @return {@code this} field projection instance. */ + @Contract("_ -> this") public Field exclude(String field) { Assert.notNull(field, "Key must not be null"); @@ -165,6 +171,7 @@ public Field exclude(String field) { * @return {@code this} field projection instance. * @since 3.1 */ + @Contract("_ -> this") public Field exclude(String... fields) { return exclude(Arrays.asList(fields)); } @@ -176,6 +183,7 @@ public Field exclude(String... fields) { * @return {@code this} field projection instance. * @since 4.4 */ + @Contract("_ -> this") public Field exclude(Collection fields) { Assert.notNull(fields, "Keys must not be null"); @@ -191,6 +199,7 @@ public Field exclude(Collection fields) { * @param size the number of elements to include. * @return {@code this} field projection instance. */ + @Contract("_, _ -> this") public Field slice(String field, int size) { Assert.notNull(field, "Key must not be null"); @@ -209,12 +218,14 @@ public Field slice(String field, int size) { * @param size the number of elements to include. * @return {@code this} field projection instance. */ + @Contract("_, _, _ -> this") public Field slice(String field, int offset, int size) { slices.put(field, Arrays.asList(offset, size)); return this; } + @Contract("_, _ -> this") public Field elemMatch(String field, Criteria elemMatchCriteria) { elemMatches.put(field, elemMatchCriteria); @@ -229,6 +240,7 @@ public Field elemMatch(String field, Criteria elemMatchCriteria) { * @param value * @return {@code this} field projection instance. */ + @Contract("_, _ -> this") public Field position(String field, int value) { Assert.hasText(field, "DocumentField must not be null or empty"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java index 83417c7200..19ecd94e23 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/GeoCommand.java @@ -17,12 +17,12 @@ import static org.springframework.util.ObjectUtils.*; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Box; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Polygon; import org.springframework.data.geo.Shape; import org.springframework.data.mongodb.core.geo.Sphere; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java index 5757aa94a2..5ec4af3989 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Meta.java @@ -23,7 +23,7 @@ import java.util.Map.Entry; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -50,8 +50,8 @@ private enum MetaKey { private Map values = Collections.emptyMap(); private Set flags = Collections.emptySet(); - private Integer cursorBatchSize; - private Boolean allowDiskUse; + private @Nullable Integer cursorBatchSize; + private @Nullable Boolean allowDiskUse; public Meta() {} @@ -85,8 +85,7 @@ public boolean hasMaxTime() { /** * @return {@literal null} if not set. */ - @Nullable - public Long getMaxTimeMsec() { + public @Nullable Long getMaxTimeMsec() { return getValue(MetaKey.MAX_TIME_MS.key); } @@ -181,8 +180,7 @@ public void setComment(String comment) { * @return {@literal null} if not set. * @since 2.1 */ - @Nullable - public Integer getCursorBatchSize() { + public @Nullable Integer getCursorBatchSize() { return cursorBatchSize; } @@ -285,9 +283,8 @@ void setValue(String key, @Nullable Object value) { this.values.put(key, value); } - @Nullable @SuppressWarnings("unchecked") - private T getValue(String key) { + private @Nullable T getValue(String key) { return (T) this.values.get(key); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java index 571bbd275c..5625de5e93 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MetricConversion.java @@ -20,9 +20,11 @@ import java.math.MathContext; import java.math.RoundingMode; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; +import org.springframework.util.Assert; /** * {@link Metric} and {@link Distance} conversions using the metric system. @@ -151,8 +153,8 @@ static ConversionMultiplierBuilder builder() { */ private static class ConversionMultiplierBuilder { - private Number from; - private Number to; + private @Nullable Number from; + private @Nullable Number to; ConversionMultiplierBuilder() {} @@ -177,6 +179,9 @@ ConversionMultiplierBuilder to(Metric to) { } ConversionMultiplier build() { + + Assert.notNull(from, "[From] must be set first"); + Assert.notNull(to, "[To] must be set first"); return new ConversionMultiplier(this.from, this.to); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java index e26a61c61e..b37c088981 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/MongoRegexCreator.java @@ -18,7 +18,7 @@ import java.util.regex.Pattern; import org.bson.BsonRegularExpression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Christoph Strobl @@ -80,8 +80,7 @@ public enum MatchMode { * @param matcherType the type of matching to perform * @return {@literal source} when {@literal source} or {@literal matcherType} is {@literal null}. */ - @Nullable - public String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) { + public @Nullable String toRegularExpression(@Nullable String source, @Nullable MatchMode matcherType) { if (matcherType == null || source == null) { return source; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index f0f3b0a4dc..6dad07b8cb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -18,6 +18,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.CustomMetric; import org.springframework.data.geo.Distance; @@ -27,7 +28,7 @@ import org.springframework.data.mongodb.core.ReadConcernAware; import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.geo.GeoJsonPoint; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -278,6 +279,7 @@ public Metric getMetric() { * @return * @since 2.2 */ + @Contract("_ -> this") public NearQuery limit(long limit) { this.limit = limit; return this; @@ -289,6 +291,7 @@ public NearQuery limit(long limit) { * @param skip * @return */ + @Contract("_ -> this") public NearQuery skip(long skip) { this.skip = skip; return this; @@ -300,6 +303,7 @@ public NearQuery skip(long skip) { * @param pageable must not be {@literal null} * @return */ + @Contract("_ -> this") public NearQuery with(Pageable pageable) { Assert.notNull(pageable, "Pageable must not be 'null'"); @@ -323,6 +327,7 @@ public NearQuery with(Pageable pageable) { * @param maxDistance * @return */ + @Contract("_ -> this") public NearQuery maxDistance(double maxDistance) { return maxDistance(new Distance(maxDistance, getMetric())); } @@ -335,6 +340,7 @@ public NearQuery maxDistance(double maxDistance) { * @param metric must not be {@literal null}. * @return */ + @Contract("_, _ -> this") public NearQuery maxDistance(double maxDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); @@ -349,6 +355,7 @@ public NearQuery maxDistance(double maxDistance, Metric metric) { * @param distance must not be {@literal null}. * @return */ + @Contract("_ -> this") public NearQuery maxDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null"); @@ -379,6 +386,7 @@ public NearQuery maxDistance(Distance distance) { * @return * @since 1.7 */ + @Contract("_ -> this") public NearQuery minDistance(double minDistance) { return minDistance(new Distance(minDistance, getMetric())); } @@ -392,6 +400,7 @@ public NearQuery minDistance(double minDistance) { * @return * @since 1.7 */ + @Contract("_, _ -> this") public NearQuery minDistance(double minDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); @@ -407,6 +416,7 @@ public NearQuery minDistance(double minDistance, Metric metric) { * @return * @since 1.7 */ + @Contract("_ -> this") public NearQuery minDistance(Distance distance) { Assert.notNull(distance, "Distance must not be null"); @@ -428,8 +438,7 @@ public NearQuery minDistance(Distance distance) { * * @return */ - @Nullable - public Distance getMaxDistance() { + public @Nullable Distance getMaxDistance() { return this.maxDistance; } @@ -439,8 +448,7 @@ public Distance getMaxDistance() { * @return * @since 1.7 */ - @Nullable - public Distance getMinDistance() { + public @Nullable Distance getMinDistance() { return this.minDistance; } @@ -450,6 +458,7 @@ public Distance getMinDistance() { * @param distanceMultiplier * @return */ + @Contract("_ -> this") public NearQuery distanceMultiplier(double distanceMultiplier) { this.metric = new CustomMetric(distanceMultiplier); @@ -462,6 +471,7 @@ public NearQuery distanceMultiplier(double distanceMultiplier) { * @param spherical * @return */ + @Contract("_ -> this") public NearQuery spherical(boolean spherical) { this.spherical = spherical; return this; @@ -482,6 +492,7 @@ public boolean isSpherical() { * * @return */ + @Contract("-> this") public NearQuery inKilometers() { return adaptMetric(Metrics.KILOMETERS); } @@ -492,6 +503,7 @@ public NearQuery inKilometers() { * * @return */ + @Contract("-> this") public NearQuery inMiles() { return adaptMetric(Metrics.MILES); } @@ -504,6 +516,7 @@ public NearQuery inMiles() { * passed. * @return */ + @Contract("_ -> this") public NearQuery in(@Nullable Metric metric) { return adaptMetric(metric == null ? Metrics.NEUTRAL : metric); } @@ -514,6 +527,7 @@ public NearQuery in(@Nullable Metric metric) { * * @param metric */ + @Contract("_ -> this") private NearQuery adaptMetric(Metric metric) { if (metric != Metrics.NEUTRAL) { @@ -530,6 +544,7 @@ private NearQuery adaptMetric(Metric metric) { * @param query must not be {@literal null}. * @return */ + @Contract("_ -> this") public NearQuery query(Query query) { Assert.notNull(query, "Cannot apply 'null' query on NearQuery"); @@ -546,8 +561,7 @@ public NearQuery query(Query query) { /** * @return the number of elements to skip. */ - @Nullable - public Long getSkip() { + public @Nullable Long getSkip() { return skip; } @@ -557,8 +571,7 @@ public Long getSkip() { * @return the {@link Collation} if set. {@literal null} otherwise. * @since 2.2 */ - @Nullable - public Collation getCollation() { + public @Nullable Collation getCollation() { return query != null ? query.getCollation().orElse(null) : null; } @@ -570,6 +583,7 @@ public Collation getCollation() { * @return this. * @since 4.1 */ + @Contract("_ -> this") public NearQuery withReadConcern(ReadConcern readConcern) { Assert.notNull(readConcern, "ReadConcern must not be null"); @@ -585,6 +599,7 @@ public NearQuery withReadConcern(ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public NearQuery withReadPreference(ReadPreference readPreference) { Assert.notNull(readPreference, "ReadPreference must not be null"); @@ -601,9 +616,8 @@ public NearQuery withReadPreference(ReadPreference readPreference) { * @since 4.1 * @see ReadConcernAware */ - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { if (query != null && query.hasReadConcern()) { return query.getReadConcern(); @@ -620,9 +634,8 @@ public ReadConcern getReadConcern() { * @since 4.1 * @see ReadPreferenceAware */ - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { if (query != null && query.hasReadPreference()) { return query.getReadPreference(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java index 31c6b9069f..47ce615fe3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Query.java @@ -15,8 +15,9 @@ */ package org.springframework.data.mongodb.core.query; -import static org.springframework.data.mongodb.core.query.SerializationUtils.*; -import static org.springframework.util.ObjectUtils.*; +import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; +import static org.springframework.util.ObjectUtils.nullSafeEquals; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; import java.time.Duration; import java.util.ArrayList; @@ -30,6 +31,7 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Limit; import org.springframework.data.domain.OffsetScrollPosition; @@ -42,7 +44,7 @@ import org.springframework.data.mongodb.core.ReadPreferenceAware; import org.springframework.data.mongodb.core.query.Meta.CursorOption; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.ReadConcern; @@ -69,7 +71,7 @@ public class Query implements ReadConcernAware, ReadPreferenceAware { private long skip; private Limit limit = Limit.unlimited(); - private KeysetScrollPosition keysetScrollPosition; + private @Nullable KeysetScrollPosition keysetScrollPosition; private @Nullable ReadConcern readConcern; private @Nullable ReadPreference readPreference; @@ -123,6 +125,7 @@ public Query(CriteriaDefinition criteriaDefinition) { * @return this. * @since 1.6 */ + @Contract("_ -> this") public Query addCriteria(CriteriaDefinition criteriaDefinition) { Assert.notNull(criteriaDefinition, "CriteriaDefinition must not be null"); @@ -157,6 +160,7 @@ public Field fields() { * @param skip number of documents to skip. Use {@literal zero} or a {@literal negative} value to avoid skipping. * @return this. */ + @Contract("_ -> this") public Query skip(long skip) { this.skip = skip; return this; @@ -169,6 +173,7 @@ public Query skip(long skip) { * @param limit number of documents to return. Use {@literal zero} or {@literal negative} for unlimited. * @return this. */ + @Contract("_ -> this") public Query limit(int limit) { this.limit = limit > 0 ? Limit.of(limit) : Limit.unlimited(); return this; @@ -181,6 +186,7 @@ public Query limit(int limit) { * @return this. * @since 4.2 */ + @Contract("_ -> this") public Query limit(Limit limit) { Assert.notNull(limit, "Limit must not be null"); @@ -202,6 +208,7 @@ public Query limit(Limit limit) { * @return this. * @see Document#parse(String) */ + @Contract("_ -> this") public Query withHint(String hint) { Assert.hasText(hint, "Hint must not be empty or null"); @@ -216,6 +223,7 @@ public Query withHint(String hint) { * @return this. * @since 3.1 */ + @Contract("_ -> this") public Query withReadConcern(ReadConcern readConcern) { Assert.notNull(readConcern, "ReadConcern must not be null"); @@ -230,6 +238,7 @@ public Query withReadConcern(ReadConcern readConcern) { * @return this. * @since 4.1 */ + @Contract("_ -> this") public Query withReadPreference(ReadPreference readPreference) { Assert.notNull(readPreference, "ReadPreference must not be null"); @@ -243,7 +252,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return this.readConcern; } @@ -253,7 +262,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { if (readPreference == null) { return getMeta().getFlags().contains(CursorOption.SECONDARY_READS) ? ReadPreference.primaryPreferred() : null; @@ -269,6 +278,7 @@ public ReadPreference getReadPreference() { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Query withHint(Document hint) { Assert.notNull(hint, "Hint must not be null"); @@ -283,6 +293,7 @@ public Query withHint(Document hint) { * @param pageable must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(Pageable pageable) { if (pageable.isPaged()) { @@ -299,6 +310,7 @@ public Query with(Pageable pageable) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(ScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -320,6 +332,7 @@ public Query with(ScrollPosition position) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(OffsetScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -335,6 +348,7 @@ public Query with(OffsetScrollPosition position) { * @param position must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(KeysetScrollPosition position) { Assert.notNull(position, "ScrollPosition must not be null"); @@ -349,8 +363,7 @@ public boolean hasKeyset() { return keysetScrollPosition != null; } - @Nullable - public KeysetScrollPosition getKeyset() { + public @Nullable KeysetScrollPosition getKeyset() { return keysetScrollPosition; } @@ -360,6 +373,7 @@ public KeysetScrollPosition getKeyset() { * @param sort must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public Query with(Sort sort) { Assert.notNull(sort, "Sort must not be null"); @@ -393,6 +407,7 @@ public Set> getRestrictedTypes() { * @param additionalTypes may not be {@literal null} * @return this. */ + @Contract("_, _ -> this") public Query restrict(Class type, Class... additionalTypes) { Assert.notNull(type, "Type must not be null"); @@ -518,6 +533,7 @@ public String getHint() { * @see Meta#setMaxTimeMsec(long) * @since 1.6 */ + @Contract("_ -> this") public Query maxTimeMsec(long maxTimeMsec) { meta.setMaxTimeMsec(maxTimeMsec); @@ -530,6 +546,7 @@ public Query maxTimeMsec(long maxTimeMsec) { * @see Meta#setMaxTime(Duration) * @since 2.1 */ + @Contract("_ -> this") public Query maxTime(Duration timeout) { meta.setMaxTime(timeout); @@ -544,6 +561,7 @@ public Query maxTime(Duration timeout) { * @see Meta#setComment(String) * @since 1.6 */ + @Contract("_ -> this") public Query comment(String comment) { meta.setComment(comment); @@ -562,6 +580,7 @@ public Query comment(String comment) { * @see Meta#setAllowDiskUse(Boolean) * @since 3.2 */ + @Contract("_ -> this") public Query allowDiskUse(boolean allowDiskUse) { meta.setAllowDiskUse(allowDiskUse); @@ -578,6 +597,7 @@ public Query allowDiskUse(boolean allowDiskUse) { * @see Meta#setCursorBatchSize(int) * @since 2.1 */ + @Contract("_ -> this") public Query cursorBatchSize(int batchSize) { meta.setCursorBatchSize(batchSize); @@ -589,6 +609,7 @@ public Query cursorBatchSize(int batchSize) { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#NO_TIMEOUT * @since 1.10 */ + @Contract("-> this") public Query noCursorTimeout() { meta.addFlag(Meta.CursorOption.NO_TIMEOUT); @@ -600,6 +621,7 @@ public Query noCursorTimeout() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#EXHAUST * @since 1.10 */ + @Contract("-> this") public Query exhaust() { meta.addFlag(Meta.CursorOption.EXHAUST); @@ -613,6 +635,7 @@ public Query exhaust() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#SECONDARY_READS * @since 3.0.2 */ + @Contract("-> this") public Query allowSecondaryReads() { meta.addFlag(Meta.CursorOption.SECONDARY_READS); @@ -624,6 +647,7 @@ public Query allowSecondaryReads() { * @see org.springframework.data.mongodb.core.query.Meta.CursorOption#PARTIAL * @since 1.10 */ + @Contract("-> this") public Query partialResults() { meta.addFlag(Meta.CursorOption.PARTIAL); @@ -655,6 +679,7 @@ public void setMeta(Meta meta) { * @return this. * @since 2.0 */ + @Contract("_ -> this") public Query collation(@Nullable Collation collation) { this.collation = Optional.ofNullable(collation); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java index 11e0f7fb24..29f8adb2c6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/SerializationUtils.java @@ -23,9 +23,9 @@ import java.util.Map; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; /** @@ -110,8 +110,8 @@ private static void toFlatMap(String currentPath, Object source, Map null; !null -> !null") + public static @Nullable String serializeToJsonSafely(@Nullable Object value) { if (value == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java index bd6d8c3469..cc87434178 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Term.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.core.query; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; /** @@ -61,6 +62,7 @@ public Term(String raw, @Nullable Type type) { * * @return */ + @Contract("-> this") public Term negate() { this.negated = true; return this; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java index e1a7d0c4d0..5cedc2e476 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextCriteria.java @@ -19,7 +19,8 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -71,7 +72,8 @@ public static TextCriteria forDefaultLanguage() { * @param language * @return */ - public static TextCriteria forLanguage(String language) { + @Contract("null -> fail") + public static TextCriteria forLanguage(@Nullable String language) { Assert.hasText(language, "Language must not be null or empty"); return new TextCriteria(language); @@ -83,6 +85,7 @@ public static TextCriteria forLanguage(String language) { * @param words the words to match. * @return */ + @Contract("_ -> this") public TextCriteria matchingAny(String... words) { for (String word : words) { @@ -97,6 +100,7 @@ public TextCriteria matchingAny(String... words) { * * @param term must not be {@literal null}. */ + @Contract("_ -> this") public TextCriteria matching(Term term) { Assert.notNull(term, "Term to add must not be null"); @@ -109,6 +113,7 @@ public TextCriteria matching(Term term) { * @param term * @return */ + @Contract("_ -> this") public TextCriteria matching(String term) { if (StringUtils.hasText(term)) { @@ -121,6 +126,7 @@ public TextCriteria matching(String term) { * @param term * @return */ + @Contract("_ -> this") public TextCriteria notMatching(String term) { if (StringUtils.hasText(term)) { @@ -133,6 +139,7 @@ public TextCriteria notMatching(String term) { * @param words * @return */ + @Contract("_ -> this") public TextCriteria notMatchingAny(String... words) { for (String word : words) { @@ -147,6 +154,7 @@ public TextCriteria notMatchingAny(String... words) { * @param phrase * @return */ + @Contract("_ -> this") public TextCriteria notMatchingPhrase(String phrase) { if (StringUtils.hasText(phrase)) { @@ -161,6 +169,7 @@ public TextCriteria notMatchingPhrase(String phrase) { * @param phrase * @return */ + @Contract("_ -> this") public TextCriteria matchingPhrase(String phrase) { if (StringUtils.hasText(phrase)) { @@ -176,6 +185,7 @@ public TextCriteria matchingPhrase(String phrase) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public TextCriteria caseSensitive(boolean caseSensitive) { this.caseSensitive = caseSensitive; @@ -189,6 +199,7 @@ public TextCriteria caseSensitive(boolean caseSensitive) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public TextCriteria diacriticSensitive(boolean diacriticSensitive) { this.diacriticSensitive = diacriticSensitive; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java index a6583299d6..a9f82a857f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/TextQuery.java @@ -19,8 +19,9 @@ import java.util.Map.Entry; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * {@link Query} implementation to be used to for performing full text searches. @@ -100,6 +101,7 @@ public static TextQuery queryText(TextCriteria criteria) { * @see TextQuery#includeScore() * @return this. */ + @Contract("-> this") public TextQuery sortByScore() { this.sortByScoreIndex = getSortObject().size(); @@ -113,6 +115,7 @@ public TextQuery sortByScore() { * * @return this. */ + @Contract("-> this") public TextQuery includeScore() { this.includeScore = true; @@ -125,6 +128,7 @@ public TextQuery includeScore() { * @param fieldname must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public TextQuery includeScore(String fieldname) { setScoreFieldName(fieldname); @@ -170,9 +174,8 @@ public Document getSortObject() { int sortByScoreIndex = this.sortByScoreIndex; - return sortByScoreIndex != 0 - ? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex) - : sortByScoreAtPositionZero(); + return sortByScoreIndex != 0 ? sortByScoreAtPosition(super.getSortObject(), sortByScoreIndex) + : sortByScoreAtPositionZero(); } return super.getSortObject(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java index 677575c9e4..c02425214d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/UntypedExampleMatcher.java @@ -17,8 +17,8 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.ExampleMatcher; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java index 32d98f5804..cfb214a5a3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Update.java @@ -27,11 +27,12 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; import org.springframework.data.domain.Sort.Order; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -114,6 +115,7 @@ public static Update fromDocument(Document object, String... exclude) { * @return this. * @see MongoDB Update operator: $set */ + @Contract("_, _ -> this") public Update set(String key, @Nullable Object value) { addMultiFieldOperation("$set", key, value); return this; @@ -128,6 +130,7 @@ public Update set(String key, @Nullable Object value) { * @see MongoDB Update operator: * $setOnInsert */ + @Contract("_, _ -> this") public Update setOnInsert(String key, @Nullable Object value) { addMultiFieldOperation("$setOnInsert", key, value); return this; @@ -140,6 +143,7 @@ public Update setOnInsert(String key, @Nullable Object value) { * @return this. * @see MongoDB Update operator: $unset */ + @Contract("_ -> this") public Update unset(String key) { addMultiFieldOperation("$unset", key, 1); return this; @@ -153,12 +157,14 @@ public Update unset(String key) { * @return this. * @see MongoDB Update operator: $inc */ + @Contract("_, _ -> this") public Update inc(String key, Number inc) { addMultiFieldOperation("$inc", key, inc); return this; } @Override + @Contract("_ -> this") public void inc(String key) { inc(key, 1L); } @@ -171,6 +177,7 @@ public void inc(String key) { * @return this. * @see MongoDB Update operator: $push */ + @Contract("_, _ -> this") public Update push(String key, @Nullable Object value) { addMultiFieldOperation("$push", key, value); return this; @@ -207,6 +214,7 @@ public PushOperatorBuilder push(String key) { * @return new instance of {@link AddToSetBuilder}. * @since 1.5 */ + @Contract("_ -> new") public AddToSetBuilder addToSet(String key) { return new AddToSetBuilder(key); } @@ -220,6 +228,7 @@ public AddToSetBuilder addToSet(String key) { * @see MongoDB Update operator: * $addToSet */ + @Contract("_, _ -> this") public Update addToSet(String key, @Nullable Object value) { addMultiFieldOperation("$addToSet", key, value); return this; @@ -233,6 +242,7 @@ public Update addToSet(String key, @Nullable Object value) { * @return this. * @see MongoDB Update operator: $pop */ + @Contract("_, _ -> this") public Update pop(String key, Position pos) { addMultiFieldOperation("$pop", key, pos == Position.FIRST ? -1 : 1); return this; @@ -246,6 +256,7 @@ public Update pop(String key, Position pos) { * @return this. * @see MongoDB Update operator: $pull */ + @Contract("_, _ -> this") public Update pull(String key, @Nullable Object value) { addMultiFieldOperation("$pull", key, value); return this; @@ -260,6 +271,7 @@ public Update pull(String key, @Nullable Object value) { * @see MongoDB Update operator: * $pullAll */ + @Contract("_, _ -> this") public Update pullAll(String key, Object[] values) { addMultiFieldOperation("$pullAll", key, Arrays.asList(values)); return this; @@ -274,6 +286,7 @@ public Update pullAll(String key, Object[] values) { * @see MongoDB Update operator: * $rename */ + @Contract("_, _ -> this") public Update rename(String oldName, String newName) { addMultiFieldOperation("$rename", oldName, newName); return this; @@ -288,6 +301,7 @@ public Update rename(String oldName, String newName) { * @see MongoDB Update operator: * $currentDate */ + @Contract("_ -> this") public Update currentDate(String key) { addMultiFieldOperation("$currentDate", key, true); @@ -303,6 +317,7 @@ public Update currentDate(String key) { * @see MongoDB Update operator: * $currentDate */ + @Contract("_ -> this") public Update currentTimestamp(String key) { addMultiFieldOperation("$currentDate", key, new Document("$type", "timestamp")); @@ -318,6 +333,7 @@ public Update currentTimestamp(String key) { * @since 1.7 * @see MongoDB Update operator: $mul */ + @Contract("_, _ -> this") public Update multiply(String key, Number multiplier) { Assert.notNull(multiplier, "Multiplier must not be null"); @@ -335,6 +351,7 @@ public Update multiply(String key, Number multiplier) { * @see Comparison/Sort Order * @see MongoDB Update operator: $max */ + @Contract("_, _ -> this") public Update max(String key, Object value) { Assert.notNull(value, "Value for max operation must not be null"); @@ -352,6 +369,7 @@ public Update max(String key, Object value) { * @see Comparison/Sort Order * @see MongoDB Update operator: $min */ + @Contract("_, _ -> this") public Update min(String key, Object value) { Assert.notNull(value, "Value for min operation must not be null"); @@ -366,6 +384,7 @@ public Update min(String key, Object value) { * @return this. * @since 1.7 */ + @Contract("_ -> new") public BitwiseOperatorBuilder bitwise(String key) { return new BitwiseOperatorBuilder(this, key); } @@ -378,6 +397,7 @@ public BitwiseOperatorBuilder bitwise(String key) { * @return this. * @since 2.0 */ + @Contract("-> this") public Update isolated() { isolated = true; @@ -392,6 +412,7 @@ public Update isolated() { * @return this. * @since 2.2 */ + @Contract("_ -> this") public Update filterArray(CriteriaDefinition criteria) { if (arrayFilters == Collections.EMPTY_LIST) { @@ -411,6 +432,7 @@ public Update filterArray(CriteriaDefinition criteria) { * @return this. * @since 2.2 */ + @Contract("_, _ -> this") public Update filterArray(String identifier, Object expression) { if (arrayFilters == Collections.EMPTY_LIST) { @@ -815,6 +837,7 @@ public Update each(Object... values) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder slice(int count) { this.modifiers.addModifier(new Slice(count)); @@ -829,6 +852,7 @@ public PushOperatorBuilder slice(int count) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder sort(Direction direction) { Assert.notNull(direction, "Direction must not be null"); @@ -844,6 +868,7 @@ public PushOperatorBuilder sort(Direction direction) { * @return never {@literal null}. * @since 1.10 */ + @Contract("_ -> this") public PushOperatorBuilder sort(Sort sort) { Assert.notNull(sort, "Sort must not be null"); @@ -859,6 +884,7 @@ public PushOperatorBuilder sort(Sort sort) { * @return never {@literal null}. * @since 1.7 */ + @Contract("_ -> this") public PushOperatorBuilder atPosition(int position) { this.modifiers.addModifier(new PositionModifier(position)); @@ -872,6 +898,7 @@ public PushOperatorBuilder atPosition(int position) { * @return never {@literal null}. * @since 1.7 */ + @Contract("_ -> this") public PushOperatorBuilder atPosition(@Nullable Position position) { if (position == null || Position.LAST.equals(position)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java index d3f67790a1..7c6889e45b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB specific query and update support. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.query; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java index b59c20c6b6..da77a0199f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/DefaultMongoJsonSchema.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core.schema; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -31,8 +31,7 @@ class DefaultMongoJsonSchema implements MongoJsonSchema { private final JsonSchemaObject root; - @Nullable // - private final Document encryptionMetadata; + private final @Nullable Document encryptionMetadata; DefaultMongoJsonSchema(JsonSchemaObject root) { this(root, null); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java index 26dbd7dffb..503d591d99 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/IdentifiableJsonSchemaProperty.java @@ -23,6 +23,7 @@ import java.util.UUID; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.mongodb.core.EncryptionAlgorithms; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; @@ -33,7 +34,7 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -97,6 +98,7 @@ public static class UntypedJsonSchemaProperty extends IdentifiableJsonSchemaProp * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -106,6 +108,7 @@ public UntypedJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -115,6 +118,7 @@ public UntypedJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -124,6 +128,7 @@ public UntypedJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -133,6 +138,7 @@ public UntypedJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty possibleValues(Collection possibleValues) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -142,6 +148,7 @@ public UntypedJsonSchemaProperty possibleValues(Collection possibleValue * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty allOf(Collection allOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -151,6 +158,7 @@ public UntypedJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty anyOf(Collection anyOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -160,6 +168,7 @@ public UntypedJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty oneOf(Collection oneOf) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -169,6 +178,7 @@ public UntypedJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -178,6 +188,7 @@ public UntypedJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#description(String) */ + @Contract("_ -> new") public UntypedJsonSchemaProperty description(String description) { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -186,6 +197,7 @@ public UntypedJsonSchemaProperty description(String description) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public UntypedJsonSchemaProperty generatedDescription() { return new UntypedJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -213,6 +225,7 @@ public static class StringJsonSchemaProperty extends IdentifiableJsonSchemaPrope * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#minLength(int) */ + @Contract("_ -> new") public StringJsonSchemaProperty minLength(int length) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minLength(length)); } @@ -222,6 +235,7 @@ public StringJsonSchemaProperty minLength(int length) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#maxLength(int) */ + @Contract("_ -> new") public StringJsonSchemaProperty maxLength(int length) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxLength(length)); } @@ -231,6 +245,7 @@ public StringJsonSchemaProperty maxLength(int length) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#matching(String) */ + @Contract("_ -> new") public StringJsonSchemaProperty matching(String pattern) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.matching(pattern)); } @@ -240,6 +255,7 @@ public StringJsonSchemaProperty matching(String pattern) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty possibleValues(String... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -249,6 +265,7 @@ public StringJsonSchemaProperty possibleValues(String... possibleValues) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -258,6 +275,7 @@ public StringJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -267,6 +285,7 @@ public StringJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -276,6 +295,7 @@ public StringJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty possibleValues(Collection possibleValues) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -285,6 +305,7 @@ public StringJsonSchemaProperty possibleValues(Collection possibleValues * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty allOf(Collection allOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -294,6 +315,7 @@ public StringJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty anyOf(Collection anyOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -303,6 +325,7 @@ public StringJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public StringJsonSchemaProperty oneOf(Collection oneOf) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -312,6 +335,7 @@ public StringJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -321,6 +345,7 @@ public StringJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#description(String) */ + @Contract("_ -> new") public StringJsonSchemaProperty description(String description) { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -329,6 +354,7 @@ public StringJsonSchemaProperty description(String description) { * @return new instance of {@link StringJsonSchemaProperty}. * @see StringJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public StringJsonSchemaProperty generatedDescription() { return new StringJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -355,6 +381,7 @@ public static class ObjectJsonSchemaProperty extends IdentifiableJsonSchemaPrope * @param range must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaProperty}. */ + @Contract("_ -> new") public ObjectJsonSchemaProperty propertiesCount(Range range) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.propertiesCount(range)); } @@ -364,6 +391,7 @@ public ObjectJsonSchemaProperty propertiesCount(Range range) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#minProperties(int) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty minProperties(int count) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minProperties(count)); } @@ -373,6 +401,7 @@ public ObjectJsonSchemaProperty minProperties(int count) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#maxProperties(int) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty maxProperties(int count) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxProperties(count)); } @@ -382,6 +411,7 @@ public ObjectJsonSchemaProperty maxProperties(int count) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#required(String...) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty required(String... properties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.required(properties)); } @@ -391,6 +421,7 @@ public ObjectJsonSchemaProperty required(String... properties) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#additionalProperties(boolean) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertiesAllowed) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalProperties(additionalPropertiesAllowed)); @@ -401,6 +432,7 @@ public ObjectJsonSchemaProperty additionalProperties(boolean additionalPropertie * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject additionalProperties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalProperties(additionalProperties)); @@ -411,6 +443,7 @@ public ObjectJsonSchemaProperty additionalProperties(ObjectJsonSchemaObject addi * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.properties(properties)); } @@ -420,6 +453,7 @@ public ObjectJsonSchemaProperty properties(JsonSchemaProperty... properties) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(Arrays.asList(possibleValues)); } @@ -429,6 +463,7 @@ public ObjectJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -438,6 +473,7 @@ public ObjectJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -447,6 +483,7 @@ public ObjectJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -456,6 +493,7 @@ public ObjectJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty possibleValues(Collection possibleValues) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -465,6 +503,7 @@ public ObjectJsonSchemaProperty possibleValues(Collection possibleValues * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty allOf(Collection allOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -474,6 +513,7 @@ public ObjectJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty anyOf(Collection anyOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -483,6 +523,7 @@ public ObjectJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty oneOf(Collection oneOf) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -492,6 +533,7 @@ public ObjectJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -501,6 +543,7 @@ public ObjectJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#description(String) */ + @Contract("_ -> new") public ObjectJsonSchemaProperty description(String description) { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -509,6 +552,7 @@ public ObjectJsonSchemaProperty description(String description) { * @return new instance of {@link ObjectJsonSchemaProperty}. * @see ObjectJsonSchemaObject#generateDescription() */ + @Contract("_ -> new") public ObjectJsonSchemaProperty generatedDescription() { return new ObjectJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.generatedDescription()); } @@ -540,6 +584,7 @@ public NumericJsonSchemaProperty(String identifier, NumericJsonSchemaObject sche * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#multipleOf */ + @Contract("_ -> new") public NumericJsonSchemaProperty multipleOf(Number value) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.multipleOf(value)); } @@ -549,6 +594,7 @@ public NumericJsonSchemaProperty multipleOf(Number value) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#within(Range) */ + @Contract("_ -> new") public NumericJsonSchemaProperty within(Range range) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.within(range)); } @@ -558,6 +604,7 @@ public NumericJsonSchemaProperty within(Range range) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#gt(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty gt(Number min) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gt(min)); } @@ -567,6 +614,7 @@ public NumericJsonSchemaProperty gt(Number min) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#gte(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty gte(Number min) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.gte(min)); } @@ -576,6 +624,7 @@ public NumericJsonSchemaProperty gte(Number min) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#lt(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty lt(Number max) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lt(max)); } @@ -585,6 +634,7 @@ public NumericJsonSchemaProperty lt(Number max) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#lte(Number) */ + @Contract("_ -> new") public NumericJsonSchemaProperty lte(Number max) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.lte(max)); } @@ -594,6 +644,7 @@ public NumericJsonSchemaProperty lte(Number max) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty possibleValues(Number... possibleValues) { return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues))); } @@ -603,6 +654,7 @@ public NumericJsonSchemaProperty possibleValues(Number... possibleValues) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(Arrays.asList(allOf)); } @@ -612,6 +664,7 @@ public NumericJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -621,6 +674,7 @@ public NumericJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -630,6 +684,7 @@ public NumericJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty possibleValues(Collection possibleValues) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -639,6 +694,7 @@ public NumericJsonSchemaProperty possibleValues(Collection possibleValue * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty allOf(Collection allOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -648,6 +704,7 @@ public NumericJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty anyOf(Collection anyOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -657,6 +714,7 @@ public NumericJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public NumericJsonSchemaProperty oneOf(Collection oneOf) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -666,6 +724,7 @@ public NumericJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -675,6 +734,7 @@ public NumericJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see NumericJsonSchemaObject#description(String) */ + @Contract("_ -> new") public NumericJsonSchemaProperty description(String description) { return new NumericJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -710,6 +770,7 @@ public ArrayJsonSchemaProperty(String identifier, ArrayJsonSchemaObject schemaOb * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#uniqueItems(boolean) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.uniqueItems(uniqueItems)); } @@ -719,6 +780,7 @@ public ArrayJsonSchemaProperty uniqueItems(boolean uniqueItems) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#range(Range) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty range(Range range) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.range(range)); } @@ -728,6 +790,7 @@ public ArrayJsonSchemaProperty range(Range range) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#minItems(int) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty minItems(int count) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.minItems(count)); } @@ -737,6 +800,7 @@ public ArrayJsonSchemaProperty minItems(int count) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#maxItems(int) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty maxItems(int count) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.maxItems(count)); } @@ -746,6 +810,7 @@ public ArrayJsonSchemaProperty maxItems(int count) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#items(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty items(JsonSchemaObject... items) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(Arrays.asList(items))); } @@ -755,6 +820,7 @@ public ArrayJsonSchemaProperty items(JsonSchemaObject... items) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#items(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty items(Collection items) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.items(items)); } @@ -764,6 +830,7 @@ public ArrayJsonSchemaProperty items(Collection items) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#additionalItems(boolean) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.additionalItems(additionalItemsAllowed)); } @@ -773,6 +840,7 @@ public ArrayJsonSchemaProperty additionalItems(boolean additionalItemsAllowed) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) { return possibleValues(new LinkedHashSet<>(Arrays.asList(possibleValues))); } @@ -782,6 +850,7 @@ public ArrayJsonSchemaProperty possibleValues(Object... possibleValues) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) { return allOf(new LinkedHashSet<>(Arrays.asList(allOf))); } @@ -791,6 +860,7 @@ public ArrayJsonSchemaProperty allOf(JsonSchemaObject... allOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { return anyOf(new LinkedHashSet<>(Arrays.asList(anyOf))); } @@ -800,6 +870,7 @@ public ArrayJsonSchemaProperty anyOf(JsonSchemaObject... anyOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { return oneOf(new LinkedHashSet<>(Arrays.asList(oneOf))); } @@ -809,6 +880,7 @@ public ArrayJsonSchemaProperty oneOf(JsonSchemaObject... oneOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty possibleValues(Collection possibleValues) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.possibleValues(possibleValues)); } @@ -818,6 +890,7 @@ public ArrayJsonSchemaProperty possibleValues(Collection possibleValues) * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty allOf(Collection allOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.allOf(allOf)); } @@ -827,6 +900,7 @@ public ArrayJsonSchemaProperty allOf(Collection allOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty anyOf(Collection anyOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.anyOf(anyOf)); } @@ -836,6 +910,7 @@ public ArrayJsonSchemaProperty anyOf(Collection anyOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty oneOf(Collection oneOf) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.oneOf(oneOf)); } @@ -845,6 +920,7 @@ public ArrayJsonSchemaProperty oneOf(Collection oneOf) { * @return new instance of {@link ArrayJsonSchemaProperty}. * @see ArrayJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.notMatch(notMatch)); } @@ -854,6 +930,7 @@ public ArrayJsonSchemaProperty notMatch(JsonSchemaObject notMatch) { * @return new instance of {@link NumericJsonSchemaProperty}. * @see ArrayJsonSchemaObject#description(String) */ + @Contract("_ -> new") public ArrayJsonSchemaProperty description(String description) { return new ArrayJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -884,6 +961,7 @@ public static class BooleanJsonSchemaProperty extends IdentifiableJsonSchemaProp * @return new instance of {@link NumericJsonSchemaProperty}. * @see BooleanJsonSchemaObject#description(String) */ + @Contract("_ -> new") public BooleanJsonSchemaProperty description(String description) { return new BooleanJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -914,6 +992,7 @@ public static class NullJsonSchemaProperty extends IdentifiableJsonSchemaPropert * @return new instance of {@link NullJsonSchemaProperty}. * @see NullJsonSchemaObject#description(String) */ + @Contract("_ -> new") public NullJsonSchemaProperty description(String description) { return new NullJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -944,6 +1023,7 @@ public static class DateJsonSchemaProperty extends IdentifiableJsonSchemaPropert * @return new instance of {@link DateJsonSchemaProperty}. * @see DateJsonSchemaProperty#description(String) */ + @Contract("_ -> new") public DateJsonSchemaProperty description(String description) { return new DateJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -974,6 +1054,7 @@ public static class TimestampJsonSchemaProperty extends IdentifiableJsonSchemaPr * @return new instance of {@link TimestampJsonSchemaProperty}. * @see TimestampJsonSchemaProperty#description(String) */ + @Contract("_ -> new") public TimestampJsonSchemaProperty description(String description) { return new TimestampJsonSchemaProperty(identifier, jsonSchemaObjectDelegate.description(description)); } @@ -1085,6 +1166,7 @@ public static EncryptedJsonSchemaProperty rangeEncrypted(JsonSchemaProperty targ * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("-> new") public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Random); } @@ -1094,6 +1176,7 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_random() { * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("-> new") public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() { return algorithm(EncryptionAlgorithms.AEAD_AES_256_CBC_HMAC_SHA_512_Deterministic); } @@ -1103,6 +1186,7 @@ public EncryptedJsonSchemaProperty aead_aes_256_cbc_hmac_sha_512_deterministic() * * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty algorithm(String algorithm) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, keyIds); } @@ -1111,6 +1195,7 @@ public EncryptedJsonSchemaProperty algorithm(String algorithm) { * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keyId(String keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, keyId, null); } @@ -1128,6 +1213,7 @@ public EncryptedJsonSchemaProperty keyId(Object keyId) { * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keys(UUID... keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId)); } @@ -1136,6 +1222,7 @@ public EncryptedJsonSchemaProperty keys(UUID... keyId) { * @param keyId must not be {@literal null}. * @return new instance of {@link EncryptedJsonSchemaProperty}. */ + @Contract("_ -> new") public EncryptedJsonSchemaProperty keys(Object... keyId) { return new EncryptedJsonSchemaProperty(targetProperty, algorithm, null, Arrays.asList(keyId)); } @@ -1180,8 +1267,8 @@ public Set getTypes() { return targetProperty.getTypes(); } - @Nullable - private Type extractPropertyType(Document source) { + + private @Nullable Type extractPropertyType(Document source) { if (source.containsKey("type")) { return Type.of(source.get("type", String.class)); @@ -1193,7 +1280,7 @@ private Type extractPropertyType(Document source) { return null; } - public Object getKeyId() { + public @Nullable Object getKeyId() { if (keyId != null) { return keyId; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java index a84f361d37..24a40efa5b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaObject.java @@ -31,6 +31,7 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ArrayJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.BooleanJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.DateJsonSchemaObject; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.StringJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.TimestampJsonSchemaObject; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java index f64218cc56..87c46d63dc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/MongoJsonSchema.java @@ -23,8 +23,9 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -212,7 +213,7 @@ interface Path { /** * @return the name of the currently processed element */ - String currentElement(); + @Nullable String currentElement(); /** * @return the path leading to the currently processed element in dot {@literal '.'} notation. @@ -285,11 +286,11 @@ static Resolution ofValue(Path path, Object value) { * @param value the value to apply. * @return */ - static Resolution ofValue(String key, Object value) { + static Resolution ofValue(@Nullable String key, Object value) { return new Resolution() { @Override - public String getKey() { + public @Nullable String getKey() { return key; } @@ -311,8 +312,7 @@ class MongoJsonSchemaBuilder { private ObjectJsonSchemaObject root; - @Nullable // - private Document encryptionMetadata; + private @Nullable Document encryptionMetadata; MongoJsonSchemaBuilder() { root = new ObjectJsonSchemaObject(); @@ -323,6 +323,7 @@ class MongoJsonSchemaBuilder { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#minProperties(int) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder minProperties(int count) { root = root.minProperties(count); @@ -334,6 +335,7 @@ public MongoJsonSchemaBuilder minProperties(int count) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#maxProperties(int) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder maxProperties(int count) { root = root.maxProperties(count); @@ -345,6 +347,7 @@ public MongoJsonSchemaBuilder maxProperties(int count) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#required(String...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder required(String... properties) { root = root.required(properties); @@ -356,6 +359,7 @@ public MongoJsonSchemaBuilder required(String... properties) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(boolean) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesAllowed) { root = root.additionalProperties(additionalPropertiesAllowed); @@ -367,6 +371,7 @@ public MongoJsonSchemaBuilder additionalProperties(boolean additionalPropertiesA * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#additionalProperties(ObjectJsonSchemaObject) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema) { root = root.additionalProperties(schema); @@ -378,6 +383,7 @@ public MongoJsonSchemaBuilder additionalProperties(ObjectJsonSchemaObject schema * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#properties(JsonSchemaProperty...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { root = root.properties(properties); @@ -389,6 +395,7 @@ public MongoJsonSchemaBuilder properties(JsonSchemaProperty... properties) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#patternProperties(JsonSchemaProperty...) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties) { root = root.patternProperties(properties); @@ -400,6 +407,7 @@ public MongoJsonSchemaBuilder patternProperties(JsonSchemaProperty... properties * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#property(JsonSchemaProperty) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { root = root.property(property); @@ -411,6 +419,7 @@ public MongoJsonSchemaBuilder property(JsonSchemaProperty property) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see ObjectJsonSchemaObject#possibleValues(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { root = root.possibleValues(possibleValues); @@ -422,6 +431,7 @@ public MongoJsonSchemaBuilder possibleValues(Set possibleValues) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#allOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder allOf(Set allOf) { root = root.allOf(allOf); @@ -433,6 +443,7 @@ public MongoJsonSchemaBuilder allOf(Set allOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#anyOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder anyOf(Set anyOf) { root = root.anyOf(anyOf); @@ -444,6 +455,7 @@ public MongoJsonSchemaBuilder anyOf(Set anyOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#oneOf(Collection) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder oneOf(Set oneOf) { root = root.oneOf(oneOf); @@ -455,6 +467,7 @@ public MongoJsonSchemaBuilder oneOf(Set oneOf) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#notMatch(JsonSchemaObject) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { root = root.notMatch(notMatch); @@ -466,6 +479,7 @@ public MongoJsonSchemaBuilder notMatch(JsonSchemaObject notMatch) { * @return {@code this} {@link MongoJsonSchemaBuilder}. * @see UntypedJsonSchemaObject#description(String) */ + @Contract("_ -> this") public MongoJsonSchemaBuilder description(String description) { root = root.description(description); @@ -487,6 +501,7 @@ public void encryptionMetadata(@Nullable Document encryptionMetadata) { * * @return new instance of {@link MongoJsonSchema}. */ + @Contract("-> new") public MongoJsonSchema build() { return new DefaultMongoJsonSchema(root, encryptionMetadata); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java index 95f116619f..87bdd8c618 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypeUnifyingMergeFunction.java @@ -22,10 +22,10 @@ import java.util.function.BiFunction; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Path; import org.springframework.data.mongodb.core.schema.MongoJsonSchema.ConflictResolutionFunction.Resolution; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -119,8 +119,7 @@ private static String getTypeKeyToUse(String key, Document source) { return key; } - @Nullable - private static Object getUnifiedExistingType(String key, Document source) { + private static @Nullable Object getUnifiedExistingType(String key, Document source) { return source.get(getTypeKeyToUse(key, source)); } @@ -155,7 +154,7 @@ public SimplePath append(String next) { } @Override - public String currentElement() { + public @Nullable String currentElement() { return CollectionUtils.lastElement(path); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java index abf8b0b8a2..7b299fd4d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/TypedJsonSchemaObject.java @@ -27,9 +27,10 @@ import java.util.stream.Collectors; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -100,6 +101,7 @@ public Set getTypes() { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject description(String description) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions); } @@ -110,6 +112,7 @@ public TypedJsonSchemaObject description(String description) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("-> new") public TypedJsonSchemaObject generatedDescription() { return new TypedJsonSchemaObject(types, description, true, restrictions); } @@ -121,6 +124,7 @@ public TypedJsonSchemaObject generatedDescription() { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject possibleValues(Collection possibleValues) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.possibleValues(possibleValues)); @@ -133,6 +137,7 @@ public TypedJsonSchemaObject possibleValues(Collection possibl * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject allOf(Collection allOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.allOf(allOf)); } @@ -144,6 +149,7 @@ public TypedJsonSchemaObject allOf(Collection allOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject anyOf(Collection anyOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.anyOf(anyOf)); } @@ -155,6 +161,7 @@ public TypedJsonSchemaObject anyOf(Collection anyOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject oneOf(Collection oneOf) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.oneOf(oneOf)); } @@ -166,6 +173,7 @@ public TypedJsonSchemaObject oneOf(Collection oneOf) { * @return new instance of {@link TypedJsonSchemaObject}. */ @Override + @Contract("_ -> new") public TypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new TypedJsonSchemaObject(types, description, generateDescription, restrictions.notMatch(notMatch)); } @@ -210,8 +218,7 @@ private Optional getOrCreateDescription() { * * @return can be {@literal null}. */ - @Nullable - protected String generateDescription() { + protected @Nullable String generateDescription() { return null; } @@ -264,6 +271,7 @@ public ObjectJsonSchemaObject propertiesCount(Range range) { * @param count the allowed minimal number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject minProperties(int count) { Bound upper = this.propertiesCount != null ? this.propertiesCount.getUpperBound() : Bound.unbounded(); @@ -276,6 +284,7 @@ public ObjectJsonSchemaObject minProperties(int count) { * @param count the allowed maximum number of properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject maxProperties(int count) { Bound lower = this.propertiesCount != null ? this.propertiesCount.getLowerBound() : Bound.unbounded(); @@ -288,6 +297,7 @@ public ObjectJsonSchemaObject maxProperties(int count) { * @param properties the names of required properties. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject required(String... properties) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -305,6 +315,7 @@ public ObjectJsonSchemaObject required(String... properties) { * @param additionalPropertiesAllowed * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesAllowed) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -319,6 +330,7 @@ public ObjectJsonSchemaObject additionalProperties(boolean additionalPropertiesA * @param schema must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -332,6 +344,7 @@ public ObjectJsonSchemaObject additionalProperties(ObjectJsonSchemaObject schema * @param properties must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -349,6 +362,7 @@ public ObjectJsonSchemaObject properties(JsonSchemaProperty... properties) { * @param regularExpressions must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExpressions) { ObjectJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -365,41 +379,49 @@ public ObjectJsonSchemaObject patternProperties(JsonSchemaProperty... regularExp * @param property must not be {@literal null}. * @return new instance of {@link ObjectJsonSchemaObject}. */ + @Contract("_ -> new") public ObjectJsonSchemaObject property(JsonSchemaProperty property) { return properties(property); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public ObjectJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -545,6 +567,7 @@ private NumericJsonSchemaObject(Set types, @Nullable String description, b * @param value must not be {@literal null}. * @return must not be {@literal null}. */ + @Contract("_ -> new") public NumericJsonSchemaObject multipleOf(Number value) { Assert.notNull(value, "Value must not be null"); @@ -561,6 +584,7 @@ public NumericJsonSchemaObject multipleOf(Number value) { * @param range must not be {@literal null}. * @return new instance of {@link NumericJsonSchemaObject}. */ + @Contract("_ -> new") public NumericJsonSchemaObject within(Range range) { Assert.notNull(range, "Range must not be null"); @@ -578,6 +602,7 @@ public NumericJsonSchemaObject within(Range range) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject gt(Number min) { Assert.notNull(min, "Min must not be null"); @@ -593,6 +618,7 @@ public NumericJsonSchemaObject gt(Number min) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject gte(Number min) { Assert.notNull(min, "Min must not be null"); @@ -608,6 +634,7 @@ public NumericJsonSchemaObject gte(Number min) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject lt(Number max) { Assert.notNull(max, "Max must not be null"); @@ -623,6 +650,7 @@ public NumericJsonSchemaObject lt(Number max) { * @return new instance of {@link NumericJsonSchemaObject}. */ @SuppressWarnings("unchecked") + @Contract("_ -> new") public NumericJsonSchemaObject lte(Number max) { Assert.notNull(max, "Max must not be null"); @@ -632,36 +660,43 @@ public NumericJsonSchemaObject lte(Number max) { } @Override + @Contract("_ -> new") public NumericJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public NumericJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -785,6 +820,7 @@ private StringJsonSchemaObject(@Nullable String description, boolean generateDes * @param range must not be {@literal null}. * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject length(Range range) { Assert.notNull(range, "Range must not be null"); @@ -801,6 +837,7 @@ public StringJsonSchemaObject length(Range range) { * @param length * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject minLength(int length) { Bound upper = this.length != null ? this.length.getUpperBound() : Bound.unbounded(); @@ -813,6 +850,7 @@ public StringJsonSchemaObject minLength(int length) { * @param length * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject maxLength(int length) { Bound lower = this.length != null ? this.length.getLowerBound() : Bound.unbounded(); @@ -825,6 +863,7 @@ public StringJsonSchemaObject maxLength(int length) { * @param pattern must not be {@literal null}. * @return new instance of {@link StringJsonSchemaObject}. */ + @Contract("_ -> new") public StringJsonSchemaObject matching(String pattern) { Assert.notNull(pattern, "Pattern must not be null"); @@ -836,36 +875,43 @@ public StringJsonSchemaObject matching(String pattern) { } @Override + @Contract("_ -> new") public StringJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public StringJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("-> new") public StringJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -946,6 +992,7 @@ private ArrayJsonSchemaObject(@Nullable String description, boolean generateDesc * @param uniqueItems * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -961,6 +1008,7 @@ public ArrayJsonSchemaObject uniqueItems(boolean uniqueItems) { * @param range must not be {@literal null}. Consider {@link Range#unbounded()} instead. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject range(Range range) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -975,6 +1023,7 @@ public ArrayJsonSchemaObject range(Range range) { * @param count the allowed minimal number of array items. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject minItems(int count) { Bound upper = this.range != null ? this.range.getUpperBound() : Bound.unbounded(); @@ -987,6 +1036,7 @@ public ArrayJsonSchemaObject minItems(int count) { * @param count the allowed maximal number of array items. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject maxItems(int count) { Bound lower = this.range != null ? this.range.getLowerBound() : Bound.unbounded(); @@ -999,6 +1049,7 @@ public ArrayJsonSchemaObject maxItems(int count) { * @param items the allowed items in the array. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject items(Collection items) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -1013,6 +1064,7 @@ public ArrayJsonSchemaObject items(Collection items) { * @param additionalItemsAllowed {@literal true} to allow additional items in the array, {@literal false} otherwise. * @return new instance of {@link ArrayJsonSchemaObject}. */ + @Contract("_ -> new") public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) { ArrayJsonSchemaObject newInstance = newInstance(description, generateDescription, restrictions); @@ -1022,36 +1074,43 @@ public ArrayJsonSchemaObject additionalItems(boolean additionalItemsAllowed) { } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject possibleValues(Collection possibleValues) { return newInstance(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject allOf(Collection allOf) { return newInstance(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject anyOf(Collection anyOf) { return newInstance(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject oneOf(Collection oneOf) { return newInstance(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return newInstance(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject description(String description) { return newInstance(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public ArrayJsonSchemaObject generatedDescription() { return newInstance(description, true, restrictions); } @@ -1147,41 +1206,49 @@ private BooleanJsonSchemaObject(@Nullable String description, boolean generateDe } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject possibleValues(Collection possibleValues) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject allOf(Collection allOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject anyOf(Collection anyOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject oneOf(Collection oneOf) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject description(String description) { return new BooleanJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("_ -> new") public BooleanJsonSchemaObject generatedDescription() { return new BooleanJsonSchemaObject(description, true, restrictions); } @Override + @Contract("-> new") protected String generateDescription() { return "Must be a boolean"; } @@ -1208,36 +1275,43 @@ private NullJsonSchemaObject(@Nullable String description, boolean generateDescr } @Override + @Contract("_ -> new") public NullJsonSchemaObject possibleValues(Collection possibleValues) { return new NullJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject allOf(Collection allOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject anyOf(Collection anyOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject oneOf(Collection oneOf) { return new NullJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new NullJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public NullJsonSchemaObject description(String description) { return new NullJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public NullJsonSchemaObject generatedDescription() { return new NullJsonSchemaObject(description, true, restrictions); } @@ -1268,36 +1342,43 @@ private DateJsonSchemaObject(@Nullable String description, boolean generateDescr } @Override + @Contract("_ -> new") public DateJsonSchemaObject possibleValues(Collection possibleValues) { return new DateJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject allOf(Collection allOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject anyOf(Collection anyOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject oneOf(Collection oneOf) { return new DateJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new DateJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public DateJsonSchemaObject description(String description) { return new DateJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public DateJsonSchemaObject generatedDescription() { return new DateJsonSchemaObject(description, true, restrictions); } @@ -1328,37 +1409,44 @@ private TimestampJsonSchemaObject(@Nullable String description, boolean generate } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject possibleValues(Collection possibleValues) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.possibleValues(possibleValues)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject allOf(Collection allOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.allOf(allOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject anyOf(Collection anyOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.anyOf(anyOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject oneOf(Collection oneOf) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.oneOf(oneOf)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions.notMatch(notMatch)); } @Override + @Contract("_ -> new") public TimestampJsonSchemaObject description(String description) { return new TimestampJsonSchemaObject(description, generateDescription, restrictions); } @Override + @Contract("-> new") public TimestampJsonSchemaObject generatedDescription() { return new TimestampJsonSchemaObject(description, true, restrictions); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java index 54ca29e0e3..d13f8d7985 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/UntypedJsonSchemaObject.java @@ -23,7 +23,8 @@ import java.util.stream.Collectors; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -69,6 +70,7 @@ public Set getTypes() { * @param description must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject description(String description) { return new UntypedJsonSchemaObject(restrictions, description, generateDescription); } @@ -78,6 +80,7 @@ public UntypedJsonSchemaObject description(String description) { * * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("-> new") public UntypedJsonSchemaObject generatedDescription() { return new UntypedJsonSchemaObject(restrictions, description, true); } @@ -88,6 +91,7 @@ public UntypedJsonSchemaObject generatedDescription() { * @param possibleValues must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject possibleValues(Collection possibleValues) { return new UntypedJsonSchemaObject(restrictions.possibleValues(possibleValues), description, generateDescription); } @@ -98,6 +102,7 @@ public UntypedJsonSchemaObject possibleValues(Collection possi * @param allOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject allOf(Collection allOf) { return new UntypedJsonSchemaObject(restrictions.allOf(allOf), description, generateDescription); } @@ -108,6 +113,7 @@ public UntypedJsonSchemaObject allOf(Collection allOf) { * @param anyOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject anyOf(Collection anyOf) { return new UntypedJsonSchemaObject(restrictions.anyOf(anyOf), description, generateDescription); } @@ -118,6 +124,7 @@ public UntypedJsonSchemaObject anyOf(Collection anyOf) { * @param oneOf must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject oneOf(Collection oneOf) { return new UntypedJsonSchemaObject(restrictions.oneOf(oneOf), description, generateDescription); } @@ -128,6 +135,7 @@ public UntypedJsonSchemaObject oneOf(Collection oneOf) { * @param notMatch must not be {@literal null}. * @return new instance of {@link TypedJsonSchemaObject}. */ + @Contract("_ -> new") public UntypedJsonSchemaObject notMatch(JsonSchemaObject notMatch) { return new UntypedJsonSchemaObject(restrictions.notMatch(notMatch), description, generateDescription); } @@ -163,8 +171,7 @@ private Optional getOrCreateDescription() { * * @return can be {@literal null}. */ - @Nullable - protected String generateDescription() { + protected @Nullable String generateDescription() { return null; } @@ -177,14 +184,14 @@ protected String generateDescription() { */ static class Restrictions { - private final Collection possibleValues; + private final Collection possibleValues; private final Collection allOf; private final Collection anyOf; private final Collection oneOf; private final @Nullable JsonSchemaObject notMatch; - Restrictions(Collection possibleValues, Collection allOf, - Collection anyOf, Collection oneOf, JsonSchemaObject notMatch) { + Restrictions(Collection possibleValues, Collection allOf, + Collection anyOf, Collection oneOf, @Nullable JsonSchemaObject notMatch) { this.possibleValues = possibleValues; this.allOf = allOf; @@ -206,7 +213,8 @@ static Restrictions empty() { * @param possibleValues must not be {@literal null}. * @return */ - Restrictions possibleValues(Collection possibleValues) { + @Contract("_ -> new") + Restrictions possibleValues(Collection possibleValues) { Assert.notNull(possibleValues, "PossibleValues must not be null"); return new Restrictions(possibleValues, allOf, anyOf, oneOf, notMatch); @@ -216,6 +224,7 @@ Restrictions possibleValues(Collection possibleValues) { * @param allOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions allOf(Collection allOf) { Assert.notNull(allOf, "AllOf must not be null"); @@ -226,6 +235,7 @@ Restrictions allOf(Collection allOf) { * @param anyOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions anyOf(Collection anyOf) { Assert.notNull(anyOf, "AnyOf must not be null"); @@ -236,6 +246,7 @@ Restrictions anyOf(Collection anyOf) { * @param oneOf must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions oneOf(Collection oneOf) { Assert.notNull(oneOf, "OneOf must not be null"); @@ -246,6 +257,7 @@ Restrictions oneOf(Collection oneOf) { * @param notMatch must not be {@literal null}. * @return */ + @Contract("_ -> new") Restrictions notMatch(JsonSchemaObject notMatch) { Assert.notNull(notMatch, "NotMatch must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java index 380d92af09..cdc583e038 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB-specific JSON schema implementation classes. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked @org.springframework.lang.NonNullFields package org.springframework.data.mongodb.core.schema; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java index 34eb8ea890..976b238fbd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/script/package-info.java @@ -3,6 +3,6 @@ * * @since 1.7 */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.core.script; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java index b4550ee8de..a5b4a2aabf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionNode.java @@ -18,13 +18,13 @@ import java.util.Collections; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.ast.Literal; import org.springframework.expression.spel.ast.MethodReference; import org.springframework.expression.spel.ast.Operator; import org.springframework.expression.spel.ast.OperatorNot; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -150,8 +150,7 @@ public boolean isLiteral() { * * @return */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return node.getValue(state); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java index 8869f51e09..89edd4eab2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformationContextSupport.java @@ -18,7 +18,7 @@ import java.util.List; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; /** @@ -67,8 +67,7 @@ public T getCurrentNode() { * * @return */ - @Nullable - public ExpressionNode getParentNode() { + public @Nullable ExpressionNode getParentNode() { return parentNode; } @@ -81,8 +80,7 @@ public ExpressionNode getParentNode() { * @see #addToPreviousOrReturn(Object) * @return */ - @Nullable - public Document getPreviousOperationObject() { + public @Nullable Document getPreviousOperationObject() { return previousOperationObject; } @@ -110,7 +108,7 @@ public boolean parentIsSameOperation() { * @param value * @return */ - public Document addToPreviousOperation(Object value) { + public Document addToPreviousOperation(@Nullable Object value) { Assert.state(previousOperationObject != null, "No previous operation available"); @@ -124,11 +122,14 @@ public Document addToPreviousOperation(Object value) { * @param value * @return */ - public Object addToPreviousOrReturn(Object value) { + public @Nullable Object addToPreviousOrReturn(@Nullable Object value) { return hasPreviousOperation() ? addToPreviousOperation(value) : value; } + @SuppressWarnings("unchecked") private List extractArgumentListFrom(Document context) { - return (List) context.get(context.keySet().iterator().next()); + + Object o = context.get(context.keySet().iterator().next()); + return o instanceof List l ? (List) l : List.of(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java index 512f753042..da5748f523 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/spel/ExpressionTransformer.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core.spel; +import org.jspecify.annotations.Nullable; + /** * SPI interface to implement components that can transform an {@link ExpressionTransformationContextSupport} into an * object. @@ -29,5 +31,5 @@ public interface ExpressionTransformer new") public Options contentType(String contentType) { Options target = new Options(new Document(metadata), chunkSize); @@ -121,6 +123,7 @@ public Options contentType(String contentType) { * @param metadata * @return new instance of {@link Options}. */ + @Contract("_ -> new") public Options metadata(Document metadata) { return new Options(metadata, chunkSize); } @@ -129,6 +132,7 @@ public Options metadata(Document metadata) { * @param chunkSize the file chunk size to use. * @return new instance of {@link Options}. */ + @Contract("_ -> new") public Options chunkSize(int chunkSize) { return new Options(metadata, chunkSize); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java index bf5a1d86e3..4878b431f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperations.java @@ -19,11 +19,11 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.GridFsUpload.GridFsUploadBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -181,8 +181,7 @@ default ObjectId store(InputStream content, @Nullable String filename, @Nullable * @param query must not be {@literal null}. * @return can be {@literal null}. */ - @Nullable - com.mongodb.client.gridfs.model.GridFSFile findOne(Query query); + com.mongodb.client.gridfs.model.@Nullable GridFSFile findOne(Query query); /** * Deletes all files matching the given {@link Query}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java index b3d3771f3c..9a5621dcbc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsOperationsSupport.java @@ -18,9 +18,9 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java index 0873432977..db6ce9833d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsResource.java @@ -21,10 +21,10 @@ import java.io.InputStream; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.InputStreamResource; import org.springframework.core.io.Resource; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.MongoGridFSException; @@ -105,6 +105,7 @@ public InputStream getInputStream() throws IOException, IllegalStateException { } @Override + @SuppressWarnings("NullAway") public long contentLength() throws IOException { verifyExists(); @@ -122,6 +123,7 @@ public boolean exists() { } @Override + @SuppressWarnings("NullAway") public long lastModified() throws IOException { verifyExists(); @@ -139,6 +141,7 @@ public String getDescription() { * @return never {@literal null}. * @throws IllegalStateException if the file does not {@link #exists()}. */ + @SuppressWarnings("NullAway") public Object getId() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); @@ -147,7 +150,8 @@ public Object getId() { } @Override - public Object getFileId() { + @SuppressWarnings("NullAway") + public @Nullable Object getFileId() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); return BsonUtils.toJavaType(getGridFSFile().getId()); @@ -157,8 +161,7 @@ public Object getFileId() { * @return the underlying {@link GridFSFile}. Can be {@literal null} if absent. * @since 2.2 */ - @Nullable - public GridFSFile getGridFSFile() { + public @Nullable GridFSFile getGridFSFile() { return this.file; } @@ -170,6 +173,7 @@ public GridFSFile getGridFSFile() { * provided via {@link GridFSFile}. * @throws IllegalStateException if the file does not {@link #exists()}. */ + @SuppressWarnings("NullAway") public String getContentType() { Assert.state(exists(), () -> String.format("%s does not exist.", getDescription())); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java index 8187c7dbc3..722a57edc1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsTemplate.java @@ -26,13 +26,13 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -167,7 +167,7 @@ public void delete(Query query) { } @Override - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java index 9f8d9a47d2..6f2b9ed85b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/GridFsUpload.java @@ -20,9 +20,9 @@ import org.bson.Document; import org.bson.types.ObjectId; - +import org.jspecify.annotations.Nullable; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -61,8 +61,7 @@ private GridFsUpload(@Nullable ID id, Lazy dataStream, String filen * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId() */ @Override - @Nullable - public ID getFileId() { + public @Nullable ID getFileId() { return id; } @@ -72,6 +71,7 @@ public String getFilename() { } @Override + @SuppressWarnings("NullAway") public InputStream getContent() { return dataStream.orElse(InputStream.nullInputStream()); } @@ -98,9 +98,9 @@ public static GridFsUploadBuilder fromStream(InputStream stream) { */ public static class GridFsUploadBuilder { - private Object id; - private Lazy dataStream; - private String filename; + private @Nullable Object id; + private @Nullable Lazy dataStream; + private @Nullable String filename; private Options options = Options.none(); private GridFsUploadBuilder() {} @@ -124,6 +124,7 @@ public GridFsUploadBuilder content(InputStream stream) { * @param stream the upload content. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder content(Supplier stream) { Assert.notNull(stream, "InputStream Supplier must not be null"); @@ -139,6 +140,8 @@ public GridFsUploadBuilder content(Supplier stream) { * @param * @return this. */ + @SuppressWarnings("unchecked") + @Contract("_ -> this") public GridFsUploadBuilder id(T1 id) { this.id = id; @@ -151,6 +154,7 @@ public GridFsUploadBuilder id(T1 id) { * @param filename the filename to use. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder filename(String filename) { this.filename = filename; @@ -163,6 +167,7 @@ public GridFsUploadBuilder filename(String filename) { * @param options must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder options(Options options) { Assert.notNull(options, "Options must not be null"); @@ -177,6 +182,7 @@ public GridFsUploadBuilder options(Options options) { * @param metadata must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder metadata(Document metadata) { this.options = this.options.metadata(metadata); @@ -189,6 +195,7 @@ public GridFsUploadBuilder metadata(Document metadata) { * @param chunkSize use negative number for default. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder chunkSize(int chunkSize) { this.options = this.options.chunkSize(chunkSize); @@ -201,6 +208,7 @@ public GridFsUploadBuilder chunkSize(int chunkSize) { * @param gridFSFile must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { Assert.notNull(gridFSFile, "GridFSFile must not be null"); @@ -219,13 +227,20 @@ public GridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { * @param contentType must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public GridFsUploadBuilder contentType(String contentType) { this.options = this.options.contentType(contentType); return this; } + @Contract("-> new") public GridFsUpload build() { + + Assert.notNull(dataStream, "DataStream must be set first"); + Assert.notNull(filename, "Filename must be set first"); + Assert.notNull(options, "Options must be set first"); + return new GridFsUpload(id, dataStream, filename, options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java index 9ee47e0bb9..f8a6bd804f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsOperations.java @@ -20,12 +20,12 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.gridfs.ReactiveGridFsUpload.ReactiveGridFsUploadBuilder; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java index aec7cadef1..e889ec7183 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsResource.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.bson.BsonValue; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; @@ -29,7 +30,6 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -115,7 +115,7 @@ public static ReactiveGridFsResource absent(String filename) { } @Override - public Object getFileId() { + public @Nullable Object getFileId() { return id instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : id; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java index 305e55aee4..092f81d1fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsTemplate.java @@ -26,6 +26,7 @@ import org.bson.BsonValue; import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; @@ -37,7 +38,6 @@ import org.springframework.data.mongodb.core.query.SerializationUtils; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java index 2f16c3b06e..09ea77798c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/ReactiveGridFsUpload.java @@ -17,9 +17,10 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import com.mongodb.client.gridfs.model.GridFSFile; @@ -58,8 +59,7 @@ private ReactiveGridFsUpload(@Nullable ID id, Publisher dataStream, * @see org.springframework.data.mongodb.gridfs.GridFsObject#getFileId() */ @Override - @Nullable - public ID getFileId() { + public @Nullable ID getFileId() { return id; } @@ -96,8 +96,8 @@ public static ReactiveGridFsUploadBuilder fromPublisher(Publisher { private @Nullable Object id; - private Publisher dataStream; - private String filename; + private @Nullable Publisher dataStream; + private @Nullable String filename; private Options options = Options.none(); private ReactiveGridFsUploadBuilder() {} @@ -108,6 +108,7 @@ private ReactiveGridFsUploadBuilder() {} * @param source the upload content. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder content(Publisher source) { this.dataStream = source; return this; @@ -120,6 +121,7 @@ public ReactiveGridFsUploadBuilder content(Publisher source) { * @param * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder id(T1 id) { this.id = id; @@ -132,6 +134,7 @@ public ReactiveGridFsUploadBuilder id(T1 id) { * @param filename the filename to use. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder filename(String filename) { this.filename = filename; @@ -144,6 +147,7 @@ public ReactiveGridFsUploadBuilder filename(String filename) { * @param options must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder options(Options options) { Assert.notNull(options, "Options must not be null"); @@ -156,8 +160,9 @@ public ReactiveGridFsUploadBuilder options(Options options) { * Set the file metadata. * * @param metadata must not be {@literal null}. - * @return + * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder metadata(Document metadata) { this.options = this.options.metadata(metadata); @@ -168,8 +173,9 @@ public ReactiveGridFsUploadBuilder metadata(Document metadata) { * Set the upload chunk size in bytes. * * @param chunkSize use negative number for default. - * @return + * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder chunkSize(int chunkSize) { this.options = this.options.chunkSize(chunkSize); @@ -182,6 +188,7 @@ public ReactiveGridFsUploadBuilder chunkSize(int chunkSize) { * @param gridFSFile must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { Assert.notNull(gridFSFile, "GridFSFile must not be null"); @@ -200,13 +207,20 @@ public ReactiveGridFsUploadBuilder gridFsFile(GridFSFile gridFSFile) { * @param contentType must not be {@literal null}. * @return this. */ + @Contract("_ -> this") public ReactiveGridFsUploadBuilder contentType(String contentType) { this.options = this.options.contentType(contentType); return this; } + @Contract("-> new") public ReactiveGridFsUpload build() { + + Assert.notNull(dataStream, "DataStream must be set first"); + Assert.notNull(filename, "Filename must be set first"); + Assert.notNull(options, "Options must be set first"); + return new ReactiveGridFsUpload(id, dataStream, filename, options); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java index 2f3b5af150..57726d69cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/gridfs/package-info.java @@ -1,6 +1,6 @@ /** * Support for MongoDB GridFS feature. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.gridfs; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java new file mode 100644 index 0000000000..40073d6022 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java @@ -0,0 +1,6 @@ +/** + * MongoDB specific JMX monitoring support. + */ +@org.jspecify.annotations.NullMarked +package org.springframework.data.mongodb.monitor; + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 8c7c6a55c4..550a71b301 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -18,6 +18,7 @@ import io.micrometer.common.KeyValues; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -64,6 +65,10 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { .and(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue(context.getCollectionName())); } + if(context.getCommandStartedEvent() == null) { + throw new IllegalStateException("not command started event present"); + } + ConnectionDescription connectionDescription = context.getCommandStartedEvent().getConnectionDescription(); if (connectionDescription != null) { @@ -98,6 +103,8 @@ public String getContextualName(MongoHandlerContext context) { String collectionName = context.getCollectionName(); CommandStartedEvent commandStartedEvent = context.getCommandStartedEvent(); + Assert.notNull(commandStartedEvent, "CommandStartedEvent must not be null"); + if (ObjectUtils.isEmpty(collectionName)) { return commandStartedEvent.getCommandName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java index 854e1481fc..6185c95db5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MapRequestContext.java @@ -17,9 +17,11 @@ import java.util.HashMap; import java.util.Map; +import java.util.NoSuchElementException; import java.util.stream.Stream; import com.mongodb.RequestContext; +import org.jspecify.annotations.Nullable; /** * A {@link Map}-based {@link RequestContext}. @@ -42,7 +44,13 @@ public MapRequestContext(Map context) { @Override public T get(Object key) { - return (T) map.get(key); + + + T value = (T) map.get(key); + if(value != null) { + return value; + } + throw new NoSuchElementException("%s is missing".formatted(key)); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java index cc58aac56e..cab9cd5cb8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoHandlerContext.java @@ -25,8 +25,7 @@ import org.bson.BsonDocument; import org.bson.BsonValue; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import com.mongodb.ConnectionString; import com.mongodb.RequestContext; @@ -55,12 +54,12 @@ public class MongoHandlerContext extends SenderContext { "killCursors", "listIndexes", "reIndex")); private final @Nullable ConnectionString connectionString; - private final CommandStartedEvent commandStartedEvent; - private final RequestContext requestContext; - private final String collectionName; + private final @Nullable CommandStartedEvent commandStartedEvent; + private final @Nullable RequestContext requestContext; + private final @Nullable String collectionName; - private CommandSucceededEvent commandSucceededEvent; - private CommandFailedEvent commandFailedEvent; + private @Nullable CommandSucceededEvent commandSucceededEvent; + private @Nullable CommandFailedEvent commandFailedEvent; public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandStartedEvent commandStartedEvent, RequestContext requestContext) { @@ -72,28 +71,27 @@ public MongoHandlerContext(@Nullable ConnectionString connectionString, CommandS this.collectionName = getCollectionName(commandStartedEvent); } - public CommandStartedEvent getCommandStartedEvent() { + public @Nullable CommandStartedEvent getCommandStartedEvent() { return this.commandStartedEvent; } - public RequestContext getRequestContext() { + public @Nullable RequestContext getRequestContext() { return this.requestContext; } public String getDatabaseName() { - return commandStartedEvent.getDatabaseName(); + return commandStartedEvent != null ? commandStartedEvent.getDatabaseName() : "n/a"; } - public String getCollectionName() { + public @Nullable String getCollectionName() { return this.collectionName; } public String getCommandName() { - return commandStartedEvent.getCommandName(); + return commandStartedEvent != null ? commandStartedEvent.getCommandName() : "n/a"; } - @Nullable - public ConnectionString getConnectionString() { + public @Nullable ConnectionString getConnectionString() { return connectionString; } @@ -135,8 +133,7 @@ private static String getCollectionName(CommandStartedEvent event) { * * @return trimmed string from {@code bsonValue} or null if the trimmed string was empty or the value wasn't a string */ - @Nullable - private static String getNonEmptyBsonString(@Nullable BsonValue bsonValue) { + private static @Nullable String getNonEmptyBsonString(@Nullable BsonValue bsonValue) { if (bsonValue == null || !bsonValue.isString()) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java index 9360a95de2..914396ab96 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/MongoObservationCommandListener.java @@ -23,7 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import com.mongodb.ConnectionString; @@ -197,8 +197,7 @@ private void doInObservation(@Nullable RequestContext requestContext, * @param context * @return */ - @Nullable - private static Observation observationFromContext(RequestContext context) { + private static @Nullable Observation observationFromContext(RequestContext context) { Observation observation = context.getOrDefault(ObservationThreadLocalAccessor.KEY, null); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java index d240e12f9e..d6319e5f4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/package-info.java @@ -1,5 +1,5 @@ /** * Infrastructure to provide driver observability using Micrometer. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.observability; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java index 900342bbcb..989655f4a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/package-info.java @@ -1,5 +1,5 @@ /** * Spring Data's MongoDB abstraction. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java index b1ba6ea3f0..00ff498731 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/RepositoryRuntimeHints.java @@ -19,6 +19,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -28,7 +29,6 @@ import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; import org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor; import org.springframework.data.querydsl.QuerydslUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java index 9016519d9b..750cc38678 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/package-info.java @@ -1,5 +1,5 @@ /** * Ahead-Of-Time processors for MongoDB repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.aot; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java index a2cbf659dd..db7edc05bd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/cdi/package-info.java @@ -1,6 +1,6 @@ /** * CDI support for MongoDB specific repository implementation. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.cdi; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java index d0d9b07081..e276d4d1e0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/package-info.java @@ -1,6 +1,6 @@ /** * Support infrastructure for the configuration of MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.config; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java index 8deddfe939..799597e19c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/package-info.java @@ -1,6 +1,6 @@ /** * MongoDB specific repository implementation. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index 910665253d..e160fd879a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -20,7 +20,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; - +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueEvaluationContextProvider; import org.springframework.data.expression.ValueExpression; import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -48,7 +48,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.Lazy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -106,7 +105,7 @@ public MongoQueryMethod getQueryMethod() { } @Override - public Object execute(Object[] parameters) { + public @Nullable Object execute(Object[] parameters) { ConvertingParameterAccessor accessor = new ConvertingParameterAccessor(operations.getConverter(), new MongoParametersParameterAccessor(method, parameters)); @@ -126,8 +125,7 @@ public Object execute(Object[] parameters) { * @param accessor for providing invocation arguments. Never {@literal null}. * @param typeToRead the desired component target type. Can be {@literal null}. */ - @Nullable - protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, @Nullable Class typeToRead) { Query query = createQuery(accessor); @@ -162,6 +160,7 @@ private Query applyAnnotatedReadPreferenceIfPresent(Query query) { return query.withReadPreference(com.mongodb.ReadPreference.valueOf(method.getAnnotatedReadPreference())); } + @SuppressWarnings("NullAway") private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { @@ -282,6 +281,7 @@ protected Query createCountQuery(ConvertingParameterAccessor accessor) { * @throws IllegalStateException if no update could be found. * @since 3.4 */ + @SuppressWarnings("NullAway") protected UpdateDefinition createUpdate(ConvertingParameterAccessor accessor) { if (accessor.getUpdate() != null) { @@ -375,6 +375,7 @@ protected ValueExpressionEvaluator getExpressionEvaluatorFor(MongoParameterAcces * @return the {@link CodecRegistry} used. * @since 2.4 */ + @SuppressWarnings("NullAway") protected CodecRegistry getCodecRegistry() { return operations.execute(MongoDatabase::getCodecRegistry); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java index 76b4b2e088..d363c93442 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractReactiveMongoQuery.java @@ -24,6 +24,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; @@ -57,7 +58,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -195,6 +195,7 @@ private ReactiveMongoQueryExecution getExecution(MongoParameterAccessor accessor return new ResultProcessingExecution(getExecutionToWrap(accessor, operation), resultProcessing); } + @SuppressWarnings("NullAway") private ReactiveMongoQueryExecution getExecutionToWrap(MongoParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { @@ -334,6 +335,7 @@ protected Mono createCountQuery(ConvertingParameterAccessor accessor) { * @throws IllegalStateException if no update could be found. * @since 3.4 */ + @SuppressWarnings("NullAway") protected Mono createUpdate(MongoParameterAccessor accessor) { if (accessor.getUpdate() != null) { @@ -425,7 +427,7 @@ ValueExpressionEvaluator getValueExpressionEvaluator(MongoParameterAccessor acce return new ValueExpressionEvaluator() { @Override - public T evaluate(String expressionString) { + public @Nullable T evaluate(String expressionString) { ValueExpression expression = valueExpressionDelegate.parse(expressionString); ValueEvaluationContext evaluationContext = valueEvaluationContextProvider .getEvaluationContext(accessor.getValues(), expression.getExpressionDependencies()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java index 6eb6a5da89..639c694ef9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AggregationUtils.java @@ -22,7 +22,7 @@ import java.util.function.LongUnaryOperator; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort.Order; import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -41,7 +41,6 @@ import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -166,8 +165,7 @@ static AggregationOptions computeOptions(MongoQueryMethod method, ConvertingPara * Prepares the AggregationPipeline including type discovery and calling {@link AggregationCallback} to run the * aggregation. */ - @Nullable - static T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor, + static @Nullable T doAggregate(AggregationPipeline pipeline, MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, Function evaluatorFunction, AggregationCallback callback) { @@ -308,8 +306,7 @@ static void appendLimitAndOffsetIfPresent(AggregationPipeline aggregationPipelin * @return can be {@literal null} if source {@link Document#isEmpty() is empty}. * @throws IllegalArgumentException when none of the above rules is met. */ - @Nullable - static T extractSimpleTypeResult(@Nullable Document source, Class targetType, MongoConverter converter) { + static @Nullable T extractSimpleTypeResult(@Nullable Document source, Class targetType, MongoConverter converter) { if (ObjectUtils.isEmpty(source)) { return null; @@ -336,9 +333,8 @@ static T extractSimpleTypeResult(@Nullable Document source, Class targetT String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson())); } - @Nullable @SuppressWarnings("unchecked") - private static T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, + private static @Nullable T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, Class targetType) { if (value == null) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java index 2aac6b77a8..108c6ee796 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/CollationUtils.java @@ -20,12 +20,12 @@ import java.util.regex.Pattern; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; -import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -55,8 +55,7 @@ private CollationUtils() { * @return can be {@literal null} if neither {@link ConvertingParameterAccessor#getCollation()} nor * {@literal collationExpression} are present. */ - @Nullable - static Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor, + static @Nullable Collation computeCollation(@Nullable String collationExpression, ConvertingParameterAccessor accessor, ValueExpressionEvaluator expressionEvaluator) { if (accessor.getCollation() != null) { @@ -98,6 +97,7 @@ static Collation computeCollation(@Nullable String collationExpression, Converti ObjectUtils.nullSafeClassName(placeholderValue))); } + Assert.notNull(placeholderValue, "PlaceholderValue must not be null"); return Collation.parse(collationExpression.replace(placeholder, placeholderValue.toString())); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index dbf87f2f2e..d075b67efe 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -21,6 +21,7 @@ import java.util.Iterator; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -35,7 +36,6 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -74,7 +74,7 @@ public PotentiallyConvertingIterator iterator() { } @Override - public ScrollPosition getScrollPosition() { + public @Nullable ScrollPosition getScrollPosition() { return delegate.getScrollPosition(); } @@ -87,34 +87,34 @@ public Sort getSort() { } @Override - public Class findDynamicProjection() { + public @Nullable Class findDynamicProjection() { return delegate.findDynamicProjection(); } - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { return getConvertedValue(delegate.getBindableValue(index), null); } @Override - public Range getDistanceRange() { + public @Nullable Range getDistanceRange() { return delegate.getDistanceRange(); } - public Point getGeoNearLocation() { + public @Nullable Point getGeoNearLocation() { return delegate.getGeoNearLocation(); } - public TextCriteria getFullText() { + public @Nullable TextCriteria getFullText() { return delegate.getFullText(); } @Override - public Collation getCollation() { + public @Nullable Collation getCollation() { return delegate.getCollation(); } @Override - public UpdateDefinition getUpdate() { + public @Nullable UpdateDefinition getUpdate() { return delegate.getUpdate(); } @@ -130,8 +130,7 @@ public Limit getLimit() { * @param typeInformation can be {@literal null}. * @return can be {@literal null}. */ - @Nullable - private Object getConvertedValue(Object value, @Nullable TypeInformation typeInformation) { + private @Nullable Object getConvertedValue(@Nullable Object value, @Nullable TypeInformation typeInformation) { return writer.convertToMongoType(value, typeInformation == null ? null : typeInformation.getActualType()); } @@ -161,11 +160,11 @@ public boolean hasNext() { return delegate.hasNext(); } - public Object next() { + public @Nullable Object next() { return delegate.next(); } - public Object nextConverted(MongoPersistentProperty property) { + public @Nullable Object nextConverted(MongoPersistentProperty property) { Object next = next(); @@ -228,7 +227,7 @@ private static Collection asCollection(@Nullable Object source) { } @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return delegate.getValues(); } @@ -244,6 +243,6 @@ public interface PotentiallyConvertingIterator extends Iterator { * * @return */ - Object nextConverted(MongoPersistentProperty property); + @Nullable Object nextConverted(MongoPersistentProperty property); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java index 8678e5a74c..c54d689b52 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoEntityInformation.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.repository.core.EntityInformation; -import org.springframework.lang.Nullable; /** * Mongo specific {@link EntityInformation}. @@ -58,8 +58,7 @@ default boolean isVersioned() { * @return can be {@literal null}. * @since 2.2 */ - @Nullable - default Object getVersion(T entity) { + default @Nullable Object getVersion(T entity) { return null; } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 5db853e810..00d748f8a9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; @@ -23,7 +24,6 @@ import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.lang.Nullable; /** * Mongo-specific {@link ParameterAccessor} exposing a maximum distance parameter. @@ -41,7 +41,7 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @return the maximum distance to apply to the geo query or {@literal null} if there's no {@link Distance} parameter * at all or the given value for it was {@literal null}. */ - Range getDistanceRange(); + @Nullable Range getDistanceRange(); /** * Returns the {@link Point} to use for a geo-near query. @@ -75,7 +75,7 @@ public interface MongoParameterAccessor extends ParameterAccessor { * @return * @since 1.8 */ - Object[] getValues(); + Object @Nullable[] getValues(); /** * Returns the {@link Update} to be used for an update execution. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index 1f66d5b77d..cb91ccd8e6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; @@ -36,7 +37,6 @@ import org.springframework.data.repository.query.Parameters; import org.springframework.data.repository.query.ParametersSource; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Custom extension of {@link Parameters} discovering additional @@ -53,9 +53,9 @@ public class MongoParameters extends Parameters private final int rangeIndex; private final int maxDistanceIndex; - private final @Nullable Integer fullTextIndex; - private final @Nullable Integer nearIndex; - private final @Nullable Integer collationIndex; + private final int fullTextIndex; + private final int nearIndex; + private final int collationIndex; private final int updateIndex; private final TypeInformation domainType; @@ -106,9 +106,8 @@ private MongoParameters(ParametersSource parametersSource, NearIndex nearIndex) this.nearIndex = nearIndex.nearIndex; } - private MongoParameters(List parameters, int maxDistanceIndex, @Nullable Integer nearIndex, - @Nullable Integer fullTextIndex, int rangeIndex, @Nullable Integer collationIndex, int updateIndex, - TypeInformation domainType) { + private MongoParameters(List parameters, int maxDistanceIndex, int nearIndex, int fullTextIndex, + int rangeIndex, int collationIndex, int updateIndex, TypeInformation domainType) { super(parameters); @@ -141,7 +140,7 @@ static boolean isGeoNearQuery(Method method) { static class NearIndex { - private final @Nullable Integer nearIndex; + private final int nearIndex; public NearIndex(ParametersSource parametersSource, boolean isGeoNearMethod) { @@ -226,7 +225,7 @@ public int getNearIndex() { * @since 1.6 */ public int getFullTextParameterIndex() { - return fullTextIndex != null ? fullTextIndex : -1; + return fullTextIndex; } /** @@ -234,7 +233,7 @@ public int getFullTextParameterIndex() { * @since 1.6 */ public boolean hasFullTextParameter() { - return this.fullTextIndex != null && this.fullTextIndex >= 0; + return this.fullTextIndex >= 0; } /** @@ -252,7 +251,7 @@ public int getRangeIndex() { * @since 2.2 */ public int getCollationParameterIndex() { - return collationIndex != null ? collationIndex : -1; + return collationIndex; } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index ac1931e10c..66529dfce9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Distance; @@ -24,7 +25,7 @@ import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParametersParameterAccessor; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -53,7 +54,8 @@ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values this.method = method; } - public Range getDistanceRange() { + @SuppressWarnings("NullAway") + public @Nullable Range getDistanceRange() { MongoParameters mongoParameters = method.getParameters(); @@ -70,7 +72,7 @@ public Range getDistanceRange() { return Range.of(Bound.unbounded(), maxDistance); } - public Point getGeoNearLocation() { + public @Nullable Point getGeoNearLocation() { int nearIndex = method.getParameters().getNearIndex(); @@ -95,14 +97,14 @@ public Point getGeoNearLocation() { return (Point) value; } - @Nullable @Override - public TextCriteria getFullText() { + public @Nullable TextCriteria getFullText() { int index = method.getParameters().getFullTextParameterIndex(); return index >= 0 ? potentiallyConvertFullText(getValue(index)) : null; } - protected TextCriteria potentiallyConvertFullText(Object fullText) { + @Contract("null -> fail") + protected TextCriteria potentiallyConvertFullText(@Nullable Object fullText) { Assert.notNull(fullText, "Fulltext parameter must not be 'null'."); @@ -124,7 +126,7 @@ protected TextCriteria potentiallyConvertFullText(Object fullText) { } @Override - public Collation getCollation() { + public @Nullable Collation getCollation() { if (method.getParameters().getCollationParameterIndex() == -1) { return null; @@ -134,12 +136,12 @@ public Collation getCollation() { } @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return super.getValues(); } @Override - public UpdateDefinition getUpdate() { + public @Nullable UpdateDefinition getUpdate() { int updateIndex = method.getParameters().getUpdateIndex(); return updateIndex == -1 ? null : (UpdateDefinition) getValue(updateIndex); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 66a8870623..7e327f4e20 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.BsonRegularExpression; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; @@ -52,7 +53,6 @@ import org.springframework.data.repository.query.parser.Part.Type; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -111,7 +111,7 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, protected Criteria create(Part part, Iterator iterator) { if (isGeoNearQuery && part.getType().equals(Type.NEAR)) { - return null; + return new Criteria(); } PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); @@ -141,7 +141,7 @@ protected Criteria or(Criteria base, Criteria criteria) { } @Override - protected Query complete(Criteria criteria, Sort sort) { + protected Query complete(@Nullable Criteria criteria, Sort sort) { Query query = (criteria == null ? new Query() : new Query(criteria)).with(sort); @@ -161,6 +161,7 @@ protected Query complete(Criteria criteria, Sort sort) { * @param parameters * @return */ + @SuppressWarnings("NullAway") private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator parameters) { Type type = part.getType(); @@ -333,6 +334,7 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro * @param value * @return the criteria extended with the regex. */ + @SuppressWarnings("NullAway") private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object value) { if (value == null) { @@ -348,8 +350,7 @@ private Criteria addAppropriateLikeRegexTo(Criteria criteria, Part part, Object * @param part * @return the regex options or {@literal null}. */ - @Nullable - private String toRegexOptions(Part part) { + private @Nullable String toRegexOptions(Part part) { String regexOptions = null; switch (part.shouldIgnoreCase()) { @@ -414,10 +415,11 @@ private Streamable asStreamable(Object value) { return Streamable.of(value); } - private String toLikeRegex(String source, Part part) { + private @Nullable String toLikeRegex(String source, Part part) { return MongoRegexCreator.INSTANCE.toRegularExpression(source, toMatchMode(part.getType())); } + @SuppressWarnings("NullAway") private boolean isSpherical(MongoPersistentProperty property) { if (property.isAnnotationPresent(GeoSpatialIndexed.class)) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index dd2b78de59..abdcf62930 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -18,6 +18,7 @@ import java.util.List; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -171,10 +171,12 @@ public Object execute(Query query) { return isListOfGeoResult(method.getReturnType()) ? results.getContent() : results; } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked","NullAway"}) GeoResults doExecuteQuery(Query query) { Point nearLocation = accessor.getGeoNearLocation(); + Assert.notNull(nearLocation, "[query.location] must not be null"); + NearQuery nearQuery = NearQuery.near(nearLocation); if (query != null) { @@ -182,6 +184,8 @@ GeoResults doExecuteQuery(Query query) { } Range distances = accessor.getDistanceRange(); + Assert.notNull(nearLocation, "[query.distance] must not be null"); + distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric())); @@ -267,7 +271,7 @@ public DeleteExecution(MongoOperations operations, MongoQueryMethod method) { } @Override - public Object execute(Query query) { + public @Nullable Object execute(Query query) { String collectionName = method.getEntityInformation().getCollectionName(); Class type = method.getEntityInformation().getJavaType(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index d3fe22b4ef..4bd6e7db5e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -21,6 +21,7 @@ import java.util.Optional; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.annotation.Collation; @@ -43,7 +44,6 @@ import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -461,7 +461,7 @@ public boolean hasAnnotatedUpdate() { * @return the {@link Update} or {@literal null} if not present. * @since 3.4 */ - public Update getUpdateSource() { + public @Nullable Update getUpdateSource() { return lookupUpdateAnnotation().orElse(null); } @@ -471,6 +471,7 @@ public Update getUpdateSource() { * @since 3.4 * @throws IllegalStateException */ + @SuppressWarnings("NullAway") public void verify() { if (isModifyingQuery()) { @@ -509,6 +510,7 @@ public void verify() { } } + @SuppressWarnings("NullAway") private boolean isNumericOrVoidReturnValue() { Class resultType = getReturnedObjectType(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index fdf08ba9d8..6116cc5534 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -78,6 +78,7 @@ public PartTree getTree() { } @Override + @SuppressWarnings("NullAway") protected Query createQuery(ConvertingParameterAccessor accessor) { MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java index 431510f11b..4b7262749a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/QueryUtils.java @@ -21,13 +21,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mapping.model.ValueExpressionEvaluator; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java index 324f01d61f..9534a9cf4f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoParameterAccessor.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -51,16 +52,21 @@ public ReactiveMongoParameterAccessor(MongoQueryMethod method, Object[] values) * @see org.springframework.data.mongodb.repository.query.MongoParametersParameterAccessor#getValues() */ @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { - Object[] result = new Object[super.getValues().length]; + Object[] values = super.getValues(); + if(values == null) { + return new Object[0]; + } + + Object[] result = new Object[values.length]; for (int i = 0; i < result.length; i++) { result[i] = getValue(i); } return result; } - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { return getValue(getParameters().getBindableParameter(index).getIndex()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index d18c6a989c..06f946d745 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.DtoInstantiatingConverter; @@ -37,7 +38,6 @@ import org.springframework.data.util.ReactiveWrappers; import org.springframework.data.util.ReflectionUtils; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -86,6 +86,8 @@ public Publisher execute(Query query, Class type, String co private Flux> doExecuteQuery(@Nullable Query query, Class type, String collection) { Point nearLocation = accessor.getGeoNearLocation(); + Assert.notNull(nearLocation, "[query.location] ist not present"); + NearQuery nearQuery = NearQuery.near(nearLocation); if (query != null) { @@ -93,6 +95,8 @@ private Flux> doExecuteQuery(@Nullable Query query, Class t } Range distances = accessor.getDistanceRange(); + + Assert.notNull(distances, "[query.range] ist not present"); distances.getUpperBound().getValue().ifPresent(it -> nearQuery.maxDistance(it).in(it.getMetric())); distances.getLowerBound().getValue().ifPresent(it -> nearQuery.minDistance(it).in(it.getMetric())); @@ -195,6 +199,7 @@ public ResultProcessingExecution(ReactiveMongoQueryExecution delegate, Converter } @Override + @SuppressWarnings("NullAway") public Publisher execute(Query query, Class type, String collection) { return (Publisher) converter.convert(delegate.execute(query, type, collection)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index b27adfab93..4aa773091b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -87,6 +87,7 @@ protected Mono createCountQuery(ConvertingParameterAccessor accessor) { return Mono.fromSupplier(() -> createQueryInternal(accessor, true)); } + @SuppressWarnings("NullAway") private Query createQueryInternal(ConvertingParameterAccessor accessor, boolean isCountQuery) { MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java index cf6e7231f8..ebc33cef96 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregation.java @@ -21,6 +21,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -32,7 +33,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.lang.Nullable; /** * A reactive {@link org.springframework.data.repository.query.RepositoryQuery} to use a plain JSON String to create an diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java index 562ee026fc..4bfe2ca39f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedMongoQuery.java @@ -15,12 +15,14 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; +import org.springframework.lang.Contract; import reactor.core.publisher.Mono; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.bson.Document; - +import org.jspecify.annotations.NonNull; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -30,7 +32,6 @@ import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.spel.ExpressionDependencies; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; /** @@ -46,7 +47,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery { private static final Log LOG = LogFactory.getLog(ReactiveStringBasedMongoQuery.class); private final String query; - private final String fieldSpec; + private final @Nullable String fieldSpec; private final ValueExpressionParser expressionParser; @@ -63,6 +64,7 @@ public class ReactiveStringBasedMongoQuery extends AbstractReactiveMongoQuery { * @param delegate must not be {@literal null}. * @since 4.4.0 */ + @SuppressWarnings("NullAway") public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) { this(method.getAnnotatedQuery(), method, mongoOperations, delegate); @@ -78,7 +80,8 @@ public ReactiveStringBasedMongoQuery(ReactiveMongoQueryMethod method, ReactiveMo * @param delegate must not be {@literal null}. * @since 4.4.0 */ - public ReactiveStringBasedMongoQuery(@NonNull String query, ReactiveMongoQueryMethod method, + @SuppressWarnings("NullAway") + public ReactiveStringBasedMongoQuery(String query, ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, ValueExpressionDelegate delegate) { super(method, mongoOperations, delegate); @@ -132,7 +135,7 @@ protected Mono createQuery(ConvertingParameterAccessor accessor) { }); } - private Mono getBindingContext(String json, ConvertingParameterAccessor accessor, + private Mono getBindingContext(@Nullable String json, ConvertingParameterAccessor accessor, ParameterBindingDocumentCodec codec) { ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java index 724c8f29ef..289b953b27 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringAggregationOperation.java @@ -20,9 +20,9 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; -import org.springframework.lang.Nullable; /** * String-based aggregation operation for a repository query method. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java index 5596435eb0..3f6a48e84c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedAggregation.java @@ -20,7 +20,7 @@ import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.SliceImpl; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; @@ -32,7 +32,6 @@ import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.util.ReflectionUtils; -import org.springframework.lang.Nullable; /** * {@link AbstractMongoQuery} implementation to run string-based aggregations using @@ -72,8 +71,7 @@ public StringBasedAggregation(MongoQueryMethod method, MongoOperations mongoOper @SuppressWarnings("unchecked") @Override - @Nullable - protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + protected @Nullable Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, @Nullable Class ignore) { return AggregationUtils.doAggregate(AggregationUtils.computePipeline(this, method, accessor), method, processor, diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java index 5e2fba381b..c990d3269d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/StringBasedMongoQuery.java @@ -55,6 +55,7 @@ public class StringBasedMongoQuery extends AbstractMongoQuery { * @param expressionSupport must not be {@literal null}. * @since 4.4.0 */ + @SuppressWarnings("NullAway") public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOperations, ValueExpressionDelegate expressionSupport) { this(method.getAnnotatedQuery(), method, mongoOperations, expressionSupport); @@ -70,6 +71,7 @@ public StringBasedMongoQuery(MongoQueryMethod method, MongoOperations mongoOpera * @param expressionSupport must not be {@literal null}. * @since 4.3 */ + @SuppressWarnings("NullAway") public StringBasedMongoQuery(String query, MongoQueryMethod method, MongoOperations mongoOperations, ValueExpressionDelegate expressionSupport) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java index c479f3faa9..360f5e80eb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ValueExpressionDelegateValueExpressionEvaluator.java @@ -17,6 +17,7 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; import org.springframework.data.mapping.model.ValueExpressionEvaluator; @@ -34,7 +35,7 @@ class ValueExpressionDelegateValueExpressionEvaluator implements ValueExpression @SuppressWarnings("unchecked") @Override - public T evaluate(String expressionString) { + public @Nullable T evaluate(String expressionString) { ValueExpression expression = delegate.parse(expressionString); return (T) expression.evaluate(expressionToContext.apply(expression)); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java index 20c77e22aa..5f0cc21049 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/package-info.java @@ -1,6 +1,6 @@ /** * Query derivation mechanism for MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.query; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java index f59a995170..abd828a9f7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java @@ -25,6 +25,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -32,7 +33,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryProxyPostProcessor; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -54,7 +54,7 @@ class CrudMethodMetadataPostProcessor implements RepositoryProxyPostProcessor, B private @Nullable ClassLoader classLoader = ClassUtils.getDefaultClassLoader(); @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { this.classLoader = classLoader; } @@ -121,7 +121,7 @@ static MethodInvocation currentInvocation() throws IllegalStateException { } @Override - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); @@ -220,7 +220,7 @@ public boolean isStatic() { } @Override - public Object getTarget() { + public @Nullable Object getTarget() { MethodInvocation invocation = CrudMethodMetadataPopulatingMethodInterceptor.currentInvocation(); return TransactionSynchronizationManager.getResource(invocation.getMethod()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java index 1d876289be..443108d2f0 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MappingMongoEntityInformation.java @@ -16,12 +16,12 @@ package org.springframework.data.mongodb.repository.support; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.PersistentPropertyAccessor; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.core.support.PersistentEntityInformation; -import org.springframework.lang.Nullable; /** * {@link MongoEntityInformation} implementation using a {@link MongoPersistentEntity} instance to lookup the necessary @@ -113,7 +113,7 @@ public boolean isVersioned() { } @Override - public Object getVersion(T entity) { + public @Nullable Object getVersion(T entity) { if (!isVersioned()) { return null; @@ -124,8 +124,7 @@ public Object getVersion(T entity) { return accessor.getProperty(this.entityMetadata.getRequiredVersionProperty()); } - @Nullable - public Collation getCollation() { + public @Nullable Collation getCollation() { return this.entityMetadata.getCollation(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java index 3c029ee5aa..6deee469e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoAnnotationProcessor.java @@ -22,9 +22,8 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.tools.Diagnostic; - +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; import com.querydsl.apt.AbstractQuerydslProcessor; import com.querydsl.apt.Configuration; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java index d0a3f7a1e4..1a39198757 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoEntityInformationSupport.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.support; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 91a3e39cbe..e1abcdc2ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.context.MappingContext; @@ -44,7 +45,6 @@ import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -77,14 +77,14 @@ public MongoRepositoryFactory(MongoOperations mongoOperations) { } @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { super.setBeanClassLoader(classLoader); crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } @Override - protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { return this.operations.getConverter().getProjectionFactory(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java index c98d38c5f5..cec54de0bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java @@ -17,13 +17,13 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -31,6 +31,7 @@ * * @author Oliver Gierke */ +@SuppressWarnings("NullAway") public class MongoRepositoryFactoryBean, S, ID extends Serializable> extends RepositoryFactoryBeanSupport { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java index ec845510ce..833ce69458 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutor.java @@ -23,6 +23,7 @@ import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -245,12 +246,12 @@ protected FluentQuerydsl create(Predicate predicate, Sort sort, int limit } @Override - public T oneValue() { + public @Nullable T oneValue() { return createQuery().fetchOne(); } @Override - public T firstValue() { + public @Nullable T firstValue() { return createQuery().fetchFirst(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index fe18fda758..ae8561bc17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -45,7 +46,6 @@ import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; import org.springframework.data.repository.query.RepositoryQuery; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -79,14 +79,15 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { } @Override - public void setBeanClassLoader(ClassLoader classLoader) { + public void setBeanClassLoader(@Nullable ClassLoader classLoader) { super.setBeanClassLoader(classLoader); crudMethodMetadataPostProcessor.setBeanClassLoader(classLoader); } @Override - protected ProjectionFactory getProjectionFactory(ClassLoader classLoader, BeanFactory beanFactory) { + @SuppressWarnings("NullAway") + protected ProjectionFactory getProjectionFactory(@Nullable ClassLoader classLoader, @Nullable BeanFactory beanFactory) { return this.operations.getConverter().getProjectionFactory(); } @@ -130,6 +131,7 @@ protected Object getTargetRepository(RepositoryInformation information) { } @Override + @SuppressWarnings("NullAway") protected Optional getQueryLookupStrategy(Key key, ValueExpressionDelegate valueExpressionDelegate) { return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index dfb7c00fe6..e3d71325f9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -17,13 +17,13 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.index.IndexOperationsAdapter; import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; import org.springframework.data.repository.core.support.RepositoryFactorySupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -78,6 +78,7 @@ public void setMappingContext(MappingContext mappingContext) { } @Override + @SuppressWarnings("NullAway") protected RepositoryFactorySupport createRepositoryFactory() { RepositoryFactorySupport factory = getFactoryInstance(operations); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java index cf5191fd42..a86ada0aad 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveSpringDataMongodbQuery.java @@ -25,6 +25,7 @@ import java.util.function.Consumer; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.ScrollPosition; @@ -36,7 +37,6 @@ import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.StringUtils; @@ -304,7 +304,7 @@ static class NoMatchException extends RuntimeException { @Override public synchronized Throwable fillInStackTrace() { - return null; + return this; } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java index 2f4c30ee7a..7e6e2fc82e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleMongoRepository.java @@ -27,6 +27,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.dao.OptimisticLockingFailureException; import org.springframework.data.domain.Example; import org.springframework.data.domain.Page; @@ -47,7 +48,6 @@ import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.StreamUtils; import org.springframework.data.util.Streamable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; @@ -427,12 +427,12 @@ protected FluentQueryByExample create(Example predicate, Sort sort, } @Override - public T oneValue() { + public @Nullable T oneValue() { return createQuery().oneValue(); } @Override - public T firstValue() { + public @Nullable T firstValue() { return createQuery().firstValue(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java index 1c1df2c9a1..7e4a3aa665 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SimpleReactiveMongoRepository.java @@ -30,6 +30,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -49,7 +50,6 @@ import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import com.mongodb.ReadPreference; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java index 0ef6c38744..24a9342ca1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuery.java @@ -22,7 +22,7 @@ import java.util.stream.Stream; import org.bson.Document; - +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; @@ -36,7 +36,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.support.PageableExecutionUtils; -import org.springframework.lang.Nullable; import com.mysema.commons.lang.CloseableIterator; import com.mysema.commons.lang.EmptyCloseableIterator; @@ -47,6 +46,7 @@ import com.querydsl.core.types.Expression; import com.querydsl.core.types.OrderSpecifier; import com.querydsl.core.types.Predicate; +import org.springframework.lang.Contract; /** * Spring Data specific simple {@link com.querydsl.core.Fetchable} {@link com.querydsl.core.SimpleQuery Query} @@ -200,7 +200,7 @@ public Slice fetchSlice(Pageable pageable) { } @Override - public T fetchFirst() { + public @Nullable T fetchFirst() { try { return find.matching(createQuery()).firstValue(); } catch (RuntimeException e) { @@ -209,7 +209,7 @@ public T fetchFirst() { } @Override - public T fetchOne() { + public @Nullable T fetchOne() { try { return find.matching(createQuery()).oneValue(); } catch (RuntimeException e) { @@ -279,7 +279,8 @@ protected List getIds(Class targetType, Predicate condition) { return mongoOperations.findDistinct(query, FieldName.ID.name(), targetType, Object.class); } - private static T handleException(RuntimeException e, T defaultValue) { + @Contract("_, !null -> !null") + private static @Nullable T handleException(RuntimeException e, @Nullable T defaultValue) { if (e.getClass().getName().endsWith("$NoResults")) { return defaultValue; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java index a64f666f3f..64ea5f2384 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbQuerySupport.java @@ -27,6 +27,8 @@ import com.querydsl.core.types.OrderSpecifier; import com.querydsl.mongodb.document.AbstractMongodbQuery; import com.querydsl.mongodb.document.MongodbDocumentSerializer; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; /** * Support query type to augment Spring Data-specific {@link #toString} representations and diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java index d9a550a0f7..756d04d0c2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/SpringDataMongodbSerializer.java @@ -20,6 +20,8 @@ import java.util.regex.Pattern; import org.bson.Document; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.QueryMapper; @@ -27,7 +29,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -48,6 +49,7 @@ * @author Christoph Strobl * @author Mark Paluch */ +@NullUnmarked class SpringDataMongodbSerializer extends MongodbDocumentSerializer { private static final String ID_KEY = FieldName.ID.name(); @@ -146,8 +148,7 @@ protected boolean isId(Path arg) { } @Override - @Nullable - protected Object convert(@Nullable Path path, @Nullable Constant constant) { + protected @Nullable Object convert(@Nullable Path path, @Nullable Constant constant) { if (constant == null) { return null; @@ -191,8 +192,7 @@ protected Object convert(@Nullable Path path, @Nullable Constant constant) return asReference(constant.getConstant(), path); } - @Nullable - private MongoPersistentProperty getPropertyFor(Path path) { + private @Nullable MongoPersistentProperty getPropertyFor(Path path) { Path parent = path.getMetadata().getParent(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java index 1d0b8beeba..42cd5a0b18 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/package-info.java @@ -1,6 +1,6 @@ /** * Support infrastructure for query derivation of MongoDB specific repositories. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.repository.support; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index cbbd4a37a9..83df5f7798 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -40,11 +40,12 @@ import org.bson.types.Binary; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.FieldName.Type; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -73,8 +74,8 @@ public class BsonUtils { public static final Document EMPTY_DOCUMENT = new EmptyDocument(); @SuppressWarnings("unchecked") - @Nullable - public static T get(Bson bson, String key) { + @Contract("null, _ -> null") + public static @Nullable T get(@Nullable Bson bson, String key) { return (T) asMap(bson).get(key); } @@ -85,7 +86,7 @@ public static T get(Bson bson, String key) { * @param bson * @return */ - public static Map asMap(Bson bson) { + public static Map asMap(@Nullable Bson bson) { return asMap(bson, MongoClientSettings.getDefaultCodecRegistry()); } @@ -126,7 +127,7 @@ public static Map asMap(@Nullable Bson bson, CodecRegistry codec * @return * @since 3.2.5 */ - public static Document asDocument(Bson bson) { + public static Document asDocument(@Nullable Bson bson) { return asDocument(bson, MongoClientSettings.getDefaultCodecRegistry()); } @@ -140,7 +141,7 @@ public static Document asDocument(Bson bson) { * @return never {@literal null}. * @since 4.0 */ - public static Document asDocument(Bson bson, CodecRegistry codecRegistry) { + public static Document asDocument(@Nullable Bson bson, CodecRegistry codecRegistry) { Map map = asMap(bson, codecRegistry); @@ -326,14 +327,14 @@ public static Object toJavaType(BsonValue value) { * @throws IllegalArgumentException if {@literal source} does not correspond to a {@link BsonValue} type. * @since 3.0 */ - public static BsonValue simpleToBsonValue(Object source) { + public static BsonValue simpleToBsonValue(@Nullable Object source) { return simpleToBsonValue(source, MongoClientSettings.getDefaultCodecRegistry()); } /** * Convert a given simple value (eg. {@link String}, {@link Long}) to its corresponding {@link BsonValue}. * - * @param source must not be {@literal null}. + * @param source can be {@literal null}. * @param codecRegistry The {@link CodecRegistry} used as a fallback to convert types using native {@link Codec}. Must * not be {@literal null}. * @return the corresponding {@link BsonValue} representation. @@ -341,7 +342,12 @@ public static BsonValue simpleToBsonValue(Object source) { * @since 4.2 */ @SuppressWarnings("unchecked") - public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegistry) { + @Contract("null, _ -> !null") + public static BsonValue simpleToBsonValue(@Nullable Object source, CodecRegistry codecRegistry) { + + if(source == null) { + return BsonNull.VALUE; + } if (source instanceof BsonValue bsonValue) { return bsonValue; @@ -398,7 +404,9 @@ public static BsonValue simpleToBsonValue(Object source, CodecRegistry codecRegi BsonCapturingWriter writer = new BsonCapturingWriter(value.getClass()); codec.encode(writer, value, ObjectUtils.isArray(value) || value instanceof Collection ? EncoderContext.builder().build() : null); - return writer.getCapturedValue(); + Object captured = writer.getCapturedValue(); + return captured instanceof BsonValue bv ? bv : BsonNull.VALUE; + } catch (CodecConfigurationException e) { throw new IllegalArgumentException( String.format("Unable to convert %s to BsonValue.", source != null ? source.getClass().getName() : "null")); @@ -450,8 +458,7 @@ public static Document toDocumentOrElse(String source, Function false") public static boolean isJsonDocument(@Nullable String value) { if (!StringUtils.hasText(value)) { @@ -488,6 +496,7 @@ public static boolean isJsonDocument(@Nullable String value) { * @return {@literal true} if the given value looks like a json array. * @since 3.0 */ + @Contract("null -> false") public static boolean isJsonArray(@Nullable String value) { return StringUtils.hasText(value) && (value.startsWith("[") && value.endsWith("]")); } @@ -525,8 +534,7 @@ public static Document parse(String json, @Nullable CodecRegistryProvider codecR * @return can be {@literal null}. * @since 3.0.8 */ - @Nullable - public static Object resolveValue(Bson bson, String key) { + public static @Nullable Object resolveValue(Bson bson, String key) { return resolveValue(asMap(bson), key); } @@ -541,7 +549,7 @@ public static Object resolveValue(Bson bson, String key) { * @return can be {@literal null}. * @since 4.2 */ - public static Object resolveValue(Bson bson, FieldName fieldName) { + public static @Nullable Object resolveValue(Bson bson, FieldName fieldName) { return resolveValue(asMap(bson), fieldName); } @@ -556,8 +564,7 @@ public static Object resolveValue(Bson bson, FieldName fieldName) { * @return can be {@literal null}. * @since 4.2 */ - @Nullable - public static Object resolveValue(Map source, FieldName fieldName) { + public static @Nullable Object resolveValue(Map source, FieldName fieldName) { if (fieldName.isKey()) { return source.get(fieldName.name()); @@ -590,8 +597,7 @@ public static Object resolveValue(Map source, FieldName fieldNam * @return can be {@literal null}. * @since 4.1 */ - @Nullable - public static Object resolveValue(Map source, String key) { + public static @Nullable Object resolveValue(Map source, String key) { if (source.containsKey(key)) { return source.get(key); @@ -643,9 +649,9 @@ public static boolean hasValue(Bson bson, String key) { * @param source can be {@literal null}. * @return can be {@literal null}. */ - @Nullable @SuppressWarnings("unchecked") - private static Map getAsMap(Object source) { + @Contract("null -> null") + private static @Nullable Map getAsMap(@Nullable Object source) { if (source instanceof Document document) { return document; @@ -745,8 +751,8 @@ public static Document mapEntries(Document source, Function null") + private static @Nullable String toJson(@Nullable Object value) { if (value == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java index 191c7d24d3..549c7ff720 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DotPath.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.util; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java index 67255b878a..78eb59a461 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/DurationUtil.java @@ -18,6 +18,7 @@ import java.time.Duration; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.Environment; import org.springframework.data.expression.ValueEvaluationContext; import org.springframework.data.expression.ValueExpression; @@ -25,7 +26,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.format.datetime.standard.DurationFormatterUtils; -import org.springframework.lang.Nullable; /** * Helper to evaluate Duration from expressions. @@ -70,13 +70,11 @@ public static Duration evaluate(String value, ValueEvaluationContext evaluationC public static Duration evaluate(String value, Supplier evaluationContext) { return evaluate(value, new ValueEvaluationContext() { - @Nullable @Override public Environment getEnvironment() { - return null; + throw new IllegalStateException(); } - @Nullable @Override public EvaluationContext getEvaluationContext() { return evaluationContext.get(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java index ffc97402fe..23ea9409cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/EmptyDocument.java @@ -66,9 +66,8 @@ public boolean replace(String key, Object oldValue, Object newValue) { throw new UnsupportedOperationException(); } - @Nullable @Override - public Object replace(String key, Object value) { + public @Nullable Object replace(String key, Object value) { throw new UnsupportedOperationException(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java index 8fc4b108ff..fbbba59e8f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java @@ -15,12 +15,9 @@ */ package org.springframework.data.mongodb.util; -import java.lang.reflect.Field; - +import org.jspecify.annotations.Nullable; import org.springframework.data.util.Version; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; import com.mongodb.internal.build.MongoDriverVersion; @@ -94,14 +91,12 @@ private static Version getMongoDbDriverVersion(ClassLoader classLoader) { return version == null ? guessDriverVersionFromClassPath(classLoader) : version; } - @Nullable - private static Version getVersionFromPackage(ClassLoader classLoader) { + private static @Nullable Version getVersionFromPackage(ClassLoader classLoader) { if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) { try { - Field field = ReflectionUtils.findField(MongoDriverVersion.class, "VERSION"); - return field != null ? Version.parse("" + field.get(null)) : null; - } catch (ReflectiveOperationException | IllegalArgumentException exception) { + return Version.parse(MongoDriverVersion.VERSION); + } catch (IllegalArgumentException exception) { // well not much we can do, right? } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java index 326a5c1e88..7fcc1383d5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoDbErrorCodes.java @@ -15,12 +15,14 @@ */ package org.springframework.data.mongodb.util; +import java.util.Collections; import java.util.HashMap; - -import org.springframework.lang.Nullable; +import java.util.Map; import com.mongodb.MongoException; +import org.jspecify.annotations.Nullable; + /** * {@link MongoDbErrorCodes} holds MongoDB specific error codes outlined in {@literal mongo/base/error_codes.yml}. * @@ -128,7 +130,9 @@ public final class MongoDbErrorCodes { clientSessionCodes.put(263, "OperationNotSupportedInTransaction"); clientSessionCodes.put(264, "TooManyLogicalSessions"); - errorCodes = new HashMap<>( + transactionCodes = new HashMap<>(0); + + errorCodes = new HashMap<>( dataAccessResourceFailureCodes.size() + dataIntegrityViolationCodes.size() + duplicateKeyCodes.size() + invalidDataAccessApiUsageException.size() + permissionDeniedCodes.size() + clientSessionCodes.size(), 1f); @@ -140,8 +144,7 @@ public final class MongoDbErrorCodes { errorCodes.putAll(clientSessionCodes); } - @Nullable - public static String getErrorDescription(@Nullable Integer errorCode) { + public static @Nullable String getErrorDescription(@Nullable Integer errorCode) { return errorCode == null ? null : errorCodes.get(errorCode); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java index 23c96f9e46..8b0f4b83ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/RegexFlags.java @@ -17,7 +17,7 @@ import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility to translate {@link Pattern#flags() regex flags} to MongoDB regex options and vice versa. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java index 344244717e..950f9ec797 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/aggregation/TestAggregationContext.java @@ -17,6 +17,7 @@ import org.bson.Document; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.ExposedFields.FieldReference; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java index 9dd3f1d8fb..be9a2e1cff 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/encryption/EncryptionUtils.java @@ -22,10 +22,10 @@ import org.bson.BsonBinary; import org.bson.BsonBinarySubType; import org.bson.types.Binary; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.util.spel.ExpressionUtils; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -48,8 +48,7 @@ public final class EncryptionUtils { * @return can be {@literal null}. * @throws IllegalArgumentException if one of the required arguments is {@literal null}. */ - @Nullable - public static Object resolveKeyId(String value, Supplier evaluationContext) { + public static @Nullable Object resolveKeyId(String value, Supplier evaluationContext) { Assert.notNull(value, "Value must not be null"); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java index b5c26755cf..3961fafc21 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/DateTimeFormatter.java @@ -23,6 +23,8 @@ import java.time.ZoneOffset; import java.time.ZonedDateTime; +import org.jspecify.annotations.NullUnmarked; + /** * DateTimeFormatter implementation borrowed from MongoDB @@ -33,6 +35,7 @@ * @author Ross Lawley * @since 2.2 */ +@NullUnmarked class DateTimeFormatter { private static final int DATE_STRING_LENGTH = "1970-01-01".length(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java index 6c31a9721f..57fecd284c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/EvaluationContextExpressionEvaluator.java @@ -18,12 +18,12 @@ import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -40,9 +40,8 @@ class EvaluationContextExpressionEvaluator implements ValueExpressionEvaluator { this.expressionParser = expressionParser; } - @Nullable @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { return evaluateExpression(expression, Collections.emptyMap()); } @@ -55,7 +54,7 @@ Expression getParsedExpression(String expressionString) { } @SuppressWarnings("unchecked") - T evaluateExpression(String expressionString, Map variables) { + @Nullable T evaluateExpression(String expressionString, Map variables) { Expression expression = getParsedExpression(expressionString); EvaluationContext ctx = getEvaluationContext(expressionString); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java index 4b4b497dae..dcb9a3ff13 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonBuffer.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.util.json; import org.bson.json.JsonParseException; +import org.jspecify.annotations.NullUnmarked; /** * JsonBuffer implementation borrowed from @@ -32,6 +33,7 @@ * @author Christoph Strobl * @since 2.2 */ +@NullUnmarked class JsonScanner { private final JsonBuffer buffer; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java index 293736123e..e73d57774b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/JsonToken.java @@ -20,6 +20,7 @@ import org.bson.BsonDouble; import org.bson.json.JsonParseException; import org.bson.types.Decimal128; +import org.jspecify.annotations.NullUnmarked; /** * JsonToken implementation borrowed from @@ -128,18 +128,15 @@ public static ParameterBindingContext forExpressions(ValueProvider valueProvider return new ParameterBindingContext(valueProvider, expressionEvaluator); } - @Nullable - public Object bindableValueForIndex(int index) { + public @Nullable Object bindableValueForIndex(int index) { return valueProvider.getBindableValue(index); } - @Nullable - public Object evaluateExpression(String expressionString) { + public @Nullable Object evaluateExpression(String expressionString) { return expressionEvaluator.evaluate(expressionString); } - @Nullable - public Object evaluateExpression(String expressionString, Map variables) { + public @Nullable Object evaluateExpression(String expressionString, Map variables) { if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator expressionEvaluator) { return expressionEvaluator.evaluateExpression(expressionString, variables); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java index adce99c904..8138f397a6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingDocumentCodec.java @@ -40,14 +40,14 @@ import org.bson.codecs.*; import org.bson.codecs.configuration.CodecRegistry; import org.bson.json.JsonParseException; - +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.expression.ValueExpressionParser; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.data.spel.ExpressionDependencies; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -66,6 +66,7 @@ * @author Rocco Lagrotteria * @since 2.2 */ +@NullUnmarked public class ParameterBindingDocumentCodec implements CollectibleCodec { private static final String ID_FIELD_NAME = FieldName.ID.name(); @@ -396,9 +397,8 @@ static class DependencyCapturingExpressionEvaluator implements ValueExpressionEv this.expressionParser = expressionParser; } - @Nullable @Override - public T evaluate(String expression) { + public @Nullable T evaluate(String expression) { dependencies.add(expressionParser.parse(expression).getExpressionDependencies()); return (T) PLACEHOLDER; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java index 8dd42e2427..c1e519e2f5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ParameterBindingJsonReader.java @@ -39,10 +39,11 @@ import org.bson.types.MaxKey; import org.bson.types.MinKey; import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.spel.EvaluationContextProvider; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -62,6 +63,7 @@ * @author Rocco Lagrotteria * @since 2.2 */ +@NullUnmarked public class ParameterBindingJsonReader extends AbstractBsonReader { private static final Pattern ENTIRE_QUERY_BINDING_PATTERN = Pattern.compile("^\\?(\\d+)$|^[\\?:][#$]\\{.*\\}$"); @@ -533,13 +535,11 @@ private BsonType bsonTypeForValue(Object value) { return BsonType.UNDEFINED; } - @Nullable - private Object evaluateExpression(String expressionString) { + private @Nullable Object evaluateExpression(String expressionString) { return bindingContext.evaluateExpression(expressionString, Collections.emptyMap()); } - @Nullable - private Object evaluateExpression(String expressionString, Map variables) { + private @Nullable Object evaluateExpression(String expressionString, Map variables) { return bindingContext.evaluateExpression(expressionString, variables); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java index 8f1d23885d..2ce22214fb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/ValueProvider.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.util.json; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A value provider to retrieve bindable values by their parameter index. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java index 8a86b3522b..60e5e8c609 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/package-info.java @@ -1,5 +1,5 @@ /** * MongoDB driver-specific utility classes for Json conversion. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.util.json; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java index 7caec410f5..a697bb7000 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/package-info.java @@ -2,5 +2,5 @@ * MongoDB driver-specific utility classes for {@link org.bson.conversions.Bson} and {@link com.mongodb.DBObject} * interaction. */ -@org.springframework.lang.NonNullApi +@org.jspecify.annotations.NullMarked package org.springframework.data.mongodb.util; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java index 9fa66b3b2b..796f618906 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/spel/ExpressionUtils.java @@ -17,12 +17,12 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; 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.StringUtils; /** @@ -42,8 +42,7 @@ public final class ExpressionUtils { * @param potentialExpression can be {@literal null} * @return can be {@literal null}. */ - @Nullable - public static Expression detectExpression(@Nullable String potentialExpression) { + public static @Nullable Expression detectExpression(@Nullable String potentialExpression) { if (!StringUtils.hasText(potentialExpression)) { return null; @@ -53,8 +52,7 @@ public static Expression detectExpression(@Nullable String potentialExpression) return expression instanceof LiteralExpression ? null : expression; } - @Nullable - public static Object evaluate(String value, Supplier evaluationContext) { + public static @Nullable Object evaluate(String value, Supplier evaluationContext) { Expression expression = detectExpression(value); if (expression == null) { diff --git a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt index d132482f65..d7784a7768 100644 --- a/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt +++ b/spring-data-mongodb/src/main/kotlin/org/springframework/data/mongodb/core/query/TypedUpdateExtensions.kt @@ -142,7 +142,7 @@ fun Update.pull(key: KProperty, value: Any) = * @since 4.4 * @see Update.pullAll */ -fun Update.pullAll(key: KProperty>, values: Array) = +fun Update.pullAll(key: KProperty>, values: Array) = pullAll(key.toDotPath(), values) /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java index 0448ad936c..c05122873c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/CapturingTransactionOptionsResolver.java @@ -21,7 +21,7 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.ListAssert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.CollectionUtils; /** @@ -36,9 +36,8 @@ public CapturingTransactionOptionsResolver(MongoTransactionOptionsResolver deleg this.delegateResolver = delegateResolver; } - @Nullable @Override - public String getLabelPrefix() { + public @Nullable String getLabelPrefix() { return delegateResolver.getLabelPrefix(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java index 44692348a0..d89edc6206 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/MongoTransactionOptionsUnitTests.java @@ -20,8 +20,8 @@ import java.time.Duration; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; @@ -90,27 +90,23 @@ void testEquals() { assertThat(MongoTransactionOptions.NONE) // .isSameAs(MongoTransactionOptions.NONE) // .isNotEqualTo(new MongoTransactionOptions() { - @Nullable @Override - public Duration getMaxCommitTime() { + public @Nullable Duration getMaxCommitTime() { return null; } - @Nullable @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return null; } - @Nullable @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return null; } - @Nullable @Override - public WriteConcern getWriteConcern() { + public @Nullable WriteConcern getWriteConcern() { return null; } }); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java index 9730e61e51..bdc151ec63 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import org.bson.BsonDocument; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mockito; @@ -31,7 +32,6 @@ import org.springframework.data.mongodb.ClientSessionException; import org.springframework.data.mongodb.MongoTransactionException; import org.springframework.data.mongodb.UncategorizedMongoDbException; -import org.springframework.lang.Nullable; import com.mongodb.MongoCursorNotFoundException; import com.mongodb.MongoException; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java index 51b3b005a5..9a6bbb4f29 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateDocumentReferenceTests.java @@ -29,6 +29,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; -import org.springframework.lang.Nullable; import com.mongodb.client.MongoClient; import com.mongodb.client.model.Filters; @@ -1936,9 +1936,8 @@ public String toString() { static class ReferencableConverter implements Converter> { - @Nullable @Override - public DocumentPointer convert(ReferenceAble source) { + public @Nullable DocumentPointer convert(ReferenceAble source) { return source::toReference; } } @@ -1947,9 +1946,8 @@ public DocumentPointer convert(ReferenceAble source) { static class DocumentToSimpleObjectRefWithReadingConverter implements Converter, SimpleObjectRefWithReadingConverter> { - @Nullable @Override - public SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { + public @Nullable SimpleObjectRefWithReadingConverter convert(DocumentPointer source) { Document document = client.getDatabase(DB_NAME).getCollection("simple-object-ref") .find(Filters.eq("_id", source.getPointer().get("ref-key-from-custom-write-converter"))).first(); @@ -1961,9 +1959,8 @@ public SimpleObjectRefWithReadingConverter convert(DocumentPointer sou static class SimpleObjectRefWithReadingConverterToDocumentConverter implements Converter> { - @Nullable @Override - public DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { + public @Nullable DocumentPointer convert(SimpleObjectRefWithReadingConverter source) { return () -> new Document("ref-key-from-custom-write-converter", source.getId()); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java index 766929c732..772392f037 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateScrollTests.java @@ -26,6 +26,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -47,7 +48,6 @@ import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.client.MongoClient; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java index f3aeec6de8..5a006bebfe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateTests.java @@ -36,6 +36,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -91,7 +92,6 @@ import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.mongodb.test.util.MongoVersion; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 79a0bb1fcb..b5892c2ca0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -39,6 +39,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -99,7 +100,6 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.mock.env.MockEnvironment; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; @@ -2908,8 +2908,7 @@ public List getValues() { return values; } - @Nullable - public T getValue() { + public @Nullable T getValue() { return CollectionUtils.lastElement(values); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java index 18da8c516d..fd1b70f3c7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateValidationTests.java @@ -25,6 +25,7 @@ import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.schema.MongoJsonSchema; import org.springframework.data.mongodb.test.util.Client; import org.springframework.data.mongodb.test.util.MongoClientExtension; -import org.springframework.lang.Nullable; import org.springframework.test.context.junit.jupiter.SpringExtension; import com.mongodb.client.MongoClient; @@ -242,13 +242,11 @@ public SimpleBean(@Nullable String nonNullString, @Nullable Integer rangedIntege this.customFieldName = customFieldName; } - @Nullable - public String getNonNullString() { + public @Nullable String getNonNullString() { return this.nonNullString; } - @Nullable - public Integer getRangedInteger() { + public @Nullable Integer getRangedInteger() { return this.rangedInteger; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java index bc126e05f0..7b07cd9448 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/Person.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.types.ObjectId; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java index 609a456912..b5a40f5738 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMapReduceOperationSupportUnitTests.java @@ -74,7 +74,7 @@ void usesExtractedCollectionName() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -84,7 +84,7 @@ void usesExplicitCollectionName() { .inCollection("the-night-angel").all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq("the-night-angel"), eq(Person.class), - eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), isNull()); + eq(MAP_FUNCTION), eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -108,7 +108,7 @@ void usesQueryWhenPresent() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).matching(query).all(); verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-2416 @@ -121,7 +121,7 @@ void usesCriteriaWhenPresent() { .matching(where("lastname").is("skywalker")).all(); verify(template).mapReduce(eq(query), eq(Person.class), eq(STAR_WARS), eq(Person.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } @Test // DATAMONGO-1929 @@ -132,7 +132,7 @@ void usesProjectionWhenPresent() { mapReduceOpsSupport.mapReduce(Person.class).map(MAP_FUNCTION).reduce(REDUCE_FUNCTION).as(Jedi.class).all(); verify(template).mapReduce(any(Query.class), eq(Person.class), eq(STAR_WARS), eq(Jedi.class), eq(MAP_FUNCTION), - eq(REDUCE_FUNCTION), isNull()); + eq(REDUCE_FUNCTION), any()); } interface Contact {} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index f89b2fa8c1..5ba0d947fe 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -43,6 +43,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -93,7 +94,6 @@ import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; -import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.CollectionUtils; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java index 8968f53a74..b8cc9cc972 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/TransactionOptionsTestService.java @@ -18,7 +18,7 @@ import java.util.function.Function; import java.util.function.UnaryOperator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.transaction.annotation.Transactional; /** @@ -48,45 +48,38 @@ public T saveWithinMaxCommitTime(T entity) { return saveFunction.apply(entity); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=available" }) - public T availableReadConcernFind(Object id) { + public @Nullable T availableReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=invalid" }) - public T invalidReadConcernFind(Object id) { + public @Nullable T invalidReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=${tx.read.concern}" }) - public T environmentReadConcernFind(Object id) { + public @Nullable T environmentReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readConcern=majority" }) - public T majorityReadConcernFind(Object id) { + public @Nullable T majorityReadConcernFind(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primaryPreferred" }) - public T findFromPrimaryPreferredReplica(Object id) { + public @Nullable T findFromPrimaryPreferredReplica(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=invalid" }) - public T findFromInvalidReplica(Object id) { + public @Nullable T findFromInvalidReplica(Object id) { return findByIdFunction.apply(id); } - @Nullable @Transactional(transactionManager = "txManager", label = { "mongo:readPreference=primary" }) - public T findFromPrimaryReplica(Object id) { + public @Nullable T findFromPrimaryReplica(Object id) { return findByIdFunction.apply(id); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java index d4c2f37f63..61cd3ecce4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/UpdateOperationsUnitTests.java @@ -20,6 +20,8 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -29,8 +31,6 @@ import org.springframework.data.mongodb.core.convert.UpdateMapper; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import com.mongodb.MongoClientSettings; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java index 25fbbbcb83..e9eae082e3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/User.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public class User { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java index 8dcf96231c..efc206a0d4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AddFieldsOperationUnitTests.java @@ -20,13 +20,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link AddFieldsOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java index 47176fd8ab..d830a44582 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/DensifyOperationUnitTests.java @@ -19,6 +19,7 @@ import java.util.Date; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.aggregation.DensifyOperation.DensifyUnits; import org.springframework.data.mongodb.core.aggregation.DensifyOperation.Range; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link DensifyOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index 9496a51c03..1b9aba1ba0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.geo.Distance; @@ -33,7 +34,6 @@ import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; -import org.springframework.lang.Nullable; /** * Unit tests for {@link GeoNearOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java index 311496ba8d..18980f6a06 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/MergeOperationUnitTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.aggregation.AccumulatorOperators.Sum; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link MergeOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java index 1174507e1c..a463b72cff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/Order.java @@ -20,6 +20,8 @@ import java.util.Date; import java.util.List; +import org.springframework.lang.Contract; + /** * @author Thomas Darimont */ diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java index 24566089e7..d29e32a988 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/RedactOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -27,7 +28,6 @@ import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.query.Criteria; -import org.springframework.lang.Nullable; /** * Unit tests for {@link RedactOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java index d6f95216a5..75632f4ace 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetOperationUnitTests.java @@ -20,13 +20,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link SetOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java index b5f5f596e6..18eb659cd0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/SetWindowFieldsOperationUnitTests.java @@ -20,6 +20,7 @@ import java.util.Date; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort.Direction; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link SetWindowFieldsOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java index e47fea289e..ef514ca882 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnionWithOperationUnitTests.java @@ -21,13 +21,13 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link UnionWithOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java index 2f081cc9fc..c406b89626 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/UnsetOperationUnitTests.java @@ -22,6 +22,7 @@ import java.util.Collections; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -29,7 +30,6 @@ import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.lang.Nullable; /** * Unit tests for {@link UnsetOperation}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java index 787e8d6746..71c395e822 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/DbRefMappingMongoConverterUnitTests.java @@ -34,6 +34,7 @@ import org.bson.Document; import org.bson.conversions.Bson; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.condition.DisabledForJreRange; @@ -55,7 +56,6 @@ 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.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.util.SerializationUtils; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index a343d15c7e..5bd7e06b97 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -40,6 +40,8 @@ import org.bson.types.Code; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -104,8 +106,6 @@ import org.springframework.data.projection.EntityProjection; import org.springframework.data.projection.EntityProjectionIntrospector; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.test.util.ReflectionTestUtils; import com.mongodb.BasicDBList; @@ -3167,15 +3167,14 @@ void beanConverter() { registrar.registerConverter(WithValueConverters.class, "viaRegisteredConverter", new PropertyValueConverter() { - @Nullable @Override - public String read(@Nullable org.bson.Document nativeValue, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document nativeValue, MongoConversionContext context) { return nativeValue.getString("bar"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String domainValue, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String domainValue, MongoConversionContext context) { return new org.bson.Document("bar", domainValue); } }); @@ -4214,9 +4213,9 @@ public SubTypeOfGenericType convert(org.bson.Document source) { @WritingConverter static class TypeImplementingMapToDocumentConverter implements Converter { - @Nullable + @Override - public org.bson.Document convert(TypeImplementingMap source) { + public org.bson.@Nullable Document convert(TypeImplementingMap source) { return new org.bson.Document("1st", source.val1).append("2nd", source.val2); } } @@ -4224,9 +4223,8 @@ public org.bson.Document convert(TypeImplementingMap source) { @ReadingConverter static class DocumentToTypeImplementingMapConverter implements Converter { - @Nullable @Override - public TypeImplementingMap convert(org.bson.Document source) { + public @Nullable TypeImplementingMap convert(org.bson.Document source) { return new TypeImplementingMap(source.getString("1st"), source.getInteger("2nd")); } } @@ -4412,30 +4410,28 @@ enum Converter2 implements MongoValueConverter { INSTANCE; - @Nullable @Override - public String read(@Nullable org.bson.Document value, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) { return value.getString("bar"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String value, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("bar", value); } } static class Converter1 implements MongoValueConverter { - @Nullable @Override - public String read(@Nullable org.bson.Document value, MongoConversionContext context) { + public @Nullable String read(org.bson.@Nullable Document value, MongoConversionContext context) { return value.getString("foo"); } - @Nullable + @Override - public org.bson.Document write(@Nullable String value, MongoConversionContext context) { + public org.bson.@Nullable Document write(@Nullable String value, MongoConversionContext context) { return new org.bson.Document("foo", value); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java index eb3b1aba1a..0fe791784d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/ReversingValueConverter.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java index 6ea0f5aa9c..b12b83fe3a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/TextQueryTests.java @@ -21,6 +21,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -35,7 +36,6 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java index 3e5f416caf..a7fba9a046 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/performance/ReactivePerformanceTests.java @@ -28,6 +28,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StopWatch; import org.springframework.util.StringUtils; @@ -99,9 +99,8 @@ public void setUp() throws Exception { converter = new MappingMongoConverter(new DbRefResolver() { - @Nullable @Override - public Object resolveReference(MongoPersistentProperty property, Object source, + public @Nullable Object resolveReference(MongoPersistentProperty property, Object source, ReferenceLookupDelegate referenceLookupDelegate, MongoEntityReader entityReader) { return null; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java index 534f44c8fb..be5be2d9ba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Address.java @@ -14,8 +14,7 @@ * limitations under the License. */ package org.springframework.data.mongodb.repository; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; import com.querydsl.core.annotations.QueryEmbeddable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java index c41abf4aa1..e0c2caee31 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MongoRepositoryTextSearchIntegrationTests.java @@ -20,6 +20,7 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +39,6 @@ import org.springframework.data.mongodb.test.util.MongoTemplateExtension; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.Template; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java index 3dace8928b..4e589d5892 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/MyId.java @@ -17,7 +17,7 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java index 664b5279c8..eeca60bc3e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/Person.java @@ -21,6 +21,7 @@ import java.util.Set; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.index.GeoSpatialIndexType; import org.springframework.data.mongodb.core.index.GeoSpatialIndexed; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.mapping.DocumentReference; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.Unwrapped; -import org.springframework.lang.Nullable; /** * Sample domain class. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index c66b554078..93a293ecff 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -23,6 +23,7 @@ import java.util.regex.Pattern; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -43,7 +44,6 @@ import org.springframework.data.mongodb.repository.Person.Sex; import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.query.Param; -import org.springframework.lang.Nullable; /** * Sample repository managing {@link Person} entities. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java index 0af684b9c1..b2b350dc4d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepositoryTransactionalTests.java @@ -27,6 +27,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -46,7 +47,6 @@ import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.ReplSetClient; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.test.context.transaction.AfterTransaction; @@ -205,9 +205,8 @@ private AfterTransactionAssertion assertAfterTransaction(Person person) { AfterTransactionAssertion assertion = new AfterTransactionAssertion<>(new Persistable() { - @Nullable @Override - public Object getId() { + public @Nullable Object getId() { return person.id; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java index 9198e002c0..751fc51ded 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/SimpleReactiveMongoRepositoryTests.java @@ -26,6 +26,7 @@ import java.util.Objects; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; @@ -48,7 +49,6 @@ import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; import org.springframework.data.mongodb.test.util.EnableIfReplicaSetAvailable; import org.springframework.data.repository.query.FluentQuery; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; import org.springframework.transaction.TransactionDefinition; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java index 606cca8647..c3bb9cb724 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/UserWithComplexId.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java index 294e4ea501..4f1adc714e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPerson.java @@ -17,9 +17,9 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.springframework.data.annotation.Version; import org.springframework.data.mongodb.core.mapping.Document; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl @@ -48,8 +48,7 @@ public String getFirstname() { return this.firstname; } - @Nullable - public String getLastname() { + public @Nullable String getLastname() { return this.lastname; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java index b4bc48cadf..b55ee77732 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveStringBasedAggregationUnitTests.java @@ -27,6 +27,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -57,7 +58,6 @@ import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.reactive.ReactiveCrudRepository; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.ReadPreference; @@ -239,14 +239,12 @@ private Class inputTypeOf(AggregationInvocation invocation) { return invocation.aggregation.getInputType(); } - @Nullable - private Collation collationOf(AggregationInvocation invocation) { + private @Nullable Collation collationOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null) : null; } - @Nullable - private Object hintOf(AggregationInvocation invocation) { + private @Nullable Object hintOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java index 463bb2a22a..827168007e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StringBasedAggregationUnitTests.java @@ -27,6 +27,7 @@ import java.util.stream.Stream; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,7 +65,6 @@ import org.springframework.data.repository.Repository; import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import com.mongodb.MongoClientSettings; @@ -323,14 +323,12 @@ private Class inputTypeOf(AggregationInvocation invocation) { return invocation.aggregation.getInputType(); } - @Nullable - private Collation collationOf(AggregationInvocation invocation) { + private @Nullable Collation collationOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getCollation().orElse(null) : null; } - @Nullable - private Object hintOf(AggregationInvocation invocation) { + private @Nullable Object hintOf(AggregationInvocation invocation) { return invocation.aggregation.getOptions() != null ? invocation.aggregation.getOptions().getHintObject().orElse(null) : null; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 1927378e80..3ed7ace0f9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -18,6 +18,7 @@ import java.util.Arrays; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; @@ -30,7 +31,6 @@ import org.springframework.data.mongodb.core.query.TextCriteria; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.repository.query.ParameterAccessor; -import org.springframework.lang.Nullable; /** * Simple {@link ParameterAccessor} that returns the given parameters unfiltered. @@ -121,7 +121,7 @@ public Collation getCollation() { * @see org.springframework.data.mongodb.repository.query.MongoParameterAccessor#getValues() */ @Override - public Object[] getValues() { + public Object @Nullable[] getValues() { return this.values; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java index 15a0538600..eda1e501a0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MappingContextConfigurer.java @@ -20,7 +20,7 @@ import java.util.HashSet; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility to configure {@link org.springframework.data.mongodb.core.mapping.MongoMappingContext} properties. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java index 09149c02ef..8300690ccd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplateConfiguration.java @@ -20,6 +20,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.context.ApplicationContext; @@ -39,7 +40,6 @@ import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback; import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl From 69f50c6f72a43b88f2a0b533e397ae76fca20f3a Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 9 Jan 2025 13:18:22 +0100 Subject: [PATCH 42/74] Add support for MongoDB AOT Repositories. Initial Support for generating repository source code at build time. Closes: #4939 --- spring-data-mongodb/pom.xml | 6 + .../aot/generated/AotQueryCreator.java | 199 ++++++ .../mongodb/aot/generated/MongoBlocks.java | 290 ++++++++ .../generated/MongoRepositoryContributor.java | 118 ++++ .../mongodb/aot/generated/StringQuery.java | 227 ++++++ .../data/mongodb/core/query/Criteria.java | 15 +- .../core/query/CriteriaDefinition.java | 17 + .../aot/AotMongoRepositoryPostProcessor.java | 14 +- .../repository/query/MongoQueryCreator.java | 2 +- .../repository/query/MongoQueryExecution.java | 59 +- .../CrudMethodMetadataPostProcessor.java | 3 +- .../data/mongodb/util/BsonUtils.java | 67 +- .../mongodb/util/json/SpringJsonWriter.java | 478 +++++++++++++ .../src/test/java/example/aot/User.java | 102 +++ .../test/java/example/aot/UserProjection.java | 29 + .../test/java/example/aot/UserRepository.java | 146 ++++ .../data/mongodb/aot/generated/DemoRepo.java | 62 ++ .../MongoRepositoryContributorTests.java | 662 ++++++++++++++++++ .../generated/StubRepositoryInformation.java | 144 ++++ .../TestMongoAotRepositoryContext.java | 129 ++++ .../mongodb/test/util/MongoTestTemplate.java | 9 + .../util/json/SpringJsonWriterUnitTests.java | 159 +++++ .../src/test/resources/logback.xml | 1 + 23 files changed, 2922 insertions(+), 16 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/User.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/UserProjection.java create mode 100644 spring-data-mongodb/src/test/java/example/aot/UserRepository.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 4d9d5a3b50..ad3c1338ec 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -288,6 +288,12 @@ test + + org.springframework + spring-core-test + test + + org.jetbrains.kotlin diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java new file mode 100644 index 0000000000..c0fbfc4ee9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java @@ -0,0 +1,199 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import org.bson.conversions.Bson; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Sort; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.convert.MongoCustomConversions; +import org.springframework.data.mongodb.core.convert.MongoWriter; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.core.query.TextCriteria; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.ConvertingParameterAccessor; +import org.springframework.data.mongodb.repository.query.MongoParameterAccessor; +import org.springframework.data.mongodb.repository.query.MongoQueryCreator; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +import com.mongodb.DBRef; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class AotQueryCreator { + + private MongoMappingContext mappingContext; + + public AotQueryCreator() { + + MongoMappingContext mongoMappingContext = new MongoMappingContext(); + mongoMappingContext.setSimpleTypeHolder( + MongoCustomConversions.create((cfg) -> cfg.useNativeDriverJavaTimeCodecs()).getSimpleTypeHolder()); + mongoMappingContext.setAutoIndexCreation(false); + mongoMappingContext.afterPropertiesSet(); + + this.mappingContext = mongoMappingContext; + } + + StringQuery createQuery(PartTree partTree, int parameterCount) { + + Query query = new MongoQueryCreator(partTree, + new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext) + .createQuery(); + + if(partTree.isLimiting()) { + query.limit(partTree.getMaxResults()); + } + return new StringQuery(query); + } + + static class PlaceholderConvertingParameterAccessor extends ConvertingParameterAccessor { + + /** + * Creates a new {@link ConvertingParameterAccessor} with the given {@link MongoWriter} and delegate. + * + * @param delegate must not be {@literal null}. + */ + public PlaceholderConvertingParameterAccessor(PlaceholderParameterAccessor delegate) { + super(PlaceholderWriter.INSTANCE, delegate); + } + } + + enum PlaceholderWriter implements MongoWriter { + + INSTANCE; + + @Nullable + @Override + public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { + return obj instanceof Placeholder p ? p.getValue() : obj; + } + + @Override + public DBRef toDBRef(Object object, @Nullable MongoPersistentProperty referringProperty) { + return null; + } + + @Override + public void write(Object source, Bson sink) { + + } + } + + static class PlaceholderParameterAccessor implements MongoParameterAccessor { + + private final List placeholders; + + public PlaceholderParameterAccessor(int parameterCount) { + if (parameterCount == 0) { + placeholders = List.of(); + } else { + placeholders = IntStream.range(0, parameterCount).mapToObj(it -> new Placeholder("?" + it)) + .collect(Collectors.toList()); + } + } + + @Override + public Range getDistanceRange() { + return null; + } + + @Nullable + @Override + public Point getGeoNearLocation() { + return null; + } + + @Nullable + @Override + public TextCriteria getFullText() { + return null; + } + + @Nullable + @Override + public Collation getCollation() { + return null; + } + + @Override + public Object[] getValues() { + return placeholders.toArray(); + } + + @Nullable + @Override + public UpdateDefinition getUpdate() { + return null; + } + + @Nullable + @Override + public ScrollPosition getScrollPosition() { + return null; + } + + @Override + public Pageable getPageable() { + return null; + } + + @Override + public Sort getSort() { + return null; + } + + @Nullable + @Override + public Class findDynamicProjection() { + return null; + } + + @Nullable + @Override + public Object getBindableValue(int index) { + return placeholders.get(index).getValue(); + } + + @Override + public boolean hasBindableNullValue() { + return false; + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public Iterator iterator() { + return ((List) placeholders).iterator(); + } + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java new file mode 100644 index 0000000000..1f550d814e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -0,0 +1,290 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.lang.reflect.Parameter; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Pattern; +import java.util.stream.Collectors; + +import org.bson.Document; +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + */ +public class MongoBlocks { + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + + static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryBlockBuilder(context); + } + + static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new QueryExecutionBlockBuilder(context); + } + + static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + return new DeleteExecutionBuilder(context); + } + + static class DeleteExecutionBuilder { + + AotRepositoryMethodGenerationContext context; + String queryVariableName; + + public DeleteExecutionBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + public DeleteExecutionBuilder referencing(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + public CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, actualReturnType, + mongoOpsRef, context.getRepositoryInformation().getDomainType()); + + Type type = Type.FIND_AND_REMOVE_ALL; + if (context.returnsSingleValue()) { + if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { + type = Type.FIND_AND_REMOVE_ONE; + } else { + type = Type.ALL; + } + } + + actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) + ? ClassName.get(context.getMethod().getReturnType()) + : context.returnsSingleValue() ? actualReturnType : context.getReturnType(); + + builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class, + DeleteExecutionX.Type.class, type.name(), queryVariableName); + + return builder.build(); + } + } + + static class QueryExecutionBlockBuilder { + + AotRepositoryMethodGenerationContext context; + private String queryVariableName; + + public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + } + + QueryExecutionBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + Object actualReturnType = isProjecting ? context.getActualReturnType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + + if (isProjecting) { + builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, + mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); + } else { + + builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, + context.getRepositoryInformation().getDomainType()); + } + + String terminatingMethod = "all()"; + if (context.returnsSingleValue()) { + + if (context.returnsOptionalValue()) { + terminatingMethod = "one()"; + } else if (context.isCountMethod()) { + terminatingMethod = "count()"; + } else if (context.isExistsMethod()) { + terminatingMethod = "exists()"; + } else { + terminatingMethod = "oneValue()"; + } + } + + if (context.returnsPage()) { + builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, + context.getPageableParameterName(), queryVariableName); + } else if (context.returnsSlice()) { + builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, + context.getPageableParameterName(), queryVariableName); + } else { + builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); + } + + return builder.build(); + + } + } + + static class QueryBlockBuilder { + + AotRepositoryMethodGenerationContext context; + StringQuery source; + List arguments; + private String queryVariableName; + + public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + this.context = context; + this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) + .collect(Collectors.toList()); + + // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); + // this.argumentSource = new MongoParameters(parametersSource, false); + + } + + public QueryBlockBuilder filter(StringQuery query) { + this.source = query; + return this; + } + + public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + String queryDocumentVariableName = "%sDocument".formatted(queryVariableName); + builder.add(renderExpressionToDocument(source.getQueryString(), queryVariableName)); + builder.addStatement("$T $L = new $T($L)", BasicQuery.class, queryVariableName, BasicQuery.class, + queryDocumentVariableName); + + if (StringUtils.hasText(source.getFieldsString())) { + builder.add(renderExpressionToDocument(source.getFieldsString(), "fields")); + builder.addStatement("$L.setFieldsObject(fieldsDocument)", queryVariableName); + } + + String sortParameter = context.getSortParameterName(); + if (StringUtils.hasText(sortParameter)) { + + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } else if (StringUtils.hasText(source.getSortString())) { + + builder.add(renderExpressionToDocument(source.getSortString(), "sort")); + builder.addStatement("$L.setSortObject(sortDocument)", queryVariableName); + } + + String limitParameter = context.getLimitParameterName(); + if (StringUtils.hasText(limitParameter)) { + builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); + } else if (context.getPageableParameterName() == null && source.isLimited()) { + builder.addStatement("$L.limit($L)", queryVariableName, source.getLimit()); + } + + String pageableParameter = context.getPageableParameterName(); + if (StringUtils.hasText(pageableParameter) && !context.returnsPage() && !context.returnsSlice()) { + builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); + } + + String hint = context.annotationValue(Hint.class, "value"); + + if (StringUtils.hasText(hint)) { + builder.addStatement("$L.withHint($S)", queryVariableName, hint); + } + + String readPreference = context.annotationValue(ReadPreference.class, "value"); + if (StringUtils.hasText(readPreference)) { + builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, + com.mongodb.ReadPreference.class, readPreference); + } + + // TODO: all the meta stuff + + return builder.build(); + } + + private CodeBlock renderExpressionToDocument(@Nullable String source, String variableName) { + + Builder builder = CodeBlock.builder(); + if (!StringUtils.hasText(source)) { + builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), Document.class); + } else if (!containsPlaceholder(source)) { + builder.addStatement("$T $L = $T.parse($S)", Document.class, "%sDocument".formatted(variableName), + Document.class, source); + } else { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + String tmpVarName = "%sString".formatted(variableName); + + builder.addStatement("String $L = $S", tmpVarName, source); + builder.addStatement("$T $L = new $T($L, $L.getConverter(), new $T[]{ $L }).toDocument()", Document.class, + "%sDocument".formatted(variableName), BindableMongoExpression.class, tmpVarName, mongoOpsRef, Object.class, + StringUtils.collectionToDelimitedString(arguments, ", ")); + } + + return builder.build(); + } + + private boolean containsPlaceholder(String source) { + return PARAMETER_BINDING_PATTERN.matcher(source).find(); + } + + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java new file mode 100644 index 0000000000..d42afd61bc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -0,0 +1,118 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.util.regex.Pattern; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class MongoRepositoryContributor extends RepositoryContributor { + + private AotQueryCreator queryCreator; + + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { + super(repositoryContext); + this.queryCreator = new AotQueryCreator(); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); + } + + @Override + protected AotRepositoryMethodBuilder contributeRepositoryMethod( + AotRepositoryMethodGenerationContext generationContext) { + + // TODO: do not generate stuff for spel expressions + + if (AnnotatedElementUtils.hasAnnotation(generationContext.getMethod(), Aggregation.class)) { + return null; + } + { + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); + if (queryAnnotation != null) { + if (StringUtils.hasText(queryAnnotation.value()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + return null; + } + } + } + + // so the rest should work + return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); + StringQuery query; + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.value())) { + query = new StringQuery(queryAnnotation.value()); + + } else { + PartTree partTree = new PartTree(context.getMethod().getName(), + context.getRepositoryInformation().getDomainType()); + query = queryCreator.createQuery(partTree, context.getMethod().getParameterCount()); + } + + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { + query.sort(queryAnnotation.sort()); + } + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) { + query.fields(queryAnnotation.fields()); + } + + writeStringQuery(context, body, query); + }); + } + + private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, StringQuery query) { + + body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context).filter(query); + + if (context.isDeleteMethod()) { + + String deleteQueryVariableName = "deleteQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); + body.addCode(MongoBlocks.deleteExecutionBlockBuilder(context).referencing(deleteQueryVariableName).build()); + } else { + + String filterQueryVariableName = "filterQuery"; + body.addCode(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); + body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).referencing(filterQueryVariableName).build()); + } + } + + private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { + writeStringQuery(context, body, new StringQuery(query.value())); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java new file mode 100644 index 0000000000..c8d7b7ab2a --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java @@ -0,0 +1,227 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.util.Optional; +import java.util.Set; + +import org.bson.Document; +import org.springframework.data.domain.KeysetScrollPosition; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Field; +import org.springframework.data.mongodb.core.query.Meta; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.util.BsonUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.StringUtils; + +import com.mongodb.ReadConcern; +import com.mongodb.ReadPreference; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +class StringQuery extends Query { + + private Query delegate; + private @Nullable String raw; + private @Nullable String sort; + private @Nullable String fields; + + private ExecutionType executionType = ExecutionType.QUERY; + + public StringQuery(Query query) { + this.delegate = query; + } + + public StringQuery(String query) { + this.delegate = new Query(); + this.raw = query; + } + + public StringQuery forCount() { + this.executionType = ExecutionType.COUNT; + return this; + } + + @Nullable + String getQueryString() { + + if (StringUtils.hasText(raw)) { + return raw; + } + + Document queryObj = getQueryObject(); + if (queryObj.isEmpty()) { + return null; + } + return toJson(queryObj); + } + + public Query sort(String sort) { + this.sort = sort; + return this; + } + + @Override + public Field fields() { + return delegate.fields(); + } + + @Override + public boolean hasReadConcern() { + return delegate.hasReadConcern(); + } + + @Override + public ReadConcern getReadConcern() { + return delegate.getReadConcern(); + } + + @Override + public boolean hasReadPreference() { + return delegate.hasReadPreference(); + } + + @Override + public ReadPreference getReadPreference() { + return delegate.getReadPreference(); + } + + @Override + public boolean hasKeyset() { + return delegate.hasKeyset(); + } + + @Override + @Nullable + public KeysetScrollPosition getKeyset() { + return delegate.getKeyset(); + } + + @Override + public Set> getRestrictedTypes() { + return delegate.getRestrictedTypes(); + } + + @Override + public Document getQueryObject() { + return delegate.getQueryObject(); + } + + @Override + public Document getFieldsObject() { + return delegate.getFieldsObject(); + } + + @Override + public Document getSortObject() { + return delegate.getSortObject(); + } + + @Override + public boolean isSorted() { + return delegate.isSorted() || StringUtils.hasText(sort); + } + + @Override + public long getSkip() { + return delegate.getSkip(); + } + + @Override + public boolean isLimited() { + return delegate.isLimited(); + } + + @Override + public int getLimit() { + return delegate.getLimit(); + } + + @Override + @Nullable + public String getHint() { + return delegate.getHint(); + } + + @Override + public Meta getMeta() { + return delegate.getMeta(); + } + + @Override + public Optional getCollation() { + return delegate.getCollation(); + } + + @Nullable + String getSortString() { + if (StringUtils.hasText(sort)) { + return sort; + } + Document sort = getSortObject(); + if (sort.isEmpty()) { + return null; + } + return toJson(sort); + } + + @Nullable + String getFieldsString() { + if (StringUtils.hasText(fields)) { + return fields; + } + + Document fields = getFieldsObject(); + if (fields.isEmpty()) { + return null; + } + return toJson(fields); + } + + StringQuery fields(String fields) { + this.fields = fields; + return this; + } + + String toJson(Document source) { + StringBuffer buffer = new StringBuffer(); + BsonUtils.writeJson(source).to(buffer); + return buffer.toString(); + } + + enum ExecutionType { + QUERY, COUNT, DELETE + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index 547c6965ce..c03f1bb6d2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -29,9 +29,21 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; +import com.mongodb.MongoClientSettings; +import org.bson.BsonReader; import org.bson.BsonRegularExpression; import org.bson.BsonType; +import org.bson.BsonWriter; import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.DocumentCodec; +import org.bson.codecs.DocumentCodecProvider; +import org.bson.codecs.Encoder; +import org.bson.codecs.EncoderContext; +import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.configuration.CodecRegistries; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.Binary; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; @@ -944,7 +956,8 @@ public Document getCriteriaObject() { for (Criteria c : this.criteriaChain) { Document document = c.getSingleCriteriaObject(); for (String k : document.keySet()) { - setValue(criteriaObject, k, document.get(k)); + Object o = document.get(k); + setValue(criteriaObject, k, o); } } return criteriaObject; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java index c75f709ab9..6fc5d60a78 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java @@ -40,4 +40,21 @@ public interface CriteriaDefinition { @Nullable String getKey(); + class Placeholder { + + Object value; + + public Placeholder(Object value) { + this.value = value; + } + + public Object getValue() { + return value; + } + + @Override + public String toString() { + return getValue().toString(); + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java index d49726f724..2de77fc9b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java @@ -16,8 +16,11 @@ package org.springframework.data.mongodb.repository.aot; import org.springframework.aot.generate.GenerationContext; +import org.springframework.data.aot.AotContext; import org.springframework.data.mongodb.aot.LazyLoadingProxyAotProcessor; import org.springframework.data.mongodb.aot.MongoAotPredicates; +import org.springframework.data.mongodb.aot.generated.MongoRepositoryContributor; +import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; import org.springframework.data.util.TypeContributor; @@ -31,7 +34,8 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor(); @Override - protected void contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { + protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + GenerationContext generationContext) { // do some custom type registration here super.contribute(repositoryContext, generationContext); @@ -39,6 +43,14 @@ protected void contribute(AotRepositoryContext repositoryContext, GenerationCont TypeContributor.contribute(type, it -> true, generationContext); lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type, generationContext); }); + + boolean enabled = Boolean.parseBoolean( + repositoryContext.getEnvironment().getProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "false")); + if (!enabled) { + return null; + } + + return new MongoRepositoryContributor(repositoryContext); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 7e327f4e20..86223e83db 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -65,7 +65,7 @@ * @author Christoph Strobl * @author Edward Prentice */ -class MongoQueryCreator extends AbstractQueryCreator { +public class MongoQueryCreator extends AbstractQueryCreator { private static final Log LOG = LogFactory.getLog(MongoQueryCreator.class); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index abdcf62930..a9a631646e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.Iterator; import java.util.List; import java.util.function.Supplier; @@ -32,6 +33,9 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; @@ -55,7 +59,7 @@ * @author Christoph Strobl */ @FunctionalInterface -interface MongoQueryExecution { +public interface MongoQueryExecution { @Nullable Object execute(Query query); @@ -67,12 +71,12 @@ interface MongoQueryExecution { * @author Christoph Strobl * @since 1.5 */ - final class SlicedExecution implements MongoQueryExecution { + final class SlicedExecution implements MongoQueryExecution { - private final FindWithQuery find; + private final FindWithQuery find; private final Pageable pageable; - public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable pageable) { + public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable pageable) { Assert.notNull(find, "Find must not be null"); Assert.notNull(pageable, "Pageable must not be null"); @@ -83,7 +87,7 @@ public SlicedExecution(ExecutableFindOperation.FindWithQuery find, Pageable p @Override @SuppressWarnings({ "unchecked", "rawtypes" }) - public Object execute(Query query) { + public Slice execute(Query query) { int pageSize = pageable.getPageSize(); @@ -93,7 +97,7 @@ public Object execute(Query query) { boolean hasNext = result.size() > pageSize; - return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); + return new SliceImpl(hasNext ? result.subList(0, pageSize) : result, pageable, hasNext); } } @@ -104,12 +108,12 @@ public Object execute(Query query) { * @author Mark Paluch * @author Christoph Strobl */ - final class PagedExecution implements MongoQueryExecution { + final class PagedExecution implements MongoQueryExecution { - private final FindWithQuery operation; + private final FindWithQuery operation; private final Pageable pageable; - public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageable pageable) { + public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageable pageable) { Assert.notNull(operation, "Operation must not be null"); Assert.notNull(pageable, "Pageable must not be null"); @@ -119,11 +123,11 @@ public PagedExecution(ExecutableFindOperation.FindWithQuery operation, Pageab } @Override - public Object execute(Query query) { + public Page execute(Query query) { int overallLimit = query.getLimit(); - TerminatingFind matching = operation.matching(query); + TerminatingFind matching = operation.matching(query); // Apply raw pagination query.with(pageable); @@ -247,6 +251,39 @@ public Object execute(Query query) { } } + final class DeleteExecutionX implements MongoQueryExecution { + + ExecutableRemoveOperation.ExecutableRemove remove; + Type type; + + public DeleteExecutionX(ExecutableRemove remove, Type type) { + this.remove = remove; + this.type = type; + } + + @Nullable + @Override + public Object execute(Query query) { + + TerminatingRemove doRemove = remove.matching(query); + if (Type.ALL.equals(type)) { + DeleteResult result = doRemove.all(); + return result.wasAcknowledged() ? Long.valueOf(result.getDeletedCount()) : Long.valueOf(0); + } else if (Type.FIND_AND_REMOVE_ALL.equals(type)) { + return doRemove.findAndRemove(); + } else if (Type.FIND_AND_REMOVE_ONE.equals(type)) { + Iterator removed = doRemove.findAndRemove().iterator(); + return removed.hasNext() ? removed.next() : null; + + } + throw new RuntimeException(); + } + + public enum Type { + FIND_AND_REMOVE_ONE, FIND_AND_REMOVE_ALL, ALL + } + } + /** * {@link MongoQueryExecution} removing documents matching the query. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java index abd828a9f7..037bd60672 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/CrudMethodMetadataPostProcessor.java @@ -39,6 +39,7 @@ import org.springframework.util.ReflectionUtils; import com.mongodb.ReadPreference; +import org.springframework.util.StringUtils; /** * {@link RepositoryProxyPostProcessor} that sets up interceptors to read metadata information from the invoked method. @@ -193,7 +194,7 @@ private static Optional findReadPreference(AnnotatedElement... a org.springframework.data.mongodb.repository.ReadPreference preference = AnnotatedElementUtils .findMergedAnnotation(element, org.springframework.data.mongodb.repository.ReadPreference.class); - if (preference != null) { + if (preference != null && StringUtils.hasText(preference.value())) { return Optional.of(com.mongodb.ReadPreference.valueOf(preference.value())); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index 83df5f7798..99acb12940 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -29,11 +29,38 @@ import java.util.function.Function; import java.util.stream.StreamSupport; -import org.bson.*; +import org.bson.AbstractBsonWriter; +import org.bson.BSONObject; +import org.bson.BsonArray; +import org.bson.BsonBinary; +import org.bson.BsonBinarySubType; +import org.bson.BsonBoolean; +import org.bson.BsonContextType; +import org.bson.BsonDateTime; +import org.bson.BsonDbPointer; +import org.bson.BsonDecimal128; +import org.bson.BsonDouble; +import org.bson.BsonInt32; +import org.bson.BsonInt64; +import org.bson.BsonJavaScript; +import org.bson.BsonNull; +import org.bson.BsonObjectId; +import org.bson.BsonReader; +import org.bson.BsonRegularExpression; +import org.bson.BsonString; +import org.bson.BsonSymbol; +import org.bson.BsonTimestamp; +import org.bson.BsonUndefined; +import org.bson.BsonValue; +import org.bson.BsonWriter; +import org.bson.BsonWriterSettings; +import org.bson.Document; import org.bson.codecs.Codec; +import org.bson.codecs.DecoderContext; import org.bson.codecs.DocumentCodec; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecConfigurationException; +import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; import org.bson.conversions.Bson; import org.bson.json.JsonParseException; @@ -45,6 +72,8 @@ import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.FieldName.Type; +import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; +import org.springframework.data.mongodb.util.json.SpringJsonWriter; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -73,6 +102,9 @@ public class BsonUtils { */ public static final Document EMPTY_DOCUMENT = new EmptyDocument(); + private static final CodecRegistry JSON_CODEC_REGISTRY = CodecRegistries.fromRegistries( + MongoClientSettings.getDefaultCodecRegistry(), CodecRegistries.fromCodecs(new PlaceholderCodec())); + @SuppressWarnings("unchecked") @Contract("null, _ -> null") public static @Nullable T get(@Nullable Bson bson, String key) { @@ -751,6 +783,17 @@ public static Document mapEntries(Document source, Function { + SpringJsonWriter writer = new SpringJsonWriter(sink); + JSON_CODEC_REGISTRY.get(Document.class).encode(writer, document, EncoderContext.builder().build()); + }; + } + + public interface JsonWriter { + void to(StringBuffer sink); + } + @Contract("null -> null") private static @Nullable String toJson(@Nullable Object value) { @@ -963,4 +1006,26 @@ public void flush() { values.clear(); } } + + static class PlaceholderCodec implements Codec { + + @Override + public Placeholder decode(BsonReader reader, DecoderContext decoderContext) { + return null; + } + + @Override + public void encode(BsonWriter writer, Placeholder value, EncoderContext encoderContext) { + if (writer instanceof SpringJsonWriter sjw) { + sjw.writePlaceholder(value.toString()); + } else { + writer.writeString(value.toString()); + } + } + + @Override + public Class getEncoderClass() { + return Placeholder.class; + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java new file mode 100644 index 0000000000..370a272f53 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java @@ -0,0 +1,478 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.util.json; + +import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; + +import java.time.Instant; +import java.time.ZoneId; +import java.time.ZonedDateTime; +import java.util.Base64; + +import org.bson.BsonBinary; +import org.bson.BsonDbPointer; +import org.bson.BsonReader; +import org.bson.BsonRegularExpression; +import org.bson.BsonTimestamp; +import org.bson.BsonWriter; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; +import org.springframework.util.StringUtils; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class SpringJsonWriter implements BsonWriter { + + private final StringBuffer buffer; + + private enum JsonContextType { + TOP_LEVEL, DOCUMENT, ARRAY, + } + + private enum State { + INITIAL, NAME, VALUE, DONE + } + + private static class JsonContext { + private final JsonContext parentContext; + private final JsonContextType contextType; + private boolean hasElements; + + JsonContext(final JsonContext parentContext, final JsonContextType contextType) { + this.parentContext = parentContext; + this.contextType = contextType; + } + + JsonContext nestedDocument() { + return new JsonContext(this, JsonContextType.DOCUMENT); + } + + JsonContext nestedArray() { + return new JsonContext(this, JsonContextType.ARRAY); + } + } + + private JsonContext context = new JsonContext(null, JsonContextType.TOP_LEVEL); + private State state = State.INITIAL; + + public SpringJsonWriter(StringBuffer buffer) { + this.buffer = buffer; + } + + @Override + public void flush() {} + + @Override + public void writeBinaryData(BsonBinary binary) { + + preWriteValue(); + writeStartDocument(); + + writeName("$binary"); + + writeStartDocument(); + writeName("base64"); + writeString(Base64.getEncoder().encodeToString(binary.getData())); + writeName("subType"); + writeInt32(binary.getBsonType().getValue()); + writeEndDocument(); + + writeEndDocument(); + } + + @Override + public void writeBinaryData(String name, BsonBinary binary) { + + writeName(name); + writeBinaryData(binary); + } + + @Override + public void writeBoolean(boolean value) { + + preWriteValue(); + write(value ? "true" : "false"); + setNextState(); + } + + @Override + public void writeBoolean(String name, boolean value) { + + writeName(name); + writeBoolean(value); + } + + @Override + public void writeDateTime(long value) { + + // "$date": "2018-11-10T22:26:12.111Z" + writeStartDocument(); + writeName("$date"); + writeString(ZonedDateTime.ofInstant(Instant.ofEpochMilli(value), ZoneId.of("Z")).format(ISO_OFFSET_DATE_TIME)); + writeEndDocument(); + } + + @Override + public void writeDateTime(String name, long value) { + + writeName(name); + writeDateTime(value); + } + + @Override + public void writeDBPointer(BsonDbPointer value) { + + } + + @Override + public void writeDBPointer(String name, BsonDbPointer value) { + + } + + @Override // {"$numberDouble":"10.5"} + public void writeDouble(double value) { + + writeStartDocument(); + writeName("$numberDouble"); + writeString(Double.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeDouble(String name, double value) { + + writeName(name); + writeDouble(value); + } + + @Override + public void writeEndArray() { + write("]"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeEndDocument() { + buffer.append("}"); + context = context.parentContext; + if (context.contextType == JsonContextType.TOP_LEVEL) { + state = State.DONE; + } else { + setNextState(); + } + } + + @Override + public void writeInt32(int value) { + + writeStartDocument(); + writeName("$numberInt"); + writeString(Integer.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt32(String name, int value) { + + writeName(name); + writeInt32(value); + } + + @Override + public void writeInt64(long value) { + + writeStartDocument(); + writeName("$numberLong"); + writeString(Long.valueOf(value).toString()); + writeEndDocument(); + } + + @Override + public void writeInt64(String name, long value) { + + writeName(name); + writeInt64(value); + } + + @Override + public void writeDecimal128(Decimal128 value) { + + // { "$numberDecimal": "" } + writeStartDocument(); + writeName("$numberDecimal"); + writeString(value.toString()); + writeEndDocument(); + } + + @Override + public void writeDecimal128(String name, Decimal128 value) { + + writeName(name); + writeDecimal128(value); + } + + @Override + public void writeJavaScript(String code) { + + writeStartDocument(); + writeName("$code"); + writeString(code); + writeEndDocument(); + } + + @Override + public void writeJavaScript(String name, String code) { + + writeName(name); + writeJavaScript(code); + } + + @Override + public void writeJavaScriptWithScope(String code) { + + } + + @Override + public void writeJavaScriptWithScope(String name, String code) { + + } + + @Override + public void writeMaxKey() { + + writeStartDocument(); + writeName("$maxKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMaxKey(String name) { + writeName(name); + writeMaxKey(); + } + + @Override + public void writeMinKey() { + + writeStartDocument(); + writeName("$minKey"); + buffer.append(1); + writeEndDocument(); + } + + @Override + public void writeMinKey(String name) { + writeName(name); + writeMinKey(); + } + + @Override + public void writeName(String name) { + if (context.hasElements) { + write(","); + } else { + context.hasElements = true; + } + + writeString(name); + buffer.append(":"); + state = State.VALUE; + } + + @Override + public void writeNull() { + buffer.append("null"); + } + + @Override + public void writeNull(String name) { + writeName(name); + writeNull(); + } + + @Override + public void writeObjectId(ObjectId objectId) { + writeStartDocument(); + writeName("$oid"); + writeString(objectId.toHexString()); + writeEndDocument(); + } + + @Override + public void writeObjectId(String name, ObjectId objectId) { + writeName(name); + writeObjectId(objectId); + } + + @Override + public void writeRegularExpression(BsonRegularExpression regularExpression) { + + writeStartDocument(); + writeName("$regex"); + + write("/"); + write(regularExpression.getPattern()); + write("/"); + + if (StringUtils.hasText(regularExpression.getOptions())) { + writeName("$options"); + writeString(regularExpression.getOptions()); + } + + writeEndDocument(); + } + + @Override + public void writeRegularExpression(String name, BsonRegularExpression regularExpression) { + writeName(name); + writeRegularExpression(regularExpression); + } + + @Override + public void writeStartArray() { + + preWriteValue(); + write("["); + context = context.nestedArray(); + } + + @Override + public void writeStartArray(String name) { + writeName(name); + writeStartArray(); + } + + @Override + public void writeStartDocument() { + + preWriteValue(); + write("{"); + context = context.nestedDocument(); + state = State.NAME; + } + + @Override + public void writeStartDocument(String name) { + writeName(name); + writeStartDocument(); + } + + @Override + public void writeString(String value) { + write("'"); + write(value); + write("'"); + } + + @Override + public void writeString(String name, String value) { + writeName(name); + writeString(value); + } + + @Override + public void writeSymbol(String value) { + + writeStartDocument(); + writeName("$symbol"); + writeString(value); + writeEndDocument(); + } + + @Override + public void writeSymbol(String name, String value) { + + writeName(name); + writeSymbol(value); + } + + @Override // {"$timestamp": {"t": , "i": }} + public void writeTimestamp(BsonTimestamp value) { + + preWriteValue(); + writeStartDocument(); + writeName("$timestamp"); + writeStartDocument(); + writeName("t"); + buffer.append(value.getTime()); + writeName("i"); + buffer.append(value.getInc()); + writeEndDocument(); + writeEndDocument(); + } + + @Override + public void writeTimestamp(String name, BsonTimestamp value) { + + writeName(name); + writeTimestamp(value); + } + + @Override + public void writeUndefined() { + + writeStartDocument(); + writeName("$undefined"); + writeBoolean(true); + writeEndDocument(); + } + + @Override + public void writeUndefined(String name) { + + writeName(name); + writeUndefined(); + } + + @Override + public void pipe(BsonReader reader) { + + } + + public void writePlaceholder(String placeholder) { + write(placeholder); + } + + private void write(String str) { + buffer.append(str); + } + + private void preWriteValue() { + + if (context.contextType == JsonContextType.ARRAY) { + if (context.hasElements) { + write(","); + } + } + context.hasElements = true; + } + + private void setNextState() { + if (context.contextType == JsonContextType.ARRAY) { + state = State.VALUE; + } else { + state = State.NAME; + } + } +} diff --git a/spring-data-mongodb/src/test/java/example/aot/User.java b/spring-data-mongodb/src/test/java/example/aot/User.java new file mode 100644 index 0000000000..28ea5911ed --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/User.java @@ -0,0 +1,102 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.aot; + +import java.time.Instant; + +import org.springframework.data.mongodb.core.mapping.Field; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class User { + + String id; + + String username; + + @Field("first_name") String firstname; + + @Field("last_name") String lastname; + + Instant registrationDate; + Instant lastSeen; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFirstname() { + return firstname; + } + + public void setFirstname(String firstname) { + this.firstname = firstname; + } + + public String getLastname() { + return lastname; + } + + public void setLastname(String lastname) { + this.lastname = lastname; + } + + public Instant getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Instant registrationDate) { + this.registrationDate = registrationDate; + } + + public Instant getLastSeen() { + return lastSeen; + } + + public void setLastSeen(Instant lastSeen) { + this.lastSeen = lastSeen; + } +} diff --git a/spring-data-mongodb/src/test/java/example/aot/UserProjection.java b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java new file mode 100644 index 0000000000..06c70f8060 --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java @@ -0,0 +1,29 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.aot; + +import java.time.Instant; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserProjection { + + String getUsername(); + + Instant getLastSeen(); +} diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java new file mode 100644 index 0000000000..104fd8d08e --- /dev/null +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -0,0 +1,146 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package example.aot; + +import java.util.List; +import java.util.Optional; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.repository.CrudRepository; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public interface UserRepository extends CrudRepository { + + /* Derived Queries */ + + List findUserNoArgumentsBy(); + + User findOneByUsername(String username); + + Optional findOptionalOneByUsername(String username); + + Long countUsersByLastname(String lastname); + + Boolean existsUserByLastname(String lastname); + + List findByLastnameStartingWith(String lastname); + + List findTop2ByLastnameStartingWith(String lastname); + + List findByLastnameStartingWithOrderByUsername(String lastname); + + List findByLastnameStartingWith(String lastname, Limit limit); + + List findByLastnameStartingWith(String lastname, Sort sort); + + List findByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + List findByLastnameStartingWith(String lastname, Pageable page); + + Page findPageOfUsersByLastnameStartingWith(String lastname, Pageable page); + + Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); + + // TODO: Streaming + // TODO: Scrolling + // TODO: GeoQueries + + /* Annotated Queries */ + + @Query("{ 'username' : ?0 }") + User findAnnotatedQueryByUsername(String username); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", count = true) + Long countAnnotatedQueryByLastname(String lastname); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname); + + @Query(""" + { + 'lastname' : { + '$regex' : '^?0' + } + }""") + List findAnnotatedMultilineQueryByLastname(String username); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Limit limit, Sort sort); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + List findAnnotatedQueryByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Page findAnnotatedQueryPageOfUsersByLastname(String lastname, Pageable pageable); + + @Query("{ 'lastname' : { '$regex' : '^?0' } }") + Slice findAnnotatedQuerySliceOfUsersByLastname(String lastname, Pageable pageable); + + /* deletes */ + + User deleteByUsername(String username); + + @Query(value = "{ 'username' : ?0 }", delete = true) + User deleteAnnotatedQueryByUsername(String username); + + Long deleteByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + Long deleteAnnotatedQueryByLastnameStartingWith(String lastname); + + List deleteUsersByLastnameStartingWith(String lastname); + + @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) + List deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname); + + // TODO: updates + // TODO: Aggregations + + /* Derived With Annotated Options */ + + @Query(sort = "{ 'username' : 1 }") + List findWithAnnotatedSortByLastnameStartingWith(String lastname); + + @Query(fields = "{ 'username' : 1 }") + List findWithAnnotatedFieldsProjectionByLastnameStartingWith(String lastname); + + @ReadPreference("no-such-read-preference") + User findWithReadPreferenceByUsername(String username); + + // TODO: hints + + /* Projecting Queries */ + + List findUserProjectionByLastnameStartingWith(String lastname); + + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java new file mode 100644 index 0000000000..bef0d34cb4 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.util.List; + +import example.aot.User; +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class DemoRepo { + + + MongoOperations operations; + + List method1(String username) { + + BindableMongoExpression filter = new BindableMongoExpression("{ 'username', ?0 }", operations.getConverter(), new Object[]{username}); + Query query = new BasicQuery(filter.toDocument()); + + return operations.query(User.class) + .as(User.class) + .matching(query) + .all(); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java new file mode 100644 index 0000000000..9caf74f31c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java @@ -0,0 +1,662 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import example.aot.User; +import example.aot.UserProjection; +import example.aot.UserRepository; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Optional; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.data.util.Lazy; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.util.StringUtils; + +import com.mongodb.client.MongoClient; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +@ExtendWith(MongoClientExtension.class) +public class MongoRepositoryContributorTests { + + private static final String DB_NAME = "aot-repo-tests"; + private static Verifyer generated; + + @Client static MongoClient client; + + @BeforeAll + static void beforeAll() { + + TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null); + TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); + + new MongoRepositoryContributor(aotContext).contribute(generationContext); + + AbstractBeanDefinition mongoTemplate = BeanDefinitionBuilder.rootBeanDefinition(MongoTestTemplate.class) + .addConstructorArgValue(DB_NAME).getBeanDefinition(); + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition("example.aot.UserRepositoryImpl__Aot").addConstructorArgReference("mongoOperations") + .getBeanDefinition(); + + generated = generateContext(generationContext) // + .register("mongoOperations", mongoTemplate) // + .register("aotUserRepository", aotGeneratedRepository); + } + + @BeforeEach + void beforeEach() { + + MongoTestUtils.flushCollection(DB_NAME, "user", client); + initUsers(); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + generated.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findOneByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + generated.verify(methodInvoker -> { + + Optional user = methodInvoker.invoke("findOptionalOneByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda")); + }); + } + + @Test + void testDerivedCount() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testDerivedExists() { + + generated.verify(methodInvoker -> { + + Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(exists).isTrue(); + }); + } + + @Test + void testDerivedFinderWithoutArguments() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository"); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + }); + } + + @Test + void testCountWorksAsExpected() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testDerivedFinderReturningList() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han"); + }); + } + + @Test + void testLimitedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testSortedDerivedFinder() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWithOrderByUsername", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testDerivedFinderWithSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username"), Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testDerivedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningSingleValueWithQuery() { + + generated.verify(methodInvoker -> { + + User user = methodInvoker.invoke("findAnnotatedQueryByUsername", "yoda").onBean("aotUserRepository"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + } + + @Test + void testAnnotatedCount() { + + generated.verify(methodInvoker -> { + + Long value = methodInvoker.invoke("countAnnotatedQueryByLastname", "Skywalker").onBean("aotUserRepository"); + assertThat(value).isEqualTo(2L); + }); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2)) + .onBean("aotUserRepository"); + assertThat(users).hasSize(2); + }); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("username")) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker + .invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningPage() { + + generated.verify(methodInvoker -> { + + Page page = methodInvoker + .invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + generated.verify(methodInvoker -> { + + Slice slice = methodInvoker + .invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + }); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteByUsername", "deleteAnnotatedQueryByUsername" }) + void testDeleteSingle(String methodName) { + + generated.verify(methodInvoker -> { + + User result = methodInvoker.invoke(methodName, "yoda").onBean("aotUserRepository"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteByLastnameStartingWith", "deleteAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleteCount(String methodName) { + + generated.verify(methodInvoker -> { + + Long result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).isEqualTo(4L); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @ParameterizedTest + @ValueSource(strings = { "deleteUsersByLastnameStartingWith", "deleteUsersAnnotatedQueryByLastnameStartingWith" }) + void testDerivedDeleteMultipleReturningDeleted(String methodName) { + + generated.verify(methodInvoker -> { + + List result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + }); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedFinderWithAnnotatedSort() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + }); + } + + @Test + void testDerivedFinderWithAnnotatedFieldsProjection() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findWithAnnotatedFieldsProjectionByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).allMatch( + user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null); + }); + } + + @Test + void testReadPreferenceAppliedToQuery() { + + generated.verify(methodInvoker -> { + + // check if it fails when trying to parse the read preference to indicate it would get applied + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> methodInvoker.invoke("findWithReadPreferenceByUsername", "S").onBean("aotUserRepository")) + .withMessageContaining("No match for read preference"); + }); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + generated.verify(methodInvoker -> { + + List users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S") + .onBean("aotUserRepository"); + assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", + "vader"); + }); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + generated.verify(methodInvoker -> { + + Page users = methodInvoker + .invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) + .onBean("aotUserRepository"); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + }); + } + + private static void initUsers() { + + Document luke = Document.parse(""" + { + "_id": "id-1", + "username": "luke", + "first_name": "Luke", + "last_name": "Skywalker", + "posts": [ + { + "message": "I have a bad feeling about this.", + "date": { + "$date": "2025-01-15T12:50:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document leia = Document.parse(""" + { + "_id": "id-2", + "username": "leia", + "first_name": "Leia", + "last_name": "Organa", + "_class": "example.springdata.aot.User" + }"""); + + Document han = Document.parse(""" + { + "_id": "id-3", + "username": "han", + "first_name": "Han", + "last_name": "Solo", + "posts": [ + { + "message": "It's the ship that made the Kessel Run in less than 12 Parsecs.", + "date": { + "$date": "2025-01-15T13:30:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document chwebacca = Document.parse(""" + { + "_id": "id-4", + "username": "chewbacca", + "_class": "example.springdata.aot.User" + }"""); + + Document yoda = Document.parse( + """ + { + "_id": "id-5", + "username": "yoda", + "posts": [ + { + "message": "Do. Or do not. There is no try.", + "date": { + "$date": "2025-01-15T13:09:33.855Z" + } + }, + { + "message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.", + "date": { + "$date": "2025-01-15T13:53:33.855Z" + } + } + ] + }"""); + + Document vader = Document.parse(""" + { + "_id": "id-6", + "username": "vader", + "first_name": "Anakin", + "last_name": "Skywalker", + "posts": [ + { + "message": "I am your father", + "date": { + "$date": "2025-01-15T13:46:33.855Z" + } + } + ] + }"""); + + Document kylo = Document.parse(""" + { + "_id": "id-7", + "username": "kylo", + "first_name": "Ben", + "last_name": "Solo" + } + """); + + client.getDatabase(DB_NAME).getCollection("user") + .insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo)); + } + + static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) { + return new GeneratedContextBuilder(generationContext); + } + + static class GeneratedContextBuilder implements Verifyer { + + TestGenerationContext generationContext; + Map beanDefinitions = new LinkedHashMap<>(); + Lazy lazyFactory; + + public GeneratedContextBuilder(TestGenerationContext generationContext) { + + this.generationContext = generationContext; + this.lazyFactory = Lazy.of(() -> { + DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> { + + freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); + for (Entry entry : beanDefinitions.entrySet()) { + freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue()); + } + }); + return freshBeanFactory; + }); + } + + GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) { + this.beanDefinitions.put(name, beanDefinition); + return this; + } + + public Verifyer verify(Consumer methodInvoker) { + methodInvoker.accept(new GeneratedContext(lazyFactory)); + return this; + } + + } + + interface Verifyer { + Verifyer verify(Consumer methodInvoker); + } + + static class GeneratedContext { + + private Supplier delegate; + + public GeneratedContext(Supplier defaultListableBeanFactory) { + this.delegate = defaultListableBeanFactory; + } + + InvocationBuilder invoke(String method, Object... arguments) { + + return new InvocationBuilder() { + @Override + public T onBean(String beanName) { + Object bean = delegate.get().getBean(beanName); + return ReflectionTestUtils.invokeMethod(bean, method, arguments); + } + }; + } + + interface InvocationBuilder { + T onBean(String beanName); + } + + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java new file mode 100644 index 0000000000..52d609be63 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java @@ -0,0 +1,144 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.lang.reflect.Method; +import java.util.Set; + +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; +import org.springframework.data.repository.core.CrudMethods; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.util.Streamable; +import org.springframework.data.util.TypeInformation; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ + +class StubRepositoryInformation implements RepositoryInformation { + + private final RepositoryMetadata metadata; + private final RepositoryComposition baseComposition; + + public StubRepositoryInformation(Class repositoryInterface, @Nullable RepositoryComposition composition) { + + this.metadata = AbstractRepositoryMetadata.getMetadata(repositoryInterface); + this.baseComposition = composition != null ? composition + : RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class)); + } + + @Override + public TypeInformation getIdTypeInformation() { + return metadata.getIdTypeInformation(); + } + + @Override + public TypeInformation getDomainTypeInformation() { + return metadata.getDomainTypeInformation(); + } + + @Override + public Class getRepositoryInterface() { + return metadata.getRepositoryInterface(); + } + + @Override + public TypeInformation getReturnType(Method method) { + return metadata.getReturnType(method); + } + + @Override + public Class getReturnedDomainClass(Method method) { + return metadata.getReturnedDomainClass(method); + } + + @Override + public CrudMethods getCrudMethods() { + return metadata.getCrudMethods(); + } + + @Override + public boolean isPagingRepository() { + return false; + } + + @Override + public Set> getAlternativeDomainTypes() { + return null; + } + + @Override + public boolean isReactiveRepository() { + return false; + } + + @Override + public Set> getFragments() { + return null; + } + + @Override + public boolean isBaseClassMethod(Method method) { + return baseComposition.findMethod(method).isPresent(); + } + + @Override + public boolean isCustomMethod(Method method) { + return false; + } + + @Override + public boolean isQueryMethod(Method method) { + return false; + } + + @Override + public Streamable getQueryMethods() { + return null; + } + + @Override + public Class getRepositoryBaseClass() { + return SimpleMongoRepository.class; + } + + @Override + public Method getTargetClassMethod(Method method) { + return null; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java new file mode 100644 index 0000000000..814710e51b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java @@ -0,0 +1,129 @@ +/* + * Copyright 2025. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.aot.generated; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.List; +import java.util.Set; + +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; +import org.springframework.core.test.tools.ClassFile; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.io.ClassPathResource; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.lang.Nullable; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class TestMongoAotRepositoryContext implements AotRepositoryContext { + + private final StubRepositoryInformation repositoryInformation; + private final Environment environment = new StandardEnvironment(); + + public TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public Set getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set> getIdentifyingAnnotations() { + return Set.of(Document.class); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set> getResolvedTypes() { + return Set.of(); + } + + public List getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); + } + } + + @Override + public Environment getEnvironment() { + return environment; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java index 53bd7c6ab7..771c17c4a9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java @@ -23,6 +23,7 @@ import java.util.function.Supplier; import java.util.stream.Collectors; +import com.mongodb.client.MongoClients; import org.bson.Document; import org.springframework.context.ApplicationContext; import org.springframework.data.mapping.callback.EntityCallbacks; @@ -44,6 +45,14 @@ public class MongoTestTemplate extends MongoTemplate { private final MongoTestTemplateConfiguration cfg; + public MongoTestTemplate() { + this("test"); + } + + public MongoTestTemplate(String databaseName) { + this(MongoClients.create(), databaseName); + } + public MongoTestTemplate(MongoClient client, String database, Class... initialEntities) { this(cfg -> { cfg.configureDatabaseFactory(it -> { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java new file mode 100644 index 0000000000..57b8df548d --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java @@ -0,0 +1,159 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.util.json; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.bson.BsonRegularExpression; +import org.bson.BsonTimestamp; +import org.bson.types.Decimal128; +import org.bson.types.ObjectId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** + * @author Christoph Strobl + * @since 2025/01 + */ +public class SpringJsonWriterUnitTests { + + StringBuffer buffer; + SpringJsonWriter writer; + + @BeforeEach + void beforeEach() { + buffer = new StringBuffer(); + writer = new SpringJsonWriter(buffer); + } + + @Test + void writeDocumentWithSingleEntry() { + + writer.writeStartDocument(); + writer.writeString("key", "value"); + writer.writeEndDocument(); + + assertThat(buffer).isEqualToIgnoringWhitespace("{'key':'value'}"); + } + + @Test + void writeDocumentWithMultipleEntries() { + + writer.writeStartDocument(); + writer.writeString("key-1", "v1"); + writer.writeString("key-2", "v2"); + writer.writeEndDocument(); + + assertThat(buffer).isEqualToIgnoringWhitespace("{'key-1':'v1','key-2':'v2'}"); + } + + @Test + void writeInt32() { + + writer.writeInt32("int32", 32); + + assertThat(buffer).isEqualToIgnoringWhitespace("'int32':{'$numberInt':'32'}"); + } + + @Test + void writeInt64() { + + writer.writeInt64("int64", 64); + + assertThat(buffer).isEqualToIgnoringWhitespace("'int64':{'$numberLong':'64'}"); + } + + @Test + void writeDouble() { + + writer.writeDouble("double", 42.24D); + + assertThat(buffer).isEqualToIgnoringWhitespace("'double':{'$numberDouble':'42.24'}"); + } + + @Test + void writeDecimal128() { + + writer.writeDecimal128("decimal128", new Decimal128(128L)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'decimal128':{'$numberDecimal':'128'}"); + } + + @Test + void writeObjectId() { + + ObjectId objectId = new ObjectId(); + writer.writeObjectId("_id", objectId); + + assertThat(buffer).isEqualToIgnoringWhitespace("'_id':{'$oid':'%s'}".formatted(objectId.toHexString())); + } + + @Test + void writeRegex() { + + String pattern = "^H"; + writer.writeRegularExpression("name", new BsonRegularExpression(pattern)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/}".formatted(pattern)); + } + + @Test + void writeRegexWithOptions() { + + String pattern = "^H"; + writer.writeRegularExpression("name", new BsonRegularExpression(pattern, "i")); + + assertThat(buffer).isEqualToIgnoringWhitespace("'name':{'$regex':/%s/,'$options':'%s'}".formatted(pattern, "i")); + } + + @Test + void writeTimestamp() { + + writer.writeTimestamp("ts", new BsonTimestamp(1234, 567)); + + assertThat(buffer).isEqualToIgnoringWhitespace("'ts':{'$timestamp':{'t':1234,'i':567}}"); + } + + @Test + void writeUndefined() { + + writer.writeUndefined("nope"); + + assertThat(buffer).isEqualToIgnoringWhitespace("'nope':{'$undefined':true}"); + } + + @Test + void writeArrayWithSingleEntry() { + + writer.writeStartArray(); + writer.writeInt32(42); + writer.writeEndArray(); + + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'}]"); + } + + @Test + void writeArrayWithMultipleEntries() { + + writer.writeStartArray(); + writer.writeInt32(42); + writer.writeInt64(24); + writer.writeEndArray(); + + assertThat(buffer).isEqualToIgnoringNewLines("[{'$numberInt':'42'},{'$numberLong':'24'}]"); + } + +} diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 64550c957c..9a65ce79b8 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -18,6 +18,7 @@ + From 2aac1a59bece4042079c745e69e925c0fbca0c4b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 28 Mar 2025 11:59:57 +0100 Subject: [PATCH 43/74] Adopt to Commons changes. See: #4939 --- .../mongodb/aot/generated/MongoBlocks.java | 99 ++++++++++++------- .../generated/MongoRepositoryContributor.java | 80 +++++++++------ .../core/convert/MappingMongoConverter.java | 6 ++ 3 files changed, 117 insertions(+), 68 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java index 1f550d814e..d811ee9a5a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java @@ -18,10 +18,13 @@ import java.lang.reflect.Parameter; import java.util.Arrays; import java.util.List; +import java.util.Optional; import java.util.regex.Pattern; import java.util.stream.Collectors; import org.bson.Document; + +import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.mongodb.BindableMongoExpression; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; @@ -33,7 +36,8 @@ import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; @@ -50,25 +54,29 @@ public class MongoBlocks { private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - static QueryBlockBuilder queryBlockBuilder(AotRepositoryMethodGenerationContext context) { - return new QueryBlockBuilder(context); + static QueryBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + return new QueryBlockBuilder(context, queryMethod); } - static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { - return new QueryExecutionBlockBuilder(context); + static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new QueryExecutionBlockBuilder(context, queryMethod); } - static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { - return new DeleteExecutionBuilder(context); + static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new DeleteExecutionBuilder(context, queryMethod); } static class DeleteExecutionBuilder { - AotRepositoryMethodGenerationContext context; + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; String queryVariableName; - public DeleteExecutionBuilder(AotRepositoryMethodGenerationContext context) { + public DeleteExecutionBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { this.context = context; + this.queryMethod = queryMethod; } public DeleteExecutionBuilder referencing(String queryVariableName) { @@ -85,15 +93,16 @@ public CodeBlock build() { && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType() + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : context.getRepositoryInformation().getDomainType(); builder.add("\n"); - builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, actualReturnType, + builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, + context.getRepositoryInformation().getDomainType(), mongoOpsRef, context.getRepositoryInformation().getDomainType()); Type type = Type.FIND_AND_REMOVE_ALL; - if (context.returnsSingleValue()) { + if (!queryMethod.isCollectionQuery()) { if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { type = Type.FIND_AND_REMOVE_ONE; } else { @@ -103,7 +112,7 @@ public CodeBlock build() { actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) ? ClassName.get(context.getMethod().getReturnType()) - : context.returnsSingleValue() ? actualReturnType : context.getReturnType(); + : queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType; builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class, DeleteExecutionX.Type.class, type.name(), queryVariableName); @@ -114,11 +123,14 @@ public CodeBlock build() { static class QueryExecutionBlockBuilder { - AotRepositoryMethodGenerationContext context; + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; private String queryVariableName; + private boolean count, exists; - public QueryExecutionBlockBuilder(AotRepositoryMethodGenerationContext context) { + public QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { this.context = context; + this.queryMethod = queryMethod; } QueryExecutionBlockBuilder referencing(String queryVariableName) { @@ -127,16 +139,24 @@ QueryExecutionBlockBuilder referencing(String queryVariableName) { return this; } + QueryExecutionBlockBuilder count(boolean count) { + this.count = count; + return this; + } + + QueryExecutionBlockBuilder exists(boolean exists) { + this.exists = exists; + return this; + } + CodeBlock build() { String mongoOpsRef = context.fieldNameOf(MongoOperations.class); Builder builder = CodeBlock.builder(); - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType() + boolean isProjecting = context.getReturnedType().isProjecting(); + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : context.getRepositoryInformation().getDomainType(); builder.add("\n"); @@ -150,24 +170,23 @@ CodeBlock build() { context.getRepositoryInformation().getDomainType()); } - String terminatingMethod = "all()"; - if (context.returnsSingleValue()) { + String terminatingMethod; - if (context.returnsOptionalValue()) { - terminatingMethod = "one()"; - } else if (context.isCountMethod()) { - terminatingMethod = "count()"; - } else if (context.isExistsMethod()) { - terminatingMethod = "exists()"; - } else { - terminatingMethod = "oneValue()"; - } + if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { + terminatingMethod = "all()"; + } else if (count) { + terminatingMethod = "count()"; + + } else if (exists) { + terminatingMethod = "exists()"; + } else { + terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; } - if (context.returnsPage()) { + if (queryMethod.isPageQuery()) { builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, context.getPageableParameterName(), queryVariableName); - } else if (context.returnsSlice()) { + } else if (queryMethod.isSliceQuery()) { builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, context.getPageableParameterName(), queryVariableName); } else { @@ -181,12 +200,14 @@ CodeBlock build() { static class QueryBlockBuilder { - AotRepositoryMethodGenerationContext context; + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + StringQuery source; List arguments; private String queryVariableName; - public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { + public QueryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { this.context = context; this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) .collect(Collectors.toList()); @@ -194,6 +215,7 @@ public QueryBlockBuilder(AotRepositoryMethodGenerationContext context) { // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); // this.argumentSource = new MongoParameters(parametersSource, false); + this.queryMethod = queryMethod; } public QueryBlockBuilder filter(StringQuery query) { @@ -239,17 +261,20 @@ CodeBlock build() { } String pageableParameter = context.getPageableParameterName(); - if (StringUtils.hasText(pageableParameter) && !context.returnsPage() && !context.returnsSlice()) { + if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) { builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); } - String hint = context.annotationValue(Hint.class, "value"); + MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); + String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; if (StringUtils.hasText(hint)) { builder.addStatement("$L.withHint($S)", queryVariableName, hint); } - String readPreference = context.annotationValue(ReadPreference.class, "value"); + MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); + String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; + if (StringUtils.hasText(readPreference)) { builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, com.mongodb.ReadPreference.class, readPreference); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java index d42afd61bc..2ad12cfba5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java @@ -15,20 +15,27 @@ */ package org.springframework.data.mongodb.aot.generated; +import java.lang.reflect.Method; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder; import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryMethodGenerationContext; +import org.springframework.data.repository.aot.generate.MethodContributor; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; import org.springframework.util.StringUtils; @@ -38,11 +45,13 @@ */ public class MongoRepositoryContributor extends RepositoryContributor { - private AotQueryCreator queryCreator; + private final AotQueryCreator queryCreator; + private final MongoMappingContext mappingContext; public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { super(repositoryContext); this.queryCreator = new AotQueryCreator(); + this.mappingContext = new MongoMappingContext(); } @Override @@ -51,36 +60,43 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB } @Override - protected AotRepositoryMethodBuilder contributeRepositoryMethod( - AotRepositoryMethodGenerationContext generationContext) { - - // TODO: do not generate stuff for spel expressions + protected @Nullable MethodContributor contributeQueryMethod(Method method, + RepositoryInformation repositoryInformation) { - if (AnnotatedElementUtils.hasAnnotation(generationContext.getMethod(), Aggregation.class)) { + if (AnnotatedElementUtils.hasAnnotation(method, Aggregation.class)) { return null; } - { - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(generationContext.getMethod(), Query.class); - if (queryAnnotation != null) { - if (StringUtils.hasText(queryAnnotation.value()) - && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { - return null; - } + + Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); + if (queryAnnotation != null) { + if (StringUtils.hasText(queryAnnotation.value()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { + return null; } } - // so the rest should work - return new AotRepositoryMethodBuilder(generationContext).customize((context, body) -> { + MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(), + mappingContext); + + return MethodContributor.forQueryMethod(queryMethod).contribute(context -> { + CodeBlock.Builder builder = CodeBlock.builder(); - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(context.getMethod(), Query.class); + boolean count, delete, exists; StringQuery query; if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.value())) { query = new StringQuery(queryAnnotation.value()); + count = queryAnnotation.count(); + delete = queryAnnotation.delete(); + exists = queryAnnotation.exists(); } else { PartTree partTree = new PartTree(context.getMethod().getName(), context.getRepositoryInformation().getDomainType()); query = queryCreator.createQuery(partTree, context.getMethod().getParameterCount()); + count = partTree.isCountProjection(); + delete = partTree.isDelete(); + exists = partTree.isExistsProjection(); + } if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { @@ -90,29 +106,31 @@ protected AotRepositoryMethodBuilder contributeRepositoryMethod( query.fields(queryAnnotation.fields()); } - writeStringQuery(context, body, query); + writeStringQuery(context, builder, count, delete, exists, query, queryMethod); + + return builder.build(); }); } - private static void writeStringQuery(AotRepositoryMethodGenerationContext context, Builder body, StringQuery query) { + private static void writeStringQuery(AotQueryMethodGenerationContext context, CodeBlock.Builder body, boolean count, + boolean delete, boolean exists, StringQuery query, MongoQueryMethod queryMethod) { - body.addCode(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context).filter(query); + body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context, queryMethod).filter(query); - if (context.isDeleteMethod()) { + if (delete) { String deleteQueryVariableName = "deleteQuery"; - body.addCode(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); - body.addCode(MongoBlocks.deleteExecutionBlockBuilder(context).referencing(deleteQueryVariableName).build()); + body.add(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); + body.add( + MongoBlocks.deleteExecutionBlockBuilder(context, queryMethod).referencing(deleteQueryVariableName).build()); } else { String filterQueryVariableName = "filterQuery"; - body.addCode(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); - body.addCode(MongoBlocks.queryExecutionBlockBuilder(context).referencing(filterQueryVariableName).build()); + body.add(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); + body.add(MongoBlocks.queryExecutionBlockBuilder(context, queryMethod).exists(exists).count(count) + .referencing(filterQueryVariableName).build()); } } - private static void userAnnotatedQuery(AotRepositoryMethodGenerationContext context, Builder body, Query query) { - writeStringQuery(context, body, new StringQuery(query.value())); - } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java index 2d073869ae..24c3c2f590 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/MappingMongoConverter.java @@ -45,6 +45,7 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.CollectionFactory; +import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; @@ -2223,6 +2224,11 @@ public org.springframework.data.util.TypeInformation specialize(Typ public TypeDescriptor toTypeDescriptor() { return delegate.toTypeDescriptor(); } + + @Override + public ResolvableType toResolvableType() { + return delegate.toResolvableType(); + } } /** From 6c6438ec21918b76b4a5b9f00b85aff48ead28cb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 7 Apr 2025 11:26:36 +0200 Subject: [PATCH 44/74] Extend AOT Repository Support. - Introduce AOT fragment base class. - Refactor Delete execution to be reusable. - Add support for updates. - Add support for aggregations. - Move types to repository package. - Update documentation. See: #4939 --- .../mongodb/aot/generated/MongoBlocks.java | 315 ------- .../generated/MongoRepositoryContributor.java | 136 --- .../data/mongodb/core/query/Criteria.java | 46 +- .../core/query/CriteriaDefinition.java | 28 +- .../aot/AggregationInteraction.java | 56 ++ .../aot/AggregationUpdateInteraction.java | 52 ++ .../aot/AotMongoRepositoryPostProcessor.java | 4 +- .../aot}/AotQueryCreator.java | 42 +- .../MongoAotRepositoryFragmentSupport.java | 147 ++++ .../repository/aot/MongoCodeBlocks.java | 777 ++++++++++++++++++ .../repository/aot/MongoInteraction.java | 62 ++ .../aot/MongoRepositoryContributor.java | 286 +++++++ .../repository/aot/QueryInteraction.java | 83 ++ .../repository/aot/StringAggregation.java | 26 + .../aot}/StringQuery.java | 51 +- .../mongodb/repository/aot/StringUpdate.java | 27 + .../repository/aot/UpdateInteraction.java | 58 ++ .../repository/query/AbstractMongoQuery.java | 5 +- .../repository/query/MongoQueryCreator.java | 35 +- .../repository/query/MongoQueryExecution.java | 77 +- .../repository/query/MongoQueryMethod.java | 4 +- .../data/mongodb/util/BsonUtils.java | 44 +- .../util/{json => }/SpringJsonWriter.java | 15 +- .../src/test/java/example/aot/User.java | 28 +- .../test/java/example/aot/UserProjection.java | 1 - .../test/java/example/aot/UserRepository.java | 142 +++- .../data/mongodb/aot/generated/DemoRepo.java | 18 +- .../MongoRepositoryContributorTests.java | 662 --------------- .../AotFragmentTestConfigurationSupport.java | 127 +++ .../aot/MongoRepositoryContributorTests.java | 650 +++++++++++++++ .../aot}/StubRepositoryInformation.java | 39 +- .../aot}/TestMongoAotRepositoryContext.java | 27 +- .../query/AbstractMongoQueryUnitTests.java | 14 +- .../query/MongoQueryExecutionUnitTests.java | 37 +- .../{json => }/SpringJsonWriterUnitTests.java | 8 +- .../src/test/resources/logback.xml | 4 +- src/main/antora/modules/ROOT/nav.adoc | 1 + .../modules/ROOT/pages/mongodb/aot.adoc | 88 ++ 38 files changed, 2837 insertions(+), 1385 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/{aot/generated => repository/aot}/AotQueryCreator.java (84%) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/{aot/generated => repository/aot}/StringQuery.java (73%) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java rename spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/{json => }/SpringJsonWriter.java (96%) delete mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/{aot/generated => repository/aot}/StubRepositoryInformation.java (78%) rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/{aot/generated => repository/aot}/TestMongoAotRepositoryContext.java (77%) rename spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/{json => }/SpringJsonWriterUnitTests.java (96%) create mode 100644 src/main/antora/modules/ROOT/pages/mongodb/aot.adoc diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java deleted file mode 100644 index d811ee9a5a..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoBlocks.java +++ /dev/null @@ -1,315 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.aot.generated; - -import java.lang.reflect.Parameter; -import java.util.Arrays; -import java.util.List; -import java.util.Optional; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import org.bson.Document; - -import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.data.mongodb.BindableMongoExpression; -import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; -import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.query.BasicQuery; -import org.springframework.data.mongodb.repository.Hint; -import org.springframework.data.mongodb.repository.ReadPreference; -import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX; -import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecutionX.Type; -import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; -import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; -import org.springframework.data.mongodb.repository.query.MongoQueryMethod; -import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; -import org.springframework.javapoet.ClassName; -import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.javapoet.TypeName; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; - -/** - * @author Christoph Strobl - */ -public class MongoBlocks { - - private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); - - static QueryBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { - return new QueryBlockBuilder(context, queryMethod); - } - - static QueryExecutionBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context, - MongoQueryMethod queryMethod) { - return new QueryExecutionBlockBuilder(context, queryMethod); - } - - static DeleteExecutionBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context, - MongoQueryMethod queryMethod) { - return new DeleteExecutionBuilder(context, queryMethod); - } - - static class DeleteExecutionBuilder { - - private final AotQueryMethodGenerationContext context; - private final MongoQueryMethod queryMethod; - String queryVariableName; - - public DeleteExecutionBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { - this.context = context; - this.queryMethod = queryMethod; - } - - public DeleteExecutionBuilder referencing(String queryVariableName) { - this.queryVariableName = queryVariableName; - return this; - } - - public CodeBlock build() { - - String mongoOpsRef = context.fieldNameOf(MongoOperations.class); - Builder builder = CodeBlock.builder(); - - boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); - - Object actualReturnType = isProjecting ? context.getActualReturnType().getType() - : context.getRepositoryInformation().getDomainType(); - - builder.add("\n"); - builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, - context.getRepositoryInformation().getDomainType(), - mongoOpsRef, context.getRepositoryInformation().getDomainType()); - - Type type = Type.FIND_AND_REMOVE_ALL; - if (!queryMethod.isCollectionQuery()) { - if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { - type = Type.FIND_AND_REMOVE_ONE; - } else { - type = Type.ALL; - } - } - - actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) - ? ClassName.get(context.getMethod().getReturnType()) - : queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType; - - builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecutionX.class, - DeleteExecutionX.Type.class, type.name(), queryVariableName); - - return builder.build(); - } - } - - static class QueryExecutionBlockBuilder { - - private final AotQueryMethodGenerationContext context; - private final MongoQueryMethod queryMethod; - private String queryVariableName; - private boolean count, exists; - - public QueryExecutionBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { - this.context = context; - this.queryMethod = queryMethod; - } - - QueryExecutionBlockBuilder referencing(String queryVariableName) { - - this.queryVariableName = queryVariableName; - return this; - } - - QueryExecutionBlockBuilder count(boolean count) { - this.count = count; - return this; - } - - QueryExecutionBlockBuilder exists(boolean exists) { - this.exists = exists; - return this; - } - - CodeBlock build() { - - String mongoOpsRef = context.fieldNameOf(MongoOperations.class); - - Builder builder = CodeBlock.builder(); - - boolean isProjecting = context.getReturnedType().isProjecting(); - Object actualReturnType = isProjecting ? context.getActualReturnType().getType() - : context.getRepositoryInformation().getDomainType(); - - builder.add("\n"); - - if (isProjecting) { - builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, - mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); - } else { - - builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, - context.getRepositoryInformation().getDomainType()); - } - - String terminatingMethod; - - if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { - terminatingMethod = "all()"; - } else if (count) { - terminatingMethod = "count()"; - - } else if (exists) { - terminatingMethod = "exists()"; - } else { - terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; - } - - if (queryMethod.isPageQuery()) { - builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, - context.getPageableParameterName(), queryVariableName); - } else if (queryMethod.isSliceQuery()) { - builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, - context.getPageableParameterName(), queryVariableName); - } else { - builder.addStatement("return finder.matching($L).$L", queryVariableName, terminatingMethod); - } - - return builder.build(); - - } - } - - static class QueryBlockBuilder { - - private final AotQueryMethodGenerationContext context; - private final MongoQueryMethod queryMethod; - - StringQuery source; - List arguments; - private String queryVariableName; - - public QueryBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { - this.context = context; - this.arguments = Arrays.stream(context.getMethod().getParameters()).map(Parameter::getName) - .collect(Collectors.toList()); - - // ParametersSource parametersSource = ParametersSource.of(repositoryInformation, metadata.getRepositoryMethod()); - // this.argumentSource = new MongoParameters(parametersSource, false); - - this.queryMethod = queryMethod; - } - - public QueryBlockBuilder filter(StringQuery query) { - this.source = query; - return this; - } - - public QueryBlockBuilder usingQueryVariableName(String queryVariableName) { - this.queryVariableName = queryVariableName; - return this; - } - - CodeBlock build() { - - CodeBlock.Builder builder = CodeBlock.builder(); - - builder.add("\n"); - String queryDocumentVariableName = "%sDocument".formatted(queryVariableName); - builder.add(renderExpressionToDocument(source.getQueryString(), queryVariableName)); - builder.addStatement("$T $L = new $T($L)", BasicQuery.class, queryVariableName, BasicQuery.class, - queryDocumentVariableName); - - if (StringUtils.hasText(source.getFieldsString())) { - builder.add(renderExpressionToDocument(source.getFieldsString(), "fields")); - builder.addStatement("$L.setFieldsObject(fieldsDocument)", queryVariableName); - } - - String sortParameter = context.getSortParameterName(); - if (StringUtils.hasText(sortParameter)) { - - builder.addStatement("$L.with($L)", queryVariableName, sortParameter); - } else if (StringUtils.hasText(source.getSortString())) { - - builder.add(renderExpressionToDocument(source.getSortString(), "sort")); - builder.addStatement("$L.setSortObject(sortDocument)", queryVariableName); - } - - String limitParameter = context.getLimitParameterName(); - if (StringUtils.hasText(limitParameter)) { - builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); - } else if (context.getPageableParameterName() == null && source.isLimited()) { - builder.addStatement("$L.limit($L)", queryVariableName, source.getLimit()); - } - - String pageableParameter = context.getPageableParameterName(); - if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) { - builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); - } - - MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); - String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; - - if (StringUtils.hasText(hint)) { - builder.addStatement("$L.withHint($S)", queryVariableName, hint); - } - - MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); - String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; - - if (StringUtils.hasText(readPreference)) { - builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, - com.mongodb.ReadPreference.class, readPreference); - } - - // TODO: all the meta stuff - - return builder.build(); - } - - private CodeBlock renderExpressionToDocument(@Nullable String source, String variableName) { - - Builder builder = CodeBlock.builder(); - if (!StringUtils.hasText(source)) { - builder.addStatement("$T $L = new $T()", Document.class, "%sDocument".formatted(variableName), Document.class); - } else if (!containsPlaceholder(source)) { - builder.addStatement("$T $L = $T.parse($S)", Document.class, "%sDocument".formatted(variableName), - Document.class, source); - } else { - - String mongoOpsRef = context.fieldNameOf(MongoOperations.class); - String tmpVarName = "%sString".formatted(variableName); - - builder.addStatement("String $L = $S", tmpVarName, source); - builder.addStatement("$T $L = new $T($L, $L.getConverter(), new $T[]{ $L }).toDocument()", Document.class, - "%sDocument".formatted(variableName), BindableMongoExpression.class, tmpVarName, mongoOpsRef, Object.class, - StringUtils.collectionToDelimitedString(arguments, ", ")); - } - - return builder.build(); - } - - private boolean containsPlaceholder(String source) { - return PARAMETER_BINDING_PATTERN.matcher(source).find(); - } - - } -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java deleted file mode 100644 index 2ad12cfba5..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributor.java +++ /dev/null @@ -1,136 +0,0 @@ -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.aot.generated; - -import java.lang.reflect.Method; -import java.util.regex.Pattern; - -import org.jspecify.annotations.Nullable; - -import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.data.mongodb.aot.generated.MongoBlocks.QueryBlockBuilder; -import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.mapping.MongoMappingContext; -import org.springframework.data.mongodb.repository.Aggregation; -import org.springframework.data.mongodb.repository.Query; -import org.springframework.data.mongodb.repository.query.MongoQueryMethod; -import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; -import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.MethodContributor; -import org.springframework.data.repository.aot.generate.RepositoryContributor; -import org.springframework.data.repository.config.AotRepositoryContext; -import org.springframework.data.repository.core.RepositoryInformation; -import org.springframework.data.repository.query.QueryMethod; -import org.springframework.data.repository.query.parser.PartTree; -import org.springframework.javapoet.CodeBlock; -import org.springframework.javapoet.TypeName; -import org.springframework.util.StringUtils; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -public class MongoRepositoryContributor extends RepositoryContributor { - - private final AotQueryCreator queryCreator; - private final MongoMappingContext mappingContext; - - public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { - super(repositoryContext); - this.queryCreator = new AotQueryCreator(); - this.mappingContext = new MongoMappingContext(); - } - - @Override - protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { - constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); - } - - @Override - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { - - if (AnnotatedElementUtils.hasAnnotation(method, Aggregation.class)) { - return null; - } - - Query queryAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, Query.class); - if (queryAnnotation != null) { - if (StringUtils.hasText(queryAnnotation.value()) - && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryAnnotation.value()).find()) { - return null; - } - } - - MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(), - mappingContext); - - return MethodContributor.forQueryMethod(queryMethod).contribute(context -> { - CodeBlock.Builder builder = CodeBlock.builder(); - - boolean count, delete, exists; - StringQuery query; - if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.value())) { - query = new StringQuery(queryAnnotation.value()); - count = queryAnnotation.count(); - delete = queryAnnotation.delete(); - exists = queryAnnotation.exists(); - - } else { - PartTree partTree = new PartTree(context.getMethod().getName(), - context.getRepositoryInformation().getDomainType()); - query = queryCreator.createQuery(partTree, context.getMethod().getParameterCount()); - count = partTree.isCountProjection(); - delete = partTree.isDelete(); - exists = partTree.isExistsProjection(); - - } - - if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { - query.sort(queryAnnotation.sort()); - } - if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) { - query.fields(queryAnnotation.fields()); - } - - writeStringQuery(context, builder, count, delete, exists, query, queryMethod); - - return builder.build(); - }); - } - - private static void writeStringQuery(AotQueryMethodGenerationContext context, CodeBlock.Builder body, boolean count, - boolean delete, boolean exists, StringQuery query, MongoQueryMethod queryMethod) { - - body.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); - QueryBlockBuilder queryBlockBuilder = MongoBlocks.queryBlockBuilder(context, queryMethod).filter(query); - - if (delete) { - - String deleteQueryVariableName = "deleteQuery"; - body.add(queryBlockBuilder.usingQueryVariableName(deleteQueryVariableName).build()); - body.add( - MongoBlocks.deleteExecutionBlockBuilder(context, queryMethod).referencing(deleteQueryVariableName).build()); - } else { - - String filterQueryVariableName = "filterQuery"; - body.add(queryBlockBuilder.usingQueryVariableName(filterQueryVariableName).build()); - body.add(MongoBlocks.queryExecutionBlockBuilder(context, queryMethod).exists(exists).count(count) - .referencing(filterQueryVariableName).build()); - } - } - -} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java index c03f1bb6d2..d25b98ab1a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/Criteria.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core.query; -import static org.springframework.util.ObjectUtils.*; +import static org.springframework.util.ObjectUtils.nullSafeHashCode; import java.util.ArrayList; import java.util.Arrays; @@ -29,21 +29,9 @@ import java.util.regex.Pattern; import java.util.stream.Collectors; -import com.mongodb.MongoClientSettings; -import org.bson.BsonReader; import org.bson.BsonRegularExpression; import org.bson.BsonType; -import org.bson.BsonWriter; import org.bson.Document; -import org.bson.codecs.Codec; -import org.bson.codecs.DecoderContext; -import org.bson.codecs.DocumentCodec; -import org.bson.codecs.DocumentCodecProvider; -import org.bson.codecs.Encoder; -import org.bson.codecs.EncoderContext; -import org.bson.codecs.configuration.CodecProvider; -import org.bson.codecs.configuration.CodecRegistries; -import org.bson.codecs.configuration.CodecRegistry; import org.bson.types.Binary; import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Example; @@ -342,8 +330,7 @@ public Criteria in(@Nullable Object ... values) { throw new InvalidMongoDbApiUsageException( "You can only pass in one argument of type " + values[1].getClass().getName()); } - criteria.put("$in", Arrays.asList(values)); - return this; + return this.in(Arrays.asList(values)); } /** @@ -355,7 +342,13 @@ public Criteria in(@Nullable Object ... values) { */ @Contract("_ -> this") public Criteria in(Collection values) { - criteria.put("$in", values); + + ArrayList objects = new ArrayList<>(values); + if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) { + criteria.put("$in", placeholder); + } else { + criteria.put("$in", objects); + } return this; } @@ -380,7 +373,13 @@ public Criteria nin(Object... values) { */ @Contract("_ -> this") public Criteria nin(Collection values) { - criteria.put("$nin", values); + + ArrayList objects = new ArrayList<>(values); + if (objects.size() == 1 && CollectionUtils.firstElement(objects) instanceof Placeholder placeholder) { + criteria.put("$nin", placeholder); + } else { + criteria.put("$nin", objects); + } return this; } @@ -926,6 +925,19 @@ public Criteria andOperator(Collection criteria) { return registerCriteriaChainElement(new Criteria("$and").is(bsonList)); } + /** + * Creates a criterion using the given {@literal operator}. + * + * @param operator the native MongoDB operator. + * @param value the operator value + * @return this + * @since 5.0 + */ + public Criteria raw(String operator, Object value) { + criteria.put(operator, value); + return this; + } + private Criteria registerCriteriaChainElement(Criteria criteria) { if (lastOperatorWasNot()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java index 6fc5d60a78..7777e5f554 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/CriteriaDefinition.java @@ -40,16 +40,36 @@ public interface CriteriaDefinition { @Nullable String getKey(); + /** + * A placeholder expression used when rending queries to JSON. + * + * @since 5.0 + * @author Christoph Strobl + */ class Placeholder { - Object value; + private final Object expression; + + /** + * Create a new placeholder for index bindable parameter. + * + * @param position the index of the parameter to bind. + * @return new instance of {@link Placeholder}. + */ + public static Placeholder indexed(int position) { + return new Placeholder("?%s".formatted(position)); + } + + public static Placeholder placeholder(String expression) { + return new Placeholder(expression); + } - public Placeholder(Object value) { - this.value = value; + Placeholder(Object value) { + this.expression = value; } public Object getValue() { - return value; + return expression; } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java new file mode 100644 index 0000000000..003982daf6 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationInteraction.java @@ -0,0 +1,56 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; + +import org.springframework.data.repository.aot.generate.QueryMetadata; + +/** + * An {@link MongoInteraction aggregation interaction}. + * + * @author Christoph Strobl + * @since 5.0 + */ +class AggregationInteraction extends MongoInteraction implements QueryMetadata { + + private final StringAggregation aggregation; + + AggregationInteraction(String[] raw) { + this.aggregation = new StringAggregation(raw); + } + + List stages() { + return Arrays.asList(aggregation.pipeline()); + } + + @Override + InteractionType getExecutionType() { + return InteractionType.AGGREGATION; + } + + @Override + public Map serialize() { + + return Map.of(pipelineSerializationKey(), stages()); + } + + protected String pipelineSerializationKey() { + return "pipeline"; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java new file mode 100644 index 0000000000..cc672ed1e9 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AggregationUpdateInteraction.java @@ -0,0 +1,52 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.Map; + +/** + * An {@link MongoInteraction} to execute an aggregation update. + * + * @author Christoph Strobl + * @since 5.0 + */ +class AggregationUpdateInteraction extends AggregationInteraction { + + private final QueryInteraction filter; + + AggregationUpdateInteraction(QueryInteraction filter, String[] raw) { + + super(raw); + this.filter = filter; + } + + QueryInteraction getFilter() { + return filter; + } + + @Override + public Map serialize() { + + Map serialized = filter.serialize(); + serialized.putAll(super.serialize()); + return serialized; + } + + @Override + protected String pipelineSerializationKey() { + return "update-" + super.pipelineSerializationKey(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java index 2de77fc9b4..324871b475 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotMongoRepositoryPostProcessor.java @@ -15,11 +15,11 @@ */ package org.springframework.data.mongodb.repository.aot; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; import org.springframework.data.aot.AotContext; import org.springframework.data.mongodb.aot.LazyLoadingProxyAotProcessor; import org.springframework.data.mongodb.aot.MongoAotPredicates; -import org.springframework.data.mongodb.aot.generated.MongoRepositoryContributor; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.config.RepositoryRegistrationAotProcessor; @@ -34,7 +34,7 @@ public class AotMongoRepositoryPostProcessor extends RepositoryRegistrationAotPr private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor(); @Override - protected RepositoryContributor contribute(AotRepositoryContext repositoryContext, + protected @Nullable RepositoryContributor contribute(AotRepositoryContext repositoryContext, GenerationContext generationContext) { // do some custom type registration here super.contribute(repositoryContext, generationContext); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java similarity index 84% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index c0fbfc4ee9..831d21bb44 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.aot.generated; +package org.springframework.data.mongodb.repository.aot; import java.util.Iterator; import java.util.List; @@ -21,6 +21,8 @@ import java.util.stream.IntStream; import org.bson.conversions.Bson; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.ScrollPosition; @@ -41,15 +43,14 @@ import org.springframework.data.mongodb.repository.query.MongoQueryCreator; import org.springframework.data.repository.query.parser.PartTree; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; import com.mongodb.DBRef; /** * @author Christoph Strobl - * @since 2025/01 + * @since 5.0 */ -public class AotQueryCreator { +class AotQueryCreator { private MongoMappingContext mappingContext; @@ -64,13 +65,14 @@ public AotQueryCreator() { this.mappingContext = mongoMappingContext; } + @SuppressWarnings("NullAway") StringQuery createQuery(PartTree partTree, int parameterCount) { Query query = new MongoQueryCreator(partTree, new PlaceholderConvertingParameterAccessor(new PlaceholderParameterAccessor(parameterCount)), mappingContext) .createQuery(); - if(partTree.isLimiting()) { + if (partTree.isLimiting()) { query.limit(partTree.getMaxResults()); } return new StringQuery(query); @@ -88,13 +90,13 @@ public PlaceholderConvertingParameterAccessor(PlaceholderParameterAccessor deleg } } + @NullUnmarked enum PlaceholderWriter implements MongoWriter { INSTANCE; - @Nullable @Override - public Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { + public @Nullable Object convertToMongoType(@Nullable Object obj, @Nullable TypeInformation typeInformation) { return obj instanceof Placeholder p ? p.getValue() : obj; } @@ -109,6 +111,7 @@ public void write(Object source, Bson sink) { } } + @NullUnmarked static class PlaceholderParameterAccessor implements MongoParameterAccessor { private final List placeholders; @@ -117,8 +120,7 @@ public PlaceholderParameterAccessor(int parameterCount) { if (parameterCount == 0) { placeholders = List.of(); } else { - placeholders = IntStream.range(0, parameterCount).mapToObj(it -> new Placeholder("?" + it)) - .collect(Collectors.toList()); + placeholders = IntStream.range(0, parameterCount).mapToObj(Placeholder::indexed).collect(Collectors.toList()); } } @@ -127,21 +129,18 @@ public Range getDistanceRange() { return null; } - @Nullable @Override - public Point getGeoNearLocation() { + public @Nullable Point getGeoNearLocation() { return null; } - @Nullable @Override - public TextCriteria getFullText() { + public @Nullable TextCriteria getFullText() { return null; } - @Nullable @Override - public Collation getCollation() { + public @Nullable Collation getCollation() { return null; } @@ -150,15 +149,13 @@ public Object[] getValues() { return placeholders.toArray(); } - @Nullable @Override - public UpdateDefinition getUpdate() { + public @Nullable UpdateDefinition getUpdate() { return null; } - @Nullable @Override - public ScrollPosition getScrollPosition() { + public @Nullable ScrollPosition getScrollPosition() { return null; } @@ -172,15 +169,13 @@ public Sort getSort() { return null; } - @Nullable @Override - public Class findDynamicProjection() { + public @Nullable Class findDynamicProjection() { return null; } - @Nullable @Override - public Object getBindableValue(int index) { + public @Nullable Object getBindableValue(int index) { return placeholders.get(index).getValue(); } @@ -195,5 +190,4 @@ public Iterator iterator() { return ((List) placeholders).iterator(); } } - } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java new file mode 100644 index 0000000000..1537b6c722 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -0,0 +1,147 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.BindableMongoExpression; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.FieldName; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; + +/** + * @author Christoph Strobl + */ +public class MongoAotRepositoryFragmentSupport { + + private final RepositoryMetadata repositoryMetadata; + private final MongoOperations mongoOperations; + private final MongoConverter mongoConverter; + private final ProjectionFactory projectionFactory; + + protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, + RepositoryFactoryBeanSupport.FragmentCreationContext context) { + this(mongoOperations, context.getRepositoryMetadata(), context.getProjectionFactory()); + } + + protected MongoAotRepositoryFragmentSupport(MongoOperations mongoOperations, RepositoryMetadata repositoryMetadata, + ProjectionFactory projectionFactory) { + + this.mongoOperations = mongoOperations; + this.mongoConverter = mongoOperations.getConverter(); + this.repositoryMetadata = repositoryMetadata; + this.projectionFactory = projectionFactory; + } + + protected Document bindParameters(String source, Object[] parameters) { + return new BindableMongoExpression(source, this.mongoConverter, parameters).toDocument(); + } + + protected BasicQuery createQuery(String queryString, Object[] parameters) { + + Document queryDocument = bindParameters(queryString, parameters); + return new BasicQuery(queryDocument); + } + + protected AggregationPipeline createPipeline(List rawStages) { + + List stages = new ArrayList<>(rawStages.size()); + boolean first = true; + for (Object rawStage : rawStages) { + if (rawStage instanceof Document stageDocument) { + if (first) { + stages.add((ctx) -> ctx.getMappedObject(stageDocument)); + } else { + stages.add((ctx) -> stageDocument); + } + } else if (rawStage instanceof AggregationOperation aggregationOperation) { + stages.add(aggregationOperation); + } else { + throw new RuntimeException("%s cannot be converted to AggregationOperation".formatted(rawStage.getClass())); + } + if (first) { + first = false; + } + } + return new AggregationPipeline(stages); + } + + protected List convertSimpleRawResults(Class targetType, List rawResults) { + + List list = new ArrayList<>(rawResults.size()); + for (Document it : rawResults) { + list.add(extractSimpleTypeResult(it, targetType, mongoConverter)); + } + return list; + } + + private static @Nullable T extractSimpleTypeResult(@Nullable Document source, Class targetType, + MongoConverter converter) { + + if (ObjectUtils.isEmpty(source)) { + return null; + } + + if (source.size() == 1) { + return getPotentiallyConvertedSimpleTypeValue(converter, source.values().iterator().next(), targetType); + } + + Document intermediate = new Document(source); + intermediate.remove(FieldName.ID.name()); + + if (intermediate.size() == 1) { + return getPotentiallyConvertedSimpleTypeValue(converter, intermediate.values().iterator().next(), targetType); + } + + for (Map.Entry entry : intermediate.entrySet()) { + if (entry != null && ClassUtils.isAssignable(targetType, entry.getValue().getClass())) { + return targetType.cast(entry.getValue()); + } + } + + throw new IllegalArgumentException( + String.format("o_O no entry of type %s found in %s.", targetType.getSimpleName(), source.toJson())); + } + + @Nullable + @SuppressWarnings("unchecked") + private static T getPotentiallyConvertedSimpleTypeValue(MongoConverter converter, @Nullable Object value, + Class targetType) { + + if (value == null) { + return null; + } + + if (ClassUtils.isAssignableValue(targetType, value)) { + return (T) value; + } + + return converter.getConversionService().convert(value, targetType); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java new file mode 100644 index 0000000000..8338ffe6bf --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -0,0 +1,777 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.regex.Pattern; + +import org.bson.Document; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.data.domain.SliceImpl; +import org.springframework.data.domain.Sort.Order; +import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; +import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOptions; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; +import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.BasicUpdate; +import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryExecution.SlicedExecution; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; +import org.springframework.data.util.ReflectionUtils; +import org.springframework.javapoet.ClassName; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.CodeBlock.Builder; +import org.springframework.javapoet.TypeName; +import org.springframework.util.ClassUtils; +import org.springframework.util.CollectionUtils; +import org.springframework.util.NumberUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * {@link CodeBlock} generator for common tasks. + * + * @author Christoph Strobl + * @since 5.0 + */ +class MongoCodeBlocks { + + private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)"); + + /** + * Builder for generating query parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return new instance of {@link QueryCodeBlockBuilder}. + */ + static QueryCodeBlockBuilder queryBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new QueryCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating finder query execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static QueryExecutionCodeBlockBuilder queryExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new QueryExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating delete execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static DeleteExecutionCodeBlockBuilder deleteExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new DeleteExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating update parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static UpdateCodeBlockBuilder updateBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + return new UpdateCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating update execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static UpdateExecutionCodeBlockBuilder updateExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new UpdateExecutionCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating aggregation (pipeline) parsing {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static AggregationCodeBlockBuilder aggregationBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new AggregationCodeBlockBuilder(context, queryMethod); + } + + /** + * Builder for generating aggregation execution {@link CodeBlock}. + * + * @param context + * @param queryMethod + * @return + */ + static AggregationExecutionCodeBlockBuilder aggregationExecutionBlockBuilder(AotQueryMethodGenerationContext context, + MongoQueryMethod queryMethod) { + + return new AggregationExecutionCodeBlockBuilder(context, queryMethod); + } + + @NullUnmarked + static class DeleteExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String queryVariableName; + + DeleteExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + DeleteExecutionCodeBlockBuilder referencing(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getActualReturnType() != null + && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), + context.getActualReturnType()); + + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, + context.getRepositoryInformation().getDomainType(), mongoOpsRef, + context.getRepositoryInformation().getDomainType()); + + DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL; + if (!queryMethod.isCollectionQuery()) { + if (!ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType())) { + type = DeleteExecution.Type.FIND_AND_REMOVE_ONE; + } else { + type = DeleteExecution.Type.ALL; + } + } + + actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) + ? ClassName.get(context.getMethod().getReturnType()) + : queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType; + + builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecution.class, + DeleteExecution.Type.class, type.name(), queryVariableName); + + return builder.build(); + } + } + + @NullUnmarked + static class UpdateExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String queryVariableName; + private String updateVariableName; + + UpdateExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + UpdateExecutionCodeBlockBuilder withFilter(String queryVariableName) { + + this.queryVariableName = queryVariableName; + return this; + } + + UpdateExecutionCodeBlockBuilder referencingUpdate(String updateVariableName) { + + this.updateVariableName = updateVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + builder.add("\n"); + + String updateReference = updateVariableName; + builder.addStatement("$T<$T> updater = $L.update($T.class)", ExecutableUpdate.class, + context.getRepositoryInformation().getDomainType(), mongoOpsRef, + context.getRepositoryInformation().getDomainType()); + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + if (ReflectionUtils.isVoid(returnType)) { + builder.addStatement("updater.matching($L).apply($L).all()", queryVariableName, updateReference); + } else if (ClassUtils.isAssignable(Long.class, returnType)) { + builder.addStatement("return updater.matching($L).apply($L).all().getModifiedCount()", queryVariableName, + updateReference); + } else { + builder.addStatement("$T modifiedCount = updater.matching($L).apply($L).all().getModifiedCount()", Long.class, + queryVariableName, updateReference); + builder.addStatement("return $T.convertNumberToTargetClass(modifiedCount, $T.class)", NumberUtils.class, + returnType); + } + + return builder.build(); + } + } + + @NullUnmarked + static class AggregationExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private String aggregationVariableName; + + AggregationExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + AggregationExecutionCodeBlockBuilder referencing(String aggregationVariableName) { + + this.aggregationVariableName = aggregationVariableName; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + Builder builder = CodeBlock.builder(); + + builder.add("\n"); + + Class outputType = queryMethod.getReturnedObjectType(); + if (MongoSimpleTypes.HOLDER.isSimpleType(outputType)) { + outputType = Document.class; + } else if (ClassUtils.isAssignable(AggregationResults.class, outputType)) { + outputType = queryMethod.getReturnType().getComponentType().getType(); + } + + if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) { + builder.addStatement("$L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); + return builder.build(); + } + + if (ClassUtils.isAssignable(AggregationResults.class, context.getMethod().getReturnType())) { + builder.addStatement("return $L.aggregate($L, $T.class)", mongoOpsRef, aggregationVariableName, outputType); + return builder.build(); + } + + if (outputType == Document.class) { + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + + builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef, + aggregationVariableName, outputType); + if (!queryMethod.isCollectionQuery()) { + builder.addStatement( + "return $T.<$T>firstElement(convertSimpleRawResults($T.class, results.getMappedResults()))", + CollectionUtils.class, returnType, returnType); + } else { + builder.addStatement("return convertSimpleRawResults($T.class, results.getMappedResults())", returnType); + } + } else { + if (queryMethod.isSliceQuery()) { + builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef, + aggregationVariableName, outputType); + builder.addStatement("boolean hasNext = results.getMappedResults().size() > $L.getPageSize()", + context.getPageableParameterName()); + builder.addStatement( + "return new $T<>(hasNext ? results.getMappedResults().subList(0, $L.getPageSize()) : results.getMappedResults(), $L, hasNext)", + SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + } else { + builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, + aggregationVariableName, outputType); + } + } + + return builder.build(); + } + } + + @NullUnmarked + static class QueryExecutionCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + private QueryInteraction query; + + QueryExecutionCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.queryMethod = queryMethod; + } + + QueryExecutionCodeBlockBuilder forQuery(QueryInteraction query) { + + this.query = query; + return this; + } + + CodeBlock build() { + + String mongoOpsRef = context.fieldNameOf(MongoOperations.class); + + Builder builder = CodeBlock.builder(); + + boolean isProjecting = context.getReturnedType().isProjecting(); + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() + : context.getRepositoryInformation().getDomainType(); + + builder.add("\n"); + + if (isProjecting) { + builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, + mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); + } else { + + builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, + context.getRepositoryInformation().getDomainType()); + } + + String terminatingMethod; + + if (queryMethod.isCollectionQuery() || queryMethod.isPageQuery() || queryMethod.isSliceQuery()) { + terminatingMethod = "all()"; + } else if (query.isCount()) { + terminatingMethod = "count()"; + } else if (query.isExists()) { + terminatingMethod = "exists()"; + } else { + terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; + } + + if (queryMethod.isPageQuery()) { + builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, + context.getPageableParameterName(), query.name()); + } else if (queryMethod.isSliceQuery()) { + builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, + context.getPageableParameterName(), query.name()); + } else { + builder.addStatement("return finder.matching($L).$L", query.name(), terminatingMethod); + } + + return builder.build(); + } + } + + @NullUnmarked + static class AggregationCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + + private AggregationInteraction source; + private List arguments; + private String aggregationVariableName; + private boolean pipelineOnly; + + AggregationCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.arguments = context.getBindableParameterNames(); + this.queryMethod = queryMethod; + } + + AggregationCodeBlockBuilder stages(AggregationInteraction aggregation) { + + this.source = aggregation; + return this; + } + + AggregationCodeBlockBuilder usingAggregationVariableName(String aggregationVariableName) { + + this.aggregationVariableName = aggregationVariableName; + return this; + } + + AggregationCodeBlockBuilder pipelineOnly(boolean pipelineOnly) { + + this.pipelineOnly = pipelineOnly; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add("\n"); + + String pipelineName = aggregationVariableName + (pipelineOnly ? "" : "Pipeline"); + builder.add(pipeline(pipelineName)); + + if (!pipelineOnly) { + + builder.addStatement("$T<$T> $L = $T.newAggregation($T.class, $L.getOperations())", TypedAggregation.class, + context.getRepositoryInformation().getDomainType(), aggregationVariableName, Aggregation.class, + context.getRepositoryInformation().getDomainType(), pipelineName); + + builder.add(aggregationOptions(aggregationVariableName)); + } + + return builder.build(); + } + + private CodeBlock pipeline(String pipelineVariableName) { + + String sortParameter = context.getSortParameterName(); + String limitParameter = context.getLimitParameterName(); + String pageableParameter = context.getPageableParameterName(); + + boolean mightBeSorted = StringUtils.hasText(sortParameter); + boolean mightBeLimited = StringUtils.hasText(limitParameter); + boolean mightBePaged = StringUtils.hasText(pageableParameter); + + int stageCount = source.stages().size(); + if (mightBeSorted) { + stageCount++; + } + if (mightBeLimited) { + stageCount++; + } + if (mightBePaged) { + stageCount += 3; + } + + Builder builder = CodeBlock.builder(); + String stagesVariableName = "stages"; + builder.add(aggregationStages(stagesVariableName, source.stages(), stageCount, arguments)); + + if (mightBeSorted) { + builder.add(sortingStage(sortParameter)); + } + + if (mightBeLimited) { + builder.add(limitingStage(limitParameter)); + } + + if (mightBePaged) { + builder.add(pagingStage(pageableParameter, queryMethod.isSliceQuery())); + } + + builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, + stagesVariableName); + return builder.build(); + } + + private CodeBlock aggregationOptions(String aggregationVariableName) { + + Builder builder = CodeBlock.builder(); + List options = new ArrayList<>(5); + if (ReflectionUtils.isVoid(queryMethod.getReturnedObjectType())) { + options.add(CodeBlock.of(".skipOutput()")); + } + + MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); + String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; + if (StringUtils.hasText(hint)) { + options.add(CodeBlock.of(".hint($S)", hint)); + } + + MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); + String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; + if (StringUtils.hasText(readPreference)) { + options.add(CodeBlock.of(".readPreference($T.valueOf($S))", com.mongodb.ReadPreference.class, readPreference)); + } + + if (queryMethod.hasAnnotatedCollation()) { + options.add(CodeBlock.of(".collation($T.parse($S))", Collation.class, queryMethod.getAnnotatedCollation())); + } + + if (!options.isEmpty()) { + + Builder optionsBuilder = CodeBlock.builder(); + optionsBuilder.add("$T aggregationOptions = $T.builder()\n", AggregationOptions.class, + AggregationOptions.class); + optionsBuilder.indent(); + for (CodeBlock optionBlock : options) { + optionsBuilder.add(optionBlock); + optionsBuilder.add("\n"); + } + optionsBuilder.add(".build();\n"); + optionsBuilder.unindent(); + builder.add(optionsBuilder.build()); + + builder.addStatement("$L = $L.withOptions(aggregationOptions)", aggregationVariableName, + aggregationVariableName); + } + return builder.build(); + } + + private static CodeBlock aggregationStages(String stageListVariableName, Iterable stages, int stageCount, + List arguments) { + + Builder builder = CodeBlock.builder(); + builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class, + stageCount); + int stageCounter = 0; + for (String stage : stages) { + String stageName = "stage_%s".formatted(stageCounter++); + builder.add(renderExpressionToDocument(stage, stageName, arguments)); + builder.addStatement("stages.add($L)", stageName); + } + return builder.build(); + } + + private static CodeBlock sortingStage(String sortProvider) { + + Builder builder = CodeBlock.builder(); + builder.beginControlFlow("if($L.isSorted())", sortProvider); + builder.addStatement("$T sortDocument = new $T()", Document.class, Document.class); + builder.beginControlFlow("for ($T order : $L)", Order.class, sortProvider); + builder.addStatement("sortDocument.append(order.getProperty(), order.isAscending() ? 1 : -1);"); + builder.endControlFlow(); + builder.addStatement("stages.add(new $T($S, sortDocument))", Document.class, "$sort"); + builder.endControlFlow(); + return builder.build(); + } + + private static CodeBlock pagingStage(String pageableProvider, boolean slice) { + + Builder builder = CodeBlock.builder(); + builder.add(sortingStage(pageableProvider + ".getSort()")); + + builder.beginControlFlow("if($L.isPaged())", pageableProvider); + builder.beginControlFlow("if($L.getOffset() > 0)", pageableProvider); + builder.addStatement("stages.add($T.skip($L.getOffset()))", Aggregation.class, pageableProvider); + builder.endControlFlow(); + if (slice) { + builder.addStatement("stages.add($T.limit($L.getPageSize() + 1))", Aggregation.class, pageableProvider); + } else { + builder.addStatement("stages.add($T.limit($L.getPageSize()))", Aggregation.class, pageableProvider); + } + builder.endControlFlow(); + + return builder.build(); + } + + private static CodeBlock limitingStage(String limitProvider) { + + Builder builder = CodeBlock.builder(); + builder.beginControlFlow("if($L.isLimited())", limitProvider); + builder.addStatement("stages.add($T.limit($L.max()))", Aggregation.class, limitProvider); + builder.endControlFlow(); + return builder.build(); + } + } + + @NullUnmarked + static class QueryCodeBlockBuilder { + + private final AotQueryMethodGenerationContext context; + private final MongoQueryMethod queryMethod; + + private QueryInteraction source; + private List arguments; + private String queryVariableName; + + QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + + this.context = context; + this.arguments = context.getBindableParameterNames(); + this.queryMethod = queryMethod; + } + + QueryCodeBlockBuilder filter(QueryInteraction query) { + + this.source = query; + return this; + } + + QueryCodeBlockBuilder usingQueryVariableName(String queryVariableName) { + this.queryVariableName = queryVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + builder.add(renderExpressionToQuery(source.getQuery().getQueryString(), queryVariableName)); + + if (StringUtils.hasText(source.getQuery().getFieldsString())) { + + builder.add(renderExpressionToDocument(source.getQuery().getFieldsString(), "fields", arguments)); + builder.addStatement("$L.setFieldsObject(fields)", queryVariableName); + } + + String sortParameter = context.getSortParameterName(); + if (StringUtils.hasText(sortParameter)) { + builder.addStatement("$L.with($L)", queryVariableName, sortParameter); + } else if (StringUtils.hasText(source.getQuery().getSortString())) { + + builder.add(renderExpressionToDocument(source.getQuery().getSortString(), "sort", arguments)); + builder.addStatement("$L.setSortObject(sort)", queryVariableName); + } + + String limitParameter = context.getLimitParameterName(); + if (StringUtils.hasText(limitParameter)) { + builder.addStatement("$L.limit($L)", queryVariableName, limitParameter); + } else if (context.getPageableParameterName() == null && source.getQuery().isLimited()) { + builder.addStatement("$L.limit($L)", queryVariableName, source.getQuery().getLimit()); + } + + String pageableParameter = context.getPageableParameterName(); + if (StringUtils.hasText(pageableParameter) && !queryMethod.isPageQuery() && !queryMethod.isSliceQuery()) { + builder.addStatement("$L.with($L)", queryVariableName, pageableParameter); + } + + MergedAnnotation hintAnnotation = context.getAnnotation(Hint.class); + String hint = hintAnnotation.isPresent() ? hintAnnotation.getString("value") : null; + + if (StringUtils.hasText(hint)) { + builder.addStatement("$L.withHint($S)", queryVariableName, hint); + } + + MergedAnnotation readPreferenceAnnotation = context.getAnnotation(ReadPreference.class); + String readPreference = readPreferenceAnnotation.isPresent() ? readPreferenceAnnotation.getString("value") : null; + + if (StringUtils.hasText(readPreference)) { + builder.addStatement("$L.withReadPreference($T.valueOf($S))", queryVariableName, + com.mongodb.ReadPreference.class, readPreference); + } + + // TODO: Meta annotation + + return builder.build(); + } + + private CodeBlock renderExpressionToQuery(@Nullable String source, String variableName) { + + Builder builder = CodeBlock.builder(); + if (!StringUtils.hasText(source)) { + + builder.addStatement("$T $L = new $T(new $T())", BasicQuery.class, variableName, BasicQuery.class, + Document.class); + } else if (!containsPlaceholder(source)) { + + String tmpVarName = "%sString".formatted(variableName); + builder.addStatement("String $L = $S", tmpVarName, source); + + builder.addStatement("$T $L = new $T($T.parse($L))", BasicQuery.class, variableName, BasicQuery.class, + Document.class, tmpVarName); + } else { + + String tmpVarName = "%sString".formatted(variableName); + builder.addStatement("String $L = $S", tmpVarName, source); + builder.addStatement("$T $L = createQuery($L, new $T[]{ $L })", BasicQuery.class, variableName, tmpVarName, + Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); + } + + return builder.build(); + } + } + + @NullUnmarked + static class UpdateCodeBlockBuilder { + + private UpdateInteraction source; + private List arguments; + private String updateVariableName; + + public UpdateCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { + this.arguments = context.getBindableParameterNames(); + } + + public UpdateCodeBlockBuilder update(UpdateInteraction update) { + this.source = update; + return this; + } + + public UpdateCodeBlockBuilder usingUpdateVariableName(String updateVariableName) { + this.updateVariableName = updateVariableName; + return this; + } + + CodeBlock build() { + + CodeBlock.Builder builder = CodeBlock.builder(); + + builder.add("\n"); + String tmpVariableName = updateVariableName + "Document"; + builder.add(renderExpressionToDocument(source.getUpdate().getUpdateString(), tmpVariableName, arguments)); + builder.addStatement("$T $L = new $T($L)", BasicUpdate.class, updateVariableName, BasicUpdate.class, + tmpVariableName); + + return builder.build(); + } + } + + private static CodeBlock renderExpressionToDocument(@Nullable String source, String variableName, + List arguments) { + + Builder builder = CodeBlock.builder(); + if (!StringUtils.hasText(source)) { + builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class); + } else if (!containsPlaceholder(source)) { + + String tmpVarName = "%sString".formatted(variableName); + builder.addStatement("String $L = $S", tmpVarName, source); + builder.addStatement("$T $L = $T.parse($L)", Document.class, variableName, Document.class, tmpVarName); + } else { + + String tmpVarName = "%sString".formatted(variableName); + builder.addStatement("String $L = $S", tmpVarName, source); + builder.addStatement("$T $L = bindParameters($L, new $T[]{ $L })", Document.class, variableName, tmpVarName, + Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); + } + return builder.build(); + } + + private static boolean containsPlaceholder(String source) { + return PARAMETER_BINDING_PATTERN.matcher(source).find(); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java new file mode 100644 index 0000000000..fa9ca2f99e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoInteraction.java @@ -0,0 +1,62 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +/** + * Base abstraction for interactions with MongoDB. + * + * @author Christoph Strobl + * @since 5.0 + */ +abstract class MongoInteraction { + + abstract InteractionType getExecutionType(); + + boolean isAggregation() { + return InteractionType.AGGREGATION.equals(getExecutionType()); + } + + boolean isCount() { + return InteractionType.COUNT.equals(getExecutionType()); + } + + boolean isDelete() { + return InteractionType.DELETE.equals(getExecutionType()); + } + + boolean isExists() { + return InteractionType.EXISTS.equals(getExecutionType()); + } + + boolean isUpdate() { + return InteractionType.UPDATE.equals(getExecutionType()); + } + + String name() { + + if (isDelete()) { + return "deleteQuery"; + } + if (isCount()) { + return "countQuery"; + } + return "filterQuery"; + } + + enum InteractionType { + QUERY, COUNT, DELETE, EXISTS, UPDATE, AGGREGATION + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java new file mode 100644 index 0000000000..def03c7973 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -0,0 +1,286 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationExecutionBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.deleteExecutionBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryExecutionBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateExecutionBlockBuilder; + +import java.lang.reflect.Method; +import java.util.regex.Pattern; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.Query; +import org.springframework.data.mongodb.repository.Update; +import org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.QueryCodeBlockBuilder; +import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; +import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; +import org.springframework.data.repository.aot.generate.MethodContributor; +import org.springframework.data.repository.aot.generate.RepositoryContributor; +import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.core.RepositoryInformation; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.QueryMethod; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.TypeName; +import org.springframework.javapoet.TypeSpec.Builder; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; + +/** + * MongoDB specific {@link RepositoryContributor}. + * + * @author Christoph Strobl + * @since 5.0 + */ +public class MongoRepositoryContributor extends RepositoryContributor { + + private static final Log logger = LogFactory.getLog(RepositoryContributor.class); + + private final AotQueryCreator queryCreator; + private final MongoMappingContext mappingContext; + + public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { + + super(repositoryContext); + this.queryCreator = new AotQueryCreator(); + this.mappingContext = new MongoMappingContext(); + } + + @Override + protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata, + Builder builder) { + builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class)); + } + + @Override + protected void customizeConstructor(AotRepositoryConstructorBuilder constructorBuilder) { + + constructorBuilder.addParameter("operations", TypeName.get(MongoOperations.class)); + constructorBuilder.addParameter("context", TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class), + false); + + constructorBuilder.customize((repositoryInformation, builder) -> { + builder.addStatement("super(operations, context)"); + }); + } + + @Override + @SuppressWarnings("NullAway") + protected @Nullable MethodContributor contributeQueryMethod(Method method, + RepositoryInformation repositoryInformation) { + + MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(), + mappingContext); + + if (backoff(queryMethod)) { + return null; + } + + try { + if (queryMethod.hasAnnotatedAggregation()) { + + AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation()); + return aggregationMethodContributor(queryMethod, aggregation); + } + + QueryInteraction query = createStringQuery(repositoryInformation, queryMethod, + AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount()); + + if (queryMethod.hasAnnotatedQuery()) { + if (StringUtils.hasText(queryMethod.getAnnotatedQuery()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) { + + if (logger.isDebugEnabled()) { + logger.debug( + "Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName())); + } + return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query); + } + } + + if (query.isDelete()) { + return deleteMethodContributor(queryMethod, query); + } + + if (queryMethod.isModifyingQuery()) { + + Update updateSource = queryMethod.getUpdateSource(); + if (StringUtils.hasText(updateSource.value())) { + UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value())); + return updateMethodContributor(queryMethod, update); + } + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); + return aggregationUpdateMethodContributor(queryMethod, update); + } + } + + return queryMethodContributor(queryMethod, query); + } catch (RuntimeException codeGenerationError) { + if (logger.isErrorEnabled()) { + logger.error("Failed to generate code for [%s] [%s]".formatted(repositoryInformation.getRepositoryInterface(), + method.getName()), codeGenerationError); + } + } + return null; + } + + @SuppressWarnings("NullAway") + private QueryInteraction createStringQuery(RepositoryInformation repositoryInformation, MongoQueryMethod queryMethod, + @Nullable Query queryAnnotation, int parameterCount) { + + QueryInteraction query; + if (queryMethod.hasAnnotatedQuery() && queryAnnotation != null) { + query = new QueryInteraction(new StringQuery(queryMethod.getAnnotatedQuery()), queryAnnotation.count(), + queryAnnotation.delete(), queryAnnotation.exists()); + } else { + + PartTree partTree = new PartTree(queryMethod.getName(), repositoryInformation.getDomainType()); + query = new QueryInteraction(queryCreator.createQuery(partTree, parameterCount), partTree.isCountProjection(), + partTree.isDelete(), partTree.isExistsProjection()); + } + + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.sort())) { + query = query.withSort(queryAnnotation.sort()); + } + if (queryAnnotation != null && StringUtils.hasText(queryAnnotation.fields())) { + query = query.withFields(queryAnnotation.fields()); + } + + return query; + } + + private static boolean backoff(MongoQueryMethod method) { + + boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery(); + + if (skip && logger.isDebugEnabled()) { + logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming or scrolling query" + .formatted(method.getName())); + } + return skip; + } + + private static MethodContributor aggregationMethodContributor(MongoQueryMethod queryMethod, + AggregationInteraction aggregation) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + + builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation) + .usingAggregationVariableName("aggregation").build()); + builder.add(aggregationExecutionBlockBuilder(context, queryMethod).referencing("aggregation").build()); + + return builder.build(); + }); + } + + private static MethodContributor updateMethodContributor(MongoQueryMethod queryMethod, + UpdateInteraction update) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + + // update filter + String filterVariableName = update.name(); + builder.add(queryBlockBuilder(context, queryMethod).filter(update.getFilter()) + .usingQueryVariableName(filterVariableName).build()); + + // update definition + String updateVariableName = "updateDefinition"; + builder.add( + updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName).build()); + + builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) + .referencingUpdate(updateVariableName).build()); + return builder.build(); + }); + } + + private static MethodContributor aggregationUpdateMethodContributor(MongoQueryMethod queryMethod, + AggregationUpdateInteraction update) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + + // update filter + String filterVariableName = update.name(); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(update.getFilter()); + builder.add(queryCodeBlockBuilder.usingQueryVariableName(filterVariableName).build()); + + // update definition + String updateVariableName = "updateDefinition"; + builder.add(aggregationBlockBuilder(context, queryMethod).stages(update) + .usingAggregationVariableName(updateVariableName).pipelineOnly(true).build()); + + builder.addStatement("$T aggregationUpdate = $T.from($L.getOperations())", AggregationUpdate.class, + AggregationUpdate.class, updateVariableName); + + builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) + .referencingUpdate("aggregationUpdate").build()); + return builder.build(); + }); + } + + private static MethodContributor deleteMethodContributor(MongoQueryMethod queryMethod, + QueryInteraction query) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); + + builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build()); + builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(query.name()).build()); + return builder.build(); + }); + } + + private static MethodContributor queryMethodContributor(MongoQueryMethod queryMethod, + QueryInteraction query) { + + return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { + + CodeBlock.Builder builder = CodeBlock.builder(); + builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); + QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); + + builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build()); + builder.add(queryExecutionBlockBuilder(context, queryMethod).forQuery(query).build()); + return builder.build(); + }); + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java new file mode 100644 index 0000000000..563079c03b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/QueryInteraction.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.data.repository.aot.generate.QueryMetadata; +import org.springframework.util.StringUtils; + +/** + * An {@link MongoInteraction} to execute a query. + * + * @author Christoph Strobl + * @since 5.0 + */ +class QueryInteraction extends MongoInteraction implements QueryMetadata { + + private final StringQuery query; + private final InteractionType interactionType; + + QueryInteraction(StringQuery query, boolean count, boolean delete, boolean exists) { + + this.query = query; + if (count) { + interactionType = InteractionType.COUNT; + } else if (exists) { + interactionType = InteractionType.EXISTS; + } else if (delete) { + interactionType = InteractionType.DELETE; + } else { + interactionType = InteractionType.QUERY; + } + } + + StringQuery getQuery() { + return query; + } + + QueryInteraction withSort(String sort) { + query.sort(sort); + return this; + } + + QueryInteraction withFields(String fields) { + query.fields(fields); + return this; + } + + @Override + InteractionType getExecutionType() { + return interactionType; + } + + @Override + public Map serialize() { + + Map serialized = new LinkedHashMap<>(); + + serialized.put("filter", query.getQueryString()); + if (query.isSorted()) { + serialized.put("sort", query.getSortString()); + } + if (StringUtils.hasText(query.getFieldsString())) { + serialized.put("fields", query.getFieldsString()); + } + + return serialized; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java new file mode 100644 index 0000000000..7b73215e98 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringAggregation.java @@ -0,0 +1,26 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +/** + * Value object holding the raw representation of an Aggregation Pipeline. + * + * @author Christoph Strobl + * @since 5.0 + */ +record StringAggregation(String[] pipeline) { + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java similarity index 73% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java index c8d7b7ab2a..d037198bba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/generated/StringQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringQuery.java @@ -1,19 +1,3 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -29,27 +13,29 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.aot.generated; +package org.springframework.data.mongodb.repository.aot; import java.util.Optional; import java.util.Set; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.query.Field; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import com.mongodb.ReadConcern; import com.mongodb.ReadPreference; /** + * Helper to capture setting for AOT queries. + * * @author Christoph Strobl - * @since 2025/01 + * @since 5.0 */ class StringQuery extends Query { @@ -58,8 +44,6 @@ class StringQuery extends Query { private @Nullable String sort; private @Nullable String fields; - private ExecutionType executionType = ExecutionType.QUERY; - public StringQuery(Query query) { this.delegate = query; } @@ -69,11 +53,6 @@ public StringQuery(String query) { this.raw = query; } - public StringQuery forCount() { - this.executionType = ExecutionType.COUNT; - return this; - } - @Nullable String getQueryString() { @@ -104,7 +83,7 @@ public boolean hasReadConcern() { } @Override - public ReadConcern getReadConcern() { + public @Nullable ReadConcern getReadConcern() { return delegate.getReadConcern(); } @@ -114,7 +93,7 @@ public boolean hasReadPreference() { } @Override - public ReadPreference getReadPreference() { + public @Nullable ReadPreference getReadPreference() { return delegate.getReadPreference(); } @@ -124,8 +103,7 @@ public boolean hasKeyset() { } @Override - @Nullable - public KeysetScrollPosition getKeyset() { + public @Nullable KeysetScrollPosition getKeyset() { return delegate.getKeyset(); } @@ -170,8 +148,7 @@ public int getLimit() { } @Override - @Nullable - public String getHint() { + public @Nullable String getHint() { return delegate.getHint(); } @@ -216,12 +193,6 @@ StringQuery fields(String fields) { } String toJson(Document source) { - StringBuffer buffer = new StringBuffer(); - BsonUtils.writeJson(source).to(buffer); - return buffer.toString(); - } - - enum ExecutionType { - QUERY, COUNT, DELETE + return BsonUtils.writeJson(source).toJsonString(); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java new file mode 100644 index 0000000000..f65ee7912f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/StringUpdate.java @@ -0,0 +1,27 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +/** + * @author Christoph Strobl + * @since 5.0 + */ +record StringUpdate(String raw) { + + String getUpdateString() { + return raw; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java new file mode 100644 index 0000000000..bbc76bec59 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java @@ -0,0 +1,58 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.util.Map; + +import org.springframework.data.repository.aot.generate.QueryMetadata; + +/** + * An {@link MongoInteraction} to execute an update. + * + * @author Christoph Strobl + * @since 5.0 + */ +class UpdateInteraction extends MongoInteraction implements QueryMetadata { + + private final QueryInteraction filter; + private final StringUpdate update; + + UpdateInteraction(QueryInteraction filter, StringUpdate update) { + this.filter = filter; + this.update = update; + } + + QueryInteraction getFilter() { + return filter; + } + + StringUpdate getUpdate() { + return update; + } + + @Override + public Map serialize() { + + Map serialized = filter.serialize(); + serialized.put("update", update.getUpdateString()); + return serialized; + } + + @Override + InteractionType getExecutionType() { + return InteractionType.UPDATE; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index e160fd879a..f56c2c7a22 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -27,6 +27,7 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; @@ -69,6 +70,7 @@ public abstract class AbstractMongoQuery implements RepositoryQuery { private final MongoOperations operations; private final ExecutableFind executableFind; private final ExecutableUpdate executableUpdate; + private final ExecutableRemove executableRemove; private final Lazy codec = Lazy .of(() -> new ParameterBindingDocumentCodec(getCodecRegistry())); private final ValueExpressionDelegate valueExpressionDelegate; @@ -95,6 +97,7 @@ public AbstractMongoQuery(MongoQueryMethod method, MongoOperations operations, V this.executableFind = operations.query(type); this.executableUpdate = operations.update(type); + this.executableRemove = operations.remove(type); this.valueExpressionDelegate = delegate; this.valueEvaluationContextProvider = delegate.createValueContextProvider(method.getParameters()); } @@ -164,7 +167,7 @@ private Query applyAnnotatedReadPreferenceIfPresent(Query query) { private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { - return new DeleteExecution(operations, method); + return new DeleteExecution<>(executableRemove, method); } if (method.isModifyingQuery()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 86223e83db..b8a8c34f48 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -184,9 +184,17 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit case IS_NULL: return criteria.is(null); case NOT_IN: - return criteria.nin(nextAsList(parameters, part)); + Object ninValue = parameters.next(); + if(ninValue instanceof Placeholder) { + return criteria.raw("$nin", ninValue); + } + return criteria.nin(valueAsList(ninValue, part)); case IN: - return criteria.in(nextAsList(parameters, part)); + Object inValue = parameters.next(); + if(inValue instanceof Placeholder) { + return criteria.raw("$in", inValue); + } + return criteria.in(valueAsList(inValue, part)); case LIKE: case STARTING_WITH: case ENDING_WITH: @@ -201,7 +209,12 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit Object param = parameters.next(); return param instanceof Pattern pattern ? criteria.regex(pattern) : criteria.regex(param.toString()); case EXISTS: - return criteria.exists((Boolean) parameters.next()); + Object next = parameters.next(); + if(next instanceof Placeholder placeholder) { + return criteria.raw("$exists", placeholder); + } else { + return criteria.exists((Boolean) next); + } case TRUE: return criteria.is(true); case FALSE: @@ -320,7 +333,11 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro Iterator parameters) { if (property.isCollectionLike()) { - return criteria.in(nextAsList(parameters, part)); + Object next = parameters.next(); + if(next instanceof Placeholder) { + return criteria.raw("$in", next); + } + return criteria.in(valueAsList(next, part)); } return addAppropriateLikeRegexTo(criteria, part, parameters.next()); @@ -384,19 +401,19 @@ private T nextAs(Iterator iterator, Class type) { String.format("Expected parameter type of %s but got %s", type, parameter.getClass())); } - private java.util.List nextAsList(Iterator iterator, Part part) { + private java.util.List valueAsList(Object value, Part part) { - Streamable streamable = asStreamable(iterator.next()); + Streamable streamable = asStreamable(value); if (!isSimpleComparisonPossible(part)) { MatchMode matchMode = toMatchMode(part.getType()); String regexOptions = toRegexOptions(part); streamable = streamable.map(it -> { - if (it instanceof String value) { + if (it instanceof String sv) { - return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(value, matchMode), - regexOptions); + return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), + regexOptions); } return it; }); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index a9a631646e..01d4e0c63d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -37,11 +37,11 @@ import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; -import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.repository.util.SliceUtils; +import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.support.PageableExecutionUtils; import org.springframework.data.util.TypeInformation; import org.springframework.util.Assert; @@ -251,19 +251,39 @@ public Object execute(Query query) { } } - final class DeleteExecutionX implements MongoQueryExecution { + /** + * {@link MongoQueryExecution} removing documents matching the query. + * + * @author Oliver Gierke + * @author Mark Paluch + * @author Artyom Gabeev + * @author Christoph Strobl + * @since 1.5 + */ + final class DeleteExecution implements MongoQueryExecution { - ExecutableRemoveOperation.ExecutableRemove remove; - Type type; + private ExecutableRemoveOperation.ExecutableRemove remove; + private Type type; - public DeleteExecutionX(ExecutableRemove remove, Type type) { + public DeleteExecution(ExecutableRemove remove, QueryMethod queryMethod) { + this.remove = remove; + if (queryMethod.isCollectionQuery()) { + this.type = Type.FIND_AND_REMOVE_ALL; + } else if (queryMethod.isQueryForEntity() + && !ClassUtils.isPrimitiveOrWrapper(queryMethod.getReturnedObjectType())) { + this.type = Type.FIND_AND_REMOVE_ONE; + } else { + this.type = Type.ALL; + } + } + + public DeleteExecution(ExecutableRemove remove, Type type) { this.remove = remove; this.type = type; } - @Nullable @Override - public Object execute(Query query) { + public @Nullable Object execute(Query query) { TerminatingRemove doRemove = remove.matching(query); if (Type.ALL.equals(type)) { @@ -274,7 +294,6 @@ public Object execute(Query query) { } else if (Type.FIND_AND_REMOVE_ONE.equals(type)) { Iterator removed = doRemove.findAndRemove().iterator(); return removed.hasNext() ? removed.next() : null; - } throw new RuntimeException(); } @@ -284,48 +303,6 @@ public enum Type { } } - /** - * {@link MongoQueryExecution} removing documents matching the query. - * - * @author Oliver Gierke - * @author Mark Paluch - * @author Artyom Gabeev - * @author Christoph Strobl - * @since 1.5 - */ - final class DeleteExecution implements MongoQueryExecution { - - private final MongoOperations operations; - private final MongoQueryMethod method; - - public DeleteExecution(MongoOperations operations, MongoQueryMethod method) { - - Assert.notNull(operations, "Operations must not be null"); - Assert.notNull(method, "Method must not be null"); - - this.operations = operations; - this.method = method; - } - - @Override - public @Nullable Object execute(Query query) { - - String collectionName = method.getEntityInformation().getCollectionName(); - Class type = method.getEntityInformation().getJavaType(); - - if (method.isCollectionQuery()) { - return operations.findAllAndRemove(query, type, collectionName); - } - - if (method.isQueryForEntity() && !ClassUtils.isPrimitiveOrWrapper(method.getReturnedObjectType())) { - return operations.findAndRemove(query, type, collectionName); - } - - DeleteResult writeResult = operations.remove(query, type, collectionName); - return writeResult.wasAcknowledged() ? writeResult.getDeletedCount() : 0L; - } - } - /** * {@link MongoQueryExecution} updating documents matching the query. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 4bd6e7db5e..52c5e32555 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -117,7 +117,7 @@ public boolean hasAnnotatedQuery() { * @return */ @Nullable - String getAnnotatedQuery() { + public String getAnnotatedQuery() { return findAnnotatedQuery().orElse(null); } @@ -204,7 +204,7 @@ Optional lookupQueryAnnotation() { return doFindAnnotation(Query.class); } - TypeInformation getReturnType() { + public TypeInformation getReturnType() { return TypeInformation.fromReturnTypeOf(method); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java index 99acb12940..dc51da84ed 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/BsonUtils.java @@ -67,13 +67,13 @@ import org.bson.types.Binary; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.CodecRegistryProvider; import org.springframework.data.mongodb.core.mapping.FieldName; import org.springframework.data.mongodb.core.mapping.FieldName.Type; import org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder; -import org.springframework.data.mongodb.util.json.SpringJsonWriter; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -337,7 +337,7 @@ public static Object toJavaType(BsonValue value) { case BINARY -> { BsonBinary binary = value.asBinary(); - if(binary.getType() != BsonBinarySubType.VECTOR.getValue()) { + if (binary.getType() != BsonBinarySubType.VECTOR.getValue()) { yield binary.getData(); } yield value.asBinary().asVector(); @@ -783,15 +783,39 @@ public static Document mapEntries(Document source, Function { - SpringJsonWriter writer = new SpringJsonWriter(sink); - JSON_CODEC_REGISTRY.get(Document.class).encode(writer, document, EncoderContext.builder().build()); - }; + return sink -> JSON_CODEC_REGISTRY.get(Document.class).encode(new SpringJsonWriter(sink), document, + EncoderContext.builder().build()); } + /** + * Interface to pipe json rendering to a given sink. + * + * @since 5.0 + */ public interface JsonWriter { + + /** + * Write the json output to the given sink. + * + * @param sink the output target + */ void to(StringBuffer sink); + + default String toJsonString() { + + StringBuffer buffer = new StringBuffer(); + to(buffer); + return buffer.toString(); + } } @Contract("null -> null") @@ -1007,6 +1031,14 @@ public void flush() { } } + /** + * Internal {@link Codec} implementation to write + * {@link org.springframework.data.mongodb.core.query.CriteriaDefinition.Placeholder placeholders}. + * + * @since 5.0 + * @author Christoph Strobl + */ + @NullUnmarked static class PlaceholderCodec implements Codec { @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java similarity index 96% rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java index 370a272f53..07eab92a01 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/json/SpringJsonWriter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/SpringJsonWriter.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.util.json; +package org.springframework.data.mongodb.util; import static java.time.format.DateTimeFormatter.ISO_OFFSET_DATE_TIME; @@ -30,13 +30,18 @@ import org.bson.BsonWriter; import org.bson.types.Decimal128; import org.bson.types.ObjectId; +import org.jspecify.annotations.NullUnmarked; import org.springframework.util.StringUtils; /** + * Internal {@link BsonWriter} implementation that allows to render {@link #writePlaceholder(String) placeholders} as + * {@code ?0}. + * * @author Christoph Strobl - * @since 2025/01 + * @since 5.0 */ -public class SpringJsonWriter implements BsonWriter { +@NullUnmarked +class SpringJsonWriter implements BsonWriter { private final StringBuffer buffer; @@ -49,6 +54,7 @@ private enum State { } private static class JsonContext { + private final JsonContext parentContext; private final JsonContextType contextType; private boolean hasElements; @@ -450,6 +456,9 @@ public void pipe(BsonReader reader) { } + /** + * @param placeholder + */ public void writePlaceholder(String placeholder) { write(placeholder); } diff --git a/spring-data-mongodb/src/test/java/example/aot/User.java b/spring-data-mongodb/src/test/java/example/aot/User.java index 28ea5911ed..06022c0a55 100644 --- a/spring-data-mongodb/src/test/java/example/aot/User.java +++ b/spring-data-mongodb/src/test/java/example/aot/User.java @@ -1,19 +1,3 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -37,7 +21,6 @@ /** * @author Christoph Strobl - * @since 2025/01 */ public class User { @@ -51,6 +34,7 @@ public class User { Instant registrationDate; Instant lastSeen; + Long visits; public String getId() { return id; @@ -99,4 +83,12 @@ public Instant getLastSeen() { public void setLastSeen(Instant lastSeen) { this.lastSeen = lastSeen; } + + public Long getVisits() { + return visits; + } + + public void setVisits(Long visits) { + this.visits = visits; + } } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserProjection.java b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java index 06c70f8060..e59598d3a9 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserProjection.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserProjection.java @@ -19,7 +19,6 @@ /** * @author Christoph Strobl - * @since 2025/01 */ public interface UserProjection { diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 104fd8d08e..cdebb4fc50 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -1,11 +1,11 @@ /* - * Copyright 2025. the original author or authors. + * Copyright 2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -15,21 +15,30 @@ */ package example.aot; +import java.time.Instant; +import java.util.Collection; +import java.util.HashSet; import java.util.List; +import java.util.Objects; import java.util.Optional; +import java.util.Set; +import org.springframework.data.annotation.Id; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.repository.Aggregation; +import org.springframework.data.mongodb.repository.Hint; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.ReadPreference; +import org.springframework.data.mongodb.repository.Update; import org.springframework.data.repository.CrudRepository; /** * @author Christoph Strobl - * @since 2025/01 */ public interface UserRepository extends CrudRepository { @@ -47,6 +56,28 @@ public interface UserRepository extends CrudRepository { List findByLastnameStartingWith(String lastname); + List findByLastnameEndsWith(String postfix); + + List findByFirstnameLike(String firstname); + + List findByFirstnameNotLike(String firstname); + + List findByUsernameIn(Collection usernames); + + List findByUsernameNotIn(Collection usernames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + List findByFirstnameOrLastname(String firstname, String lastname); + + List findByVisitsBetween(long from, long to); + + List findByLastSeenGreaterThan(Instant time); + + List findByVisitsExists(boolean exists); + + List findByLastnameNot(String lastname); + List findTop2ByLastnameStartingWith(String lastname); List findByLastnameStartingWithOrderByUsername(String lastname); @@ -66,6 +97,7 @@ public interface UserRepository extends CrudRepository { // TODO: Streaming // TODO: Scrolling // TODO: GeoQueries + // TODO: TextSearch /* Annotated Queries */ @@ -121,8 +153,17 @@ public interface UserRepository extends CrudRepository { @Query(value = "{ 'lastname' : { '$regex' : '^?0' } }", delete = true) List deleteUsersAnnotatedQueryByLastnameStartingWith(String lastname); - // TODO: updates - // TODO: Aggregations + /* Updates */ + + @Update("{ '$inc' : { 'visits' : ?1 } }") + int findUserAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + int updateAllByLastname(String lastname, int increment); + + @Update(pipeline = { "{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }" }) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); /* Derived With Annotated Options */ @@ -143,4 +184,95 @@ public interface UserRepository extends CrudRepository { Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + /* Aggregations */ + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnames(); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + List groupByLastnameAnd(String property); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + List groupByLastnameAnd(String property, Pageable pageable); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + Slice groupByLastnameAndReturnPage(String property, Pageable pageable); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + AggregationResults groupByLastnameAndAsAggregationResults(String property); + + @Aggregation(pipeline = { // + "{ '$match' : { 'posts' : { '$ne' : null } } }", // + "{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", // + "{ '$group' : { '_id' : null, 'total' : { $sum: '$nrPosts' } } }" }) + int sumPosts(); + + @Hint("ln-idx") + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnamesUsingIndex(); + + @ReadPreference("no-such-read-preference") + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }) + List findAllLastnamesWithReadPreference(); + + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$project': { '_id' : '$last_name' } }" }, collation = "no_collation") + List findAllLastnamesWithCollation(); + + class UserAggregate { + + @Id // + private final String lastname; + private final Set names; + + public UserAggregate(String lastname, Collection names) { + this.lastname = lastname; + this.names = new HashSet<>(names); + } + + public String getLastname() { + return this.lastname; + } + + public Set getNames() { + return this.names; + } + + @Override + public String toString() { + return "UserAggregate{" + "lastname='" + lastname + '\'' + ", names=" + names + '}'; + } + + @Override + public boolean equals(Object o) { + if (o == this) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + UserAggregate that = (UserAggregate) o; + return Objects.equals(lastname, that.lastname) && names.equals(that.names); + } + + @Override + public int hashCode() { + return Objects.hash(lastname, names); + } + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java index bef0d34cb4..41759e68c7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/DemoRepo.java @@ -1,19 +1,3 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java deleted file mode 100644 index 9caf74f31c..0000000000 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/MongoRepositoryContributorTests.java +++ /dev/null @@ -1,662 +0,0 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -/* - * Copyright 2025 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.springframework.data.mongodb.aot.generated; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import example.aot.User; -import example.aot.UserProjection; -import example.aot.UserRepository; - -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Supplier; - -import org.bson.Document; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; -import org.springframework.aot.test.generate.TestGenerationContext; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.support.AbstractBeanDefinition; -import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.core.test.tools.TestCompiler; -import org.springframework.data.domain.Limit; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Slice; -import org.springframework.data.domain.Sort; -import org.springframework.data.mongodb.test.util.Client; -import org.springframework.data.mongodb.test.util.MongoClientExtension; -import org.springframework.data.mongodb.test.util.MongoTestTemplate; -import org.springframework.data.mongodb.test.util.MongoTestUtils; -import org.springframework.data.util.Lazy; -import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.util.StringUtils; - -import com.mongodb.client.MongoClient; - -/** - * @author Christoph Strobl - * @since 2025/01 - */ -@ExtendWith(MongoClientExtension.class) -public class MongoRepositoryContributorTests { - - private static final String DB_NAME = "aot-repo-tests"; - private static Verifyer generated; - - @Client static MongoClient client; - - @BeforeAll - static void beforeAll() { - - TestMongoAotRepositoryContext aotContext = new TestMongoAotRepositoryContext(UserRepository.class, null); - TestGenerationContext generationContext = new TestGenerationContext(UserRepository.class); - - new MongoRepositoryContributor(aotContext).contribute(generationContext); - - AbstractBeanDefinition mongoTemplate = BeanDefinitionBuilder.rootBeanDefinition(MongoTestTemplate.class) - .addConstructorArgValue(DB_NAME).getBeanDefinition(); - AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder - .genericBeanDefinition("example.aot.UserRepositoryImpl__Aot").addConstructorArgReference("mongoOperations") - .getBeanDefinition(); - - generated = generateContext(generationContext) // - .register("mongoOperations", mongoTemplate) // - .register("aotUserRepository", aotGeneratedRepository); - } - - @BeforeEach - void beforeEach() { - - MongoTestUtils.flushCollection(DB_NAME, "user", client); - initUsers(); - } - - @Test - void testFindDerivedFinderSingleEntity() { - - generated.verify(methodInvoker -> { - - User user = methodInvoker.invoke("findOneByUsername", "yoda").onBean("aotUserRepository"); - assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); - }); - } - - @Test - void testFindDerivedFinderOptionalEntity() { - - generated.verify(methodInvoker -> { - - Optional user = methodInvoker.invoke("findOptionalOneByUsername", "yoda").onBean("aotUserRepository"); - assertThat(user).isNotNull().containsInstanceOf(User.class) - .hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda")); - }); - } - - @Test - void testDerivedCount() { - - generated.verify(methodInvoker -> { - - Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(value).isEqualTo(2L); - }); - } - - @Test - void testDerivedExists() { - - generated.verify(methodInvoker -> { - - Boolean exists = methodInvoker.invoke("existsUserByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(exists).isTrue(); - }); - } - - @Test - void testDerivedFinderWithoutArguments() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findUserNoArgumentsBy").onBean("aotUserRepository"); - assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); - }); - } - - @Test - void testCountWorksAsExpected() { - - generated.verify(methodInvoker -> { - - Long value = methodInvoker.invoke("countUsersByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(value).isEqualTo(2L); - }); - } - - @Test - void testDerivedFinderReturningList() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han"); - }); - } - - @Test - void testLimitedDerivedFinder() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findTop2ByLastnameStartingWith", "S").onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testSortedDerivedFinder() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWithOrderByUsername", "S") - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testDerivedFinderWithLimitArgument() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testDerivedFinderWithSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testDerivedFinderWithSortAndLimit() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findByLastnameStartingWith", "S", Sort.by("username"), Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testDerivedFinderReturningListWithPageable() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker - .invoke("findByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testDerivedFinderReturningPage() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findPageOfUsersByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testDerivedFinderReturningSlice() { - - generated.verify(methodInvoker -> { - - Slice slice = methodInvoker - .invoke("findSliceOfUserByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(slice.hasNext()).isTrue(); - assertThat(slice.getSize()).isEqualTo(2); - assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testAnnotatedFinderReturningSingleValueWithQuery() { - - generated.verify(methodInvoker -> { - - User user = methodInvoker.invoke("findAnnotatedQueryByUsername", "yoda").onBean("aotUserRepository"); - assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); - }); - } - - @Test - void testAnnotatedCount() { - - generated.verify(methodInvoker -> { - - Long value = methodInvoker.invoke("countAnnotatedQueryByLastname", "Skywalker").onBean("aotUserRepository"); - assertThat(value).isEqualTo(2L); - }); - } - - @Test - void testAnnotatedFinderReturningListWithQuery() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testAnnotatedMultilineFinderWithQuery() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedMultilineQueryByLastname", "S").onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testAnnotatedFinderWithQueryAndLimit() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2)) - .onBean("aotUserRepository"); - assertThat(users).hasSize(2); - }); - } - - @Test - void testAnnotatedFinderWithQueryAndSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Sort.by("username")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testAnnotatedFinderWithQueryLimitAndSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findAnnotatedQueryByLastname", "S", Limit.of(2), Sort.by("username")) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testAnnotatedFinderReturningListWithPageable() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker - .invoke("findAnnotatedQueryByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testAnnotatedFinderReturningPage() { - - generated.verify(methodInvoker -> { - - Page page = methodInvoker - .invoke("findAnnotatedQueryPageOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(page.getTotalElements()).isEqualTo(4); - assertThat(page.getSize()).isEqualTo(2); - assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @Test - void testAnnotatedFinderReturningSlice() { - - generated.verify(methodInvoker -> { - - Slice slice = methodInvoker - .invoke("findAnnotatedQuerySliceOfUsersByLastname", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(slice.hasNext()).isTrue(); - assertThat(slice.getSize()).isEqualTo(2); - assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); - }); - } - - @ParameterizedTest - @ValueSource(strings = { "deleteByUsername", "deleteAnnotatedQueryByUsername" }) - void testDeleteSingle(String methodName) { - - generated.verify(methodInvoker -> { - - User result = methodInvoker.invoke(methodName, "yoda").onBean("aotUserRepository"); - - assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); - }); - - assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); - } - - @ParameterizedTest - @ValueSource(strings = { "deleteByLastnameStartingWith", "deleteAnnotatedQueryByLastnameStartingWith" }) - void testDerivedDeleteMultipleReturningDeleteCount(String methodName) { - - generated.verify(methodInvoker -> { - - Long result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); - - assertThat(result).isEqualTo(4L); - }); - - assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); - } - - @ParameterizedTest - @ValueSource(strings = { "deleteUsersByLastnameStartingWith", "deleteUsersAnnotatedQueryByLastnameStartingWith" }) - void testDerivedDeleteMultipleReturningDeleted(String methodName) { - - generated.verify(methodInvoker -> { - - List result = methodInvoker.invoke(methodName, "S").onBean("aotUserRepository"); - - assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); - }); - - assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); - } - - @Test - void testDerivedFinderWithAnnotatedSort() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findWithAnnotatedSortByLastnameStartingWith", "S") - .onBean("aotUserRepository"); - assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); - }); - } - - @Test - void testDerivedFinderWithAnnotatedFieldsProjection() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findWithAnnotatedFieldsProjectionByLastnameStartingWith", "S") - .onBean("aotUserRepository"); - assertThat(users).allMatch( - user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null); - }); - } - - @Test - void testReadPreferenceAppliedToQuery() { - - generated.verify(methodInvoker -> { - - // check if it fails when trying to parse the read preference to indicate it would get applied - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> methodInvoker.invoke("findWithReadPreferenceByUsername", "S").onBean("aotUserRepository")) - .withMessageContaining("No match for read preference"); - }); - } - - @Test - void testDerivedFinderReturningListOfProjections() { - - generated.verify(methodInvoker -> { - - List users = methodInvoker.invoke("findUserProjectionByLastnameStartingWith", "S") - .onBean("aotUserRepository"); - assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", - "vader"); - }); - } - - @Test - void testDerivedFinderReturningPageOfProjections() { - - generated.verify(methodInvoker -> { - - Page users = methodInvoker - .invoke("findUserProjectionByLastnameStartingWith", "S", PageRequest.of(0, 2, Sort.by("username"))) - .onBean("aotUserRepository"); - assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); - }); - } - - private static void initUsers() { - - Document luke = Document.parse(""" - { - "_id": "id-1", - "username": "luke", - "first_name": "Luke", - "last_name": "Skywalker", - "posts": [ - { - "message": "I have a bad feeling about this.", - "date": { - "$date": "2025-01-15T12:50:33.855Z" - } - } - ], - "_class": "example.springdata.aot.User" - }"""); - - Document leia = Document.parse(""" - { - "_id": "id-2", - "username": "leia", - "first_name": "Leia", - "last_name": "Organa", - "_class": "example.springdata.aot.User" - }"""); - - Document han = Document.parse(""" - { - "_id": "id-3", - "username": "han", - "first_name": "Han", - "last_name": "Solo", - "posts": [ - { - "message": "It's the ship that made the Kessel Run in less than 12 Parsecs.", - "date": { - "$date": "2025-01-15T13:30:33.855Z" - } - } - ], - "_class": "example.springdata.aot.User" - }"""); - - Document chwebacca = Document.parse(""" - { - "_id": "id-4", - "username": "chewbacca", - "_class": "example.springdata.aot.User" - }"""); - - Document yoda = Document.parse( - """ - { - "_id": "id-5", - "username": "yoda", - "posts": [ - { - "message": "Do. Or do not. There is no try.", - "date": { - "$date": "2025-01-15T13:09:33.855Z" - } - }, - { - "message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.", - "date": { - "$date": "2025-01-15T13:53:33.855Z" - } - } - ] - }"""); - - Document vader = Document.parse(""" - { - "_id": "id-6", - "username": "vader", - "first_name": "Anakin", - "last_name": "Skywalker", - "posts": [ - { - "message": "I am your father", - "date": { - "$date": "2025-01-15T13:46:33.855Z" - } - } - ] - }"""); - - Document kylo = Document.parse(""" - { - "_id": "id-7", - "username": "kylo", - "first_name": "Ben", - "last_name": "Solo" - } - """); - - client.getDatabase(DB_NAME).getCollection("user") - .insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo)); - } - - static GeneratedContextBuilder generateContext(TestGenerationContext generationContext) { - return new GeneratedContextBuilder(generationContext); - } - - static class GeneratedContextBuilder implements Verifyer { - - TestGenerationContext generationContext; - Map beanDefinitions = new LinkedHashMap<>(); - Lazy lazyFactory; - - public GeneratedContextBuilder(TestGenerationContext generationContext) { - - this.generationContext = generationContext; - this.lazyFactory = Lazy.of(() -> { - DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory(); - TestCompiler.forSystem().with(generationContext).compile(compiled -> { - - freshBeanFactory.setBeanClassLoader(compiled.getClassLoader()); - for (Entry entry : beanDefinitions.entrySet()) { - freshBeanFactory.registerBeanDefinition(entry.getKey(), entry.getValue()); - } - }); - return freshBeanFactory; - }); - } - - GeneratedContextBuilder register(String name, BeanDefinition beanDefinition) { - this.beanDefinitions.put(name, beanDefinition); - return this; - } - - public Verifyer verify(Consumer methodInvoker) { - methodInvoker.accept(new GeneratedContext(lazyFactory)); - return this; - } - - } - - interface Verifyer { - Verifyer verify(Consumer methodInvoker); - } - - static class GeneratedContext { - - private Supplier delegate; - - public GeneratedContext(Supplier defaultListableBeanFactory) { - this.delegate = defaultListableBeanFactory; - } - - InvocationBuilder invoke(String method, Object... arguments) { - - return new InvocationBuilder() { - @Override - public T onBean(String beanName) { - Object bean = delegate.get().getBean(beanName); - return ReflectionTestUtils.invokeMethod(bean, method, arguments); - } - }; - } - - interface InvocationBuilder { - T onBean(String beanName); - } - - } -} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java new file mode 100644 index 0000000000..2427aec84b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java @@ -0,0 +1,127 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.util.ReflectionUtils; + +/** + * Test Configuration Support Class for generated AOT Repository Fragments based on a Repository Interface. + *

+ * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT + * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method + * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy. + * + * @author Christoph Strobl + */ +public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { + + private final Class repositoryInterface; + private final TestMongoAotRepositoryContext repositoryContext; + + public AotFragmentTestConfigurationSupport(Class repositoryInterface) { + + this.repositoryInterface = repositoryInterface; + this.repositoryContext = new TestMongoAotRepositoryContext(repositoryInterface, null); + } + + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { + + TestGenerationContext generationContext = new TestGenerationContext(repositoryInterface); + + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); + + AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder + .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") // + .addConstructorArgReference("mongoOperations") // + .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); + + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { + beanFactory.setBeanClassLoader(compiled.getClassLoader()); + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragment", aotGeneratedRepository); + }); + + BeanDefinition fragmentFacade = BeanDefinitionBuilder.rootBeanDefinition((Class) repositoryInterface, () -> { + + Object fragment = beanFactory.getBean("fragment"); + Object proxy = getFragmentFacadeProxy(fragment); + + return repositoryInterface.cast(proxy); + }).getBeanDefinition(); + + ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade); + } + + private Object getFragmentFacadeProxy(Object fragment) { + + return Proxy.newProxyInstance(repositoryInterface.getClassLoader(), new Class[] { repositoryInterface }, + (p, method, args) -> { + + Method target = ReflectionUtils.findMethod(fragment.getClass(), method.getName(), method.getParameterTypes()); + + if (target == null) { + throw new NoSuchMethodException("Method [%s] is not implemented by [%s]".formatted(method, target)); + } + + try { + return target.invoke(fragment, args); + } catch (ReflectiveOperationException e) { + ReflectionUtils.handleReflectionException(e); + } + + return null; + }); + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestMongoAotRepositoryContext repositoryContext) { + + return new RepositoryFactoryBeanSupport.FragmentCreationContext() { + + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java new file mode 100644 index 0000000000..1ec0c3609b --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -0,0 +1,650 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatException; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import example.aot.User; +import example.aot.UserProjection; +import example.aot.UserRepository; +import example.aot.UserRepository.UserAggregate; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; + +import org.bson.Document; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.test.util.Client; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.mongodb.test.util.MongoTestUtils; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; +import org.springframework.util.StringUtils; + +import com.mongodb.client.MongoClient; + +/** + * @author Christoph Strobl + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorTests.JpaRepositoryContributorConfiguration.class) +public class MongoRepositoryContributorTests { + + private static final String DB_NAME = "aot-repo-tests"; + + @Client static MongoClient client; + @Autowired UserRepository fragment; + + @Configuration + static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public JpaRepositoryContributorConfiguration() { + super(UserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return new MongoTemplate(client, DB_NAME); + } + } + + @BeforeEach + void beforeEach() { + + MongoTestUtils.flushCollection(DB_NAME, "user", client); + initUsers(); + } + + @Test + void testFindDerivedFinderSingleEntity() { + + User user = fragment.findOneByUsername("yoda"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + } + + @Test + void testFindDerivedFinderOptionalEntity() { + + Optional user = fragment.findOptionalOneByUsername("yoda"); + assertThat(user).isNotNull().containsInstanceOf(User.class) + .hasValueSatisfying(it -> assertThat(it).extracting(User::getUsername).isEqualTo("yoda")); + } + + @Test + void testDerivedCount() { + + Long value = fragment.countUsersByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testDerivedExists() { + + assertThat(fragment.existsUserByLastname("Skywalker")).isTrue(); + } + + @Test + void testDerivedFinderWithoutArguments() { + + List users = fragment.findUserNoArgumentsBy(); + assertThat(users).hasSize(7).hasOnlyElementsOfType(User.class); + } + + @Test + void testCountWorksAsExpected() { + + Long value = fragment.countUsersByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testDerivedFinderReturningList() { + + List users = fragment.findByLastnameStartingWith("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader", "kylo", "han"); + } + + @Test + void testEndingWith() { + + List users = fragment.findByLastnameEndsWith("er"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("luke", "vader"); + } + + @Test + void testLike() { + + List users = fragment.findByFirstnameLike("ei"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("leia"); + } + + @Test + void testNotLike() { + + List users = fragment.findByFirstnameNotLike("ei"); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("leia"); + } + + @Test + void testIn() { + + List users = fragment.findByUsernameIn(List.of("chewbacca", "kylo")); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("chewbacca", "kylo"); + } + + @Test + void testNotIn() { + + List users = fragment.findByUsernameNotIn(List.of("chewbacca", "kylo")); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("chewbacca", "kylo"); + } + + @Test + void testAnd() { + + List users = fragment.findByFirstnameAndLastname("Han", "Solo"); + assertThat(users).extracting(User::getUsername).containsExactly("han"); + } + + @Test + void testOr() { + + List users = fragment.findByFirstnameOrLastname("Han", "Skywalker"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "vader", "luke"); + } + + @Test + void testBetween() { + + List users = fragment.findByVisitsBetween(10, 100); + assertThat(users).extracting(User::getUsername).containsExactly("vader"); + } + + @Test + void testTimeValue() { + + List users = fragment.findByLastSeenGreaterThan(Instant.parse("2025-01-01T00:00:00.000Z")); + assertThat(users).extracting(User::getUsername).containsExactly("luke"); + } + + @Test + void testNot() { + + List users = fragment.findByLastnameNot("Skywalker"); + assertThat(users).extracting(User::getUsername).isNotEmpty().doesNotContain("luke", "vader"); + } + + @Test + void testExistsCriteria() { + + List users = fragment.findByVisitsExists(false); + assertThat(users).extracting(User::getUsername).contains("kylo"); + } + + @Test + void testLimitedDerivedFinder() { + + List users = fragment.findTop2ByLastnameStartingWith("S"); + assertThat(users).hasSize(2); + } + + @Test + void testSortedDerivedFinder() { + + List users = fragment.findByLastnameStartingWithOrderByUsername("S"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithLimitArgument() { + + List users = fragment.findByLastnameStartingWith("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testDerivedFinderWithSort() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithSortAndLimit() { + + List users = fragment.findByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningListWithPageable() { + + List users = fragment.findByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningPage() { + + Page page = fragment.findPageOfUsersByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedFinderReturningSlice() { + + Slice slice = fragment.findSliceOfUserByLastnameStartingWith("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningSingleValueWithQuery() { + + User user = fragment.findAnnotatedQueryByUsername("yoda"); + assertThat(user).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + } + + @Test + void testAnnotatedCount() { + + Long value = fragment.countAnnotatedQueryByLastname("Skywalker"); + assertThat(value).isEqualTo(2L); + } + + @Test + void testAnnotatedFinderReturningListWithQuery() { + + List users = fragment.findAnnotatedQueryByLastname("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedMultilineFinderWithQuery() { + + List users = fragment.findAnnotatedMultilineQueryByLastname("S"); + assertThat(users).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedFinderWithQueryAndLimit() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2)); + assertThat(users).hasSize(2); + } + + @Test + void testAnnotatedFinderWithQueryAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testAnnotatedFinderWithQueryLimitAndSort() { + + List users = fragment.findAnnotatedQueryByLastname("S", Limit.of(2), Sort.by("username")); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningListWithPageable() { + + List users = fragment.findAnnotatedQueryByLastname("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningPage() { + + Page page = fragment.findAnnotatedQueryPageOfUsersByLastname("S", PageRequest.of(0, 2, Sort.by("username"))); + assertThat(page.getTotalElements()).isEqualTo(4); + assertThat(page.getSize()).isEqualTo(2); + assertThat(page.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testAnnotatedFinderReturningSlice() { + + Slice slice = fragment.findAnnotatedQuerySliceOfUsersByLastname("S", + PageRequest.of(0, 2, Sort.by("username"))); + assertThat(slice.hasNext()).isTrue(); + assertThat(slice.getSize()).isEqualTo(2); + assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDeleteSingle() { + + User result = fragment.deleteByUsername("yoda"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @Test + void testDeleteSingleAnnotatedQuery() { + + User result = fragment.deleteAnnotatedQueryByUsername("yoda"); + + assertThat(result).isNotNull().extracting(User::getUsername).isEqualTo("yoda"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(6L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleteCount() { + + Long result = fragment.deleteByLastnameStartingWith("S"); + + assertThat(result).isEqualTo(4L); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleteCountAnnotatedQuery() { + + Long result = fragment.deleteAnnotatedQueryByLastnameStartingWith("S"); + + assertThat(result).isEqualTo(4L); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeleted() { + + List result = fragment.deleteUsersByLastnameStartingWith("S"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedDeleteMultipleReturningDeletedAnnotatedQuery() { + + List result = fragment.deleteUsersAnnotatedQueryByLastnameStartingWith("S"); + + assertThat(result).extracting(User::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + assertThat(client.getDatabase(DB_NAME).getCollection("user").countDocuments()).isEqualTo(3L); + } + + @Test + void testDerivedFinderWithAnnotatedSort() { + + List users = fragment.findWithAnnotatedSortByLastnameStartingWith("S"); + assertThat(users).extracting(User::getUsername).containsExactly("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderWithAnnotatedFieldsProjection() { + + List users = fragment.findWithAnnotatedFieldsProjectionByLastnameStartingWith("S"); + assertThat(users).allMatch( + user -> StringUtils.hasText(user.getUsername()) && user.getLastname() == null && user.getFirstname() == null); + } + + @Test + void testReadPreferenceAppliedToQuery() { + + // check if it fails when trying to parse the read preference to indicate it would get applied + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> fragment.findWithReadPreferenceByUsername("S")) + .withMessageContaining("No match for read preference"); + } + + @Test + void testDerivedFinderReturningListOfProjections() { + + List users = fragment.findUserProjectionByLastnameStartingWith("S"); + assertThat(users).extracting(UserProjection::getUsername).containsExactlyInAnyOrder("han", "kylo", "luke", "vader"); + } + + @Test + void testDerivedFinderReturningPageOfProjections() { + + Page users = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("username"))); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testUpdateWithDerivedQuery() { + + int modifiedCount = fragment.findUserAndIncrementVisitsByLastname("Organa", 42); + + assertThat(modifiedCount).isOne(); + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testUpdateWithAnnotatedQuery() { + + int modifiedCount = fragment.updateAllByLastname("Organa", 42); + + assertThat(modifiedCount).isOne(); + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testAggregationPipelineUpdate() { + + fragment.findAndIncrementVisitsViaPipelineByLastname("Organa", 42); + + assertThat(client.getDatabase(DB_NAME).getCollection("user").find(new Document("_id", "id-2")).first().get("visits", + Integer.class)).isEqualTo(42); + } + + @Test + void testAggregationWithExtractedSimpleResults() { + + List allLastnames = fragment.findAllLastnames(); + assertThat(allLastnames).containsExactlyInAnyOrder("Skywalker", "Solo", "Organa", "Solo", "Skywalker"); + } + + @Test + void testAggregationWithProjectedResults() { + + List allLastnames = fragment.groupByLastnameAnd("first_name"); + assertThat(allLastnames).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + + @Test + void testAggregationWithProjectedResultsLimitedByPageable() { + + List allLastnames = fragment.groupByLastnameAnd("first_name", PageRequest.of(1, 1, Sort.by("_id"))); + assertThat(allLastnames).containsExactly(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")) // + ); + } + + @Test + void testAggregationWithProjectedResultsAsPage() { + + Slice allLastnames = fragment.groupByLastnameAndReturnPage("first_name", + PageRequest.of(1, 1, Sort.by("_id"))); + assertThat(allLastnames.hasPrevious()).isTrue(); + assertThat(allLastnames.hasNext()).isTrue(); + assertThat(allLastnames.getContent()).containsExactly(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")) // + ); + } + + @Test + void testAggregationWithProjectedResultsWrappedInAggregationResults() { + + AggregationResults allLastnames = fragment.groupByLastnameAndAsAggregationResults("first_name"); + assertThat(allLastnames.getMappedResults()).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + + @Test + void testAggregationWithSingleResultExtraction() { + assertThat(fragment.sumPosts()).isEqualTo(5); + } + + @Test + void testAggregationWithHint() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesUsingIndex()) + .withMessageContaining("hint provided does not correspond to an existing index"); + } + + @Test + void testAggregationWithReadPreference() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithReadPreference()) + .withMessageContaining("No match for read preference"); + } + + @Test + void testAggregationWithCollation() { + assertThatException().isThrownBy(() -> fragment.findAllLastnamesWithCollation()) + .withMessageContaining("'locale' is invalid"); + } + + private static void initUsers() { + + Document luke = Document.parse(""" + { + "_id": "id-1", + "username": "luke", + "first_name": "Luke", + "last_name": "Skywalker", + "visits" : 2, + "lastSeen" : { + "$date": "2025-04-01T00:00:00.000Z" + }, + "posts": [ + { + "message": "I have a bad feeling about this.", + "date": { + "$date": "2025-01-15T12:50:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document leia = Document.parse(""" + { + "_id": "id-2", + "username": "leia", + "first_name": "Leia", + "last_name": "Organa", + "_class": "example.springdata.aot.User" + }"""); + + Document han = Document.parse(""" + { + "_id": "id-3", + "username": "han", + "first_name": "Han", + "last_name": "Solo", + "posts": [ + { + "message": "It's the ship that made the Kessel Run in less than 12 Parsecs.", + "date": { + "$date": "2025-01-15T13:30:33.855Z" + } + } + ], + "_class": "example.springdata.aot.User" + }"""); + + Document chwebacca = Document.parse(""" + { + "_id": "id-4", + "username": "chewbacca", + "lastSeen" : { + "$date": "2025-01-01T00:00:00.000Z" + }, + "_class": "example.springdata.aot.User" + }"""); + + Document yoda = Document.parse( + """ + { + "_id": "id-5", + "username": "yoda", + "visits" : 1000, + "posts": [ + { + "message": "Do. Or do not. There is no try.", + "date": { + "$date": "2025-01-15T13:09:33.855Z" + } + }, + { + "message": "Decide you must, how to serve them best. If you leave now, help them you could; but you would destroy all for which they have fought, and suffered.", + "date": { + "$date": "2025-01-15T13:53:33.855Z" + } + } + ] + }"""); + + Document vader = Document.parse(""" + { + "_id": "id-6", + "username": "vader", + "first_name": "Anakin", + "last_name": "Skywalker", + "visits" : 50, + "posts": [ + { + "message": "I am your father", + "date": { + "$date": "2025-01-15T13:46:33.855Z" + } + } + ] + }"""); + + Document kylo = Document.parse(""" + { + "_id": "id-7", + "username": "kylo", + "first_name": "Ben", + "last_name": "Solo" + } + """); + + client.getDatabase(DB_NAME).getCollection("user") + .insertMany(List.of(luke, leia, han, chwebacca, yoda, vader, kylo)); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java similarity index 78% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java index 52d609be63..36b01fa997 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/StubRepositoryInformation.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java @@ -1,19 +1,3 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -29,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.aot.generated; +package org.springframework.data.mongodb.repository.aot; import java.lang.reflect.Method; +import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; import org.springframework.data.repository.core.CrudMethods; import org.springframework.data.repository.core.RepositoryInformation; @@ -41,15 +27,11 @@ import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; import org.springframework.data.repository.core.support.RepositoryComposition; import org.springframework.data.repository.core.support.RepositoryFragment; -import org.springframework.data.util.Streamable; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl - * @since 2025/01 */ - class StubRepositoryInformation implements RepositoryInformation { private final RepositoryMetadata metadata; @@ -124,11 +106,15 @@ public boolean isCustomMethod(Method method) { @Override public boolean isQueryMethod(Method method) { - return false; + if (isBaseClassMethod(method)) { + return false; + } + + return true; } @Override - public Streamable getQueryMethods() { + public List getQueryMethods() { return null; } @@ -141,4 +127,9 @@ public Class getRepositoryBaseClass() { public Method getTargetClassMethod(Method method) { return null; } + + @Override + public RepositoryComposition getRepositoryComposition() { + return baseComposition; + } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java similarity index 77% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 814710e51b..2349524fab 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/generated/TestMongoAotRepositoryContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -1,19 +1,3 @@ -/* - * Copyright 2025. the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - /* * Copyright 2025 the original author or authors. * @@ -21,7 +5,7 @@ * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * https://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, @@ -29,13 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.aot.generated; +package org.springframework.data.mongodb.repository.aot; import java.io.IOException; import java.lang.annotation.Annotation; import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.Environment; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.test.tools.ClassFile; @@ -46,18 +31,16 @@ import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; -import org.springframework.lang.Nullable; /** * @author Christoph Strobl - * @since 2025/01 */ -public class TestMongoAotRepositoryContext implements AotRepositoryContext { +class TestMongoAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Environment environment = new StandardEnvironment(); - public TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java index ea3c9ad023..dad28ae5aa 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/AbstractMongoQueryUnitTests.java @@ -51,6 +51,7 @@ import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.core.ExecutableFindOperation.ExecutableFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.TerminatingUpdate; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.UpdateWithQuery; @@ -104,6 +105,7 @@ class AbstractMongoQueryUnitTests { @Mock UpdateWithQuery updateWithQuery; @Mock UpdateWithUpdate updateWithUpdate; @Mock TerminatingUpdate terminatingUpdate; + @Mock ExecutableRemove executableRemove; @Mock BasicMongoPersistentEntity persitentEntityMock; @Mock MongoMappingContext mappingContextMock; @Mock DeleteResult deleteResultMock; @@ -130,8 +132,9 @@ void setUp() { doReturn(executableUpdate).when(mongoOperationsMock).update(any()); doReturn(updateWithQuery).when(executableUpdate).matching(any(Query.class)); doReturn(terminatingUpdate).when(updateWithQuery).apply(any(UpdateDefinition.class)); - - when(mongoOperationsMock.remove(any(), any(), anyString())).thenReturn(deleteResultMock); + doReturn(executableRemove).when(mongoOperationsMock).remove(any()); + doReturn(executableRemove).when(executableRemove).matching(any(Query.class)); + when(executableRemove.all()).thenReturn(deleteResultMock); when(mongoOperationsMock.updateMulti(any(), any(), any(), anyString())).thenReturn(updateResultMock); } @@ -140,8 +143,7 @@ void testDeleteExecutionCallsRemoveCorrectly() { createQueryForMethod("deletePersonByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" }); - verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons")); - verify(mongoOperationsMock, times(0)).find(any(), any(), any()); + verify(executableRemove, times(1)).all(); } @Test // DATAMONGO-566, DATAMONGO-1040 @@ -149,7 +151,7 @@ void testDeleteExecutionLoadsListOfRemovedDocumentsWhenReturnTypeIsCollectionLik createQueryForMethod("deleteByLastname", String.class).setDeleteQuery(true).execute(new Object[] { "booh" }); - verify(mongoOperationsMock, times(1)).findAllAndRemove(any(), eq(Person.class), eq("persons")); + verify(executableRemove, times(1)).findAndRemove(); } @Test // DATAMONGO-566 @@ -171,7 +173,7 @@ void testDeleteExecutionReturnsNrDocumentsDeletedFromWriteResult() { query.setDeleteQuery(true); assertThat(query.execute(new Object[] { "fake" })).isEqualTo(100L); - verify(mongoOperationsMock, times(1)).remove(any(), eq(Person.class), eq("persons")); + verify(executableRemove, times(1)).all(); } @Test // DATAMONGO-957 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java index 326ccf5f3a..dbd17aa805 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java @@ -15,20 +15,23 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.ArgumentMatchers.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; import java.lang.reflect.Method; import java.util.Arrays; import java.util.Collections; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; @@ -41,6 +44,7 @@ import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFindNear; +import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.convert.DbRefResolver; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; @@ -78,6 +82,7 @@ class MongoQueryExecutionUnitTests { @Mock FindWithQuery operationMock; @Mock TerminatingFind terminatingMock; @Mock TerminatingFindNear terminatingGeoMock; + @Mock ExecutableRemove removeMock; @Mock DbRefResolver dbRefResolver; private Point POINT = new Point(10, 20); @@ -183,38 +188,36 @@ void pagingGeoExecutionRetrievesObjectsForPageableOutOfRange() { @Test // DATAMONGO-2351 void acknowledgedDeleteReturnsDeletedCount() { + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.all()).thenReturn(DeleteResult.acknowledged(10)); Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString())) - .thenReturn(DeleteResult.acknowledged(10)); - - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(10L); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(10L); } @Test // DATAMONGO-2351 void unacknowledgedDeleteReturnsZeroDeletedCount() { + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.all()).thenReturn(DeleteResult.unacknowledged()); Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteAllByLastname", String.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - when(mongoOperationsMock.remove(any(Query.class), any(Class.class), anyString())) - .thenReturn(DeleteResult.unacknowledged()); - - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(0L); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(0L); } @Test // DATAMONGO-1997 void deleteExecutionWithEntityReturnTypeTriggersFindAndRemove() { - Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class); - MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - Person person = new Person(); + doReturn(removeMock).when(removeMock).matching(any(Query.class)); + when(removeMock.findAndRemove()).thenReturn(List.of(person)); - when(mongoOperationsMock.findAndRemove(any(Query.class), any(Class.class), anyString())).thenReturn(person); + Method method = ReflectionUtils.findMethod(PersonRepository.class, "deleteByLastname", String.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - assertThat(new DeleteExecution(mongoOperationsMock, queryMethod).execute(new Query())).isEqualTo(person); + assertThat(new DeleteExecution(removeMock, queryMethod).execute(new Query())).isEqualTo(person); } interface PersonRepository extends Repository { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java similarity index 96% rename from spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java rename to spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java index 57b8df548d..878623944c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/json/SpringJsonWriterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/SpringJsonWriterUnitTests.java @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.mongodb.util.json; +package org.springframework.data.mongodb.util; import static org.assertj.core.api.Assertions.assertThat; @@ -25,10 +25,12 @@ import org.junit.jupiter.api.Test; /** + * Unit tests for {@link SpringJsonWriter}. + * * @author Christoph Strobl - * @since 2025/01 + * @since 5.0 */ -public class SpringJsonWriterUnitTests { +class SpringJsonWriterUnitTests { StringBuffer buffer; SpringJsonWriter writer; diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 9a65ce79b8..6ad8c163ec 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -18,7 +18,9 @@ - + + + diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 221f47c011..a7401fb11f 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -53,6 +53,7 @@ ** xref:mongodb/repositories/cdi-integration.adoc[] ** xref:repositories/query-keywords-reference.adoc[] ** xref:repositories/query-return-types-reference.adoc[] +** xref:mongodb/aot.adoc[] // Observability * xref:observability/observability.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc new file mode 100644 index 0000000000..345b24cb76 --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc @@ -0,0 +1,88 @@ += Ahead of Time Optimizations + +This chapter covers Spring Data's Ahead of Time (AOT) optimizations that build upon {spring-framework-docs}/core/aot.html[Spring's Ahead of Time Optimizations]. + +[[aot.hints]] +== Runtime Hints + +Running an application as a native image requires additional information compared to a regular JVM runtime. +Spring Data contributes {spring-framework-docs}/core/aot.html#aot.hints[Runtime Hints] during AOT processing for native image usage. +These are in particular hints for: + +* Auditing +* `ManagedTypes` to capture the outcome of class-path scans +* Repositories +** Reflection hints for entities, return types, and Spring Data annotations +** Repository fragments +** Querydsl `Q` classes +** Kotlin Coroutine support +* Web support (Jackson Hints for `PagedModel`) + +[[aot.repositories]] +== Ahead of Time Repositories + +AOT Repositories are an extension to AOT processing by pre-generating eligible query method implementations. +Query methods are opaque to developers regarding their underlying queries being executed in a query method call. +AOT repositories contribute query method implementations based on derived or annotated queries, updates or aggregations that are known at build-time. +This optimization moves query method processing from runtime to build-time, which can lead to a significant bootstrap performance improvement as query methods do not need to be analyzed reflectively upon each application start. + +The resulting AOT repository fragment follows the naming scheme of `Impl__Aot` and is placed in the same package as the repository interface. +You can find all queries in their MQL form for generated repository query methods. + +[TIP] +==== +`spring.aot.repositories.enabled` property needs to be set to `true` for repository fragment code generation. +==== + +[NOTE] +==== +Consider AOT repository classes an internal optimization. +Do not use them directly in your code as generation and implementation details may change in future releases. +==== + +=== Running with AOT Repositories + +AOT is a mandatory step to transform a Spring application to a native executable, so it is automatically enabled when running in this mode. +It is also possible to use those optimizations on the JVM by setting the `spring.aot.enabled` and `spring.aot.repositories.enabled` System properties to `true`. + +AOT repositories contribute configuration changes to the actual repository bean registration to register the generated repository fragment. + +[NOTE] +==== +When AOT optimizations are included, some decisions that have been taken at build-time are hard-coded in the application setup. +For instance, profiles that have been enabled at build-time are automatically enabled at runtime as well. +Also, the Spring Data module implementing a repository is fixed. +Changing the implementation requires AOT re-processing. +==== + +=== Eligible Methods in Data MongoDB + +AOT repositories filter methods that are eligible for AOT processing. +These are typically all query methods that are not backed by an xref:repositories/custom-implementations.adoc[implementation fragment]. + +**Supported Features** + +* Derived `find`, `count`, `exists` and `delete` methods +* Query methods annotated with `@Query` (excluding those containing SpEL) +* Methods annotated with `@Aggregation` +* Methods using `@Update` +* `@Hint` & `@ReadPreference` support +* `Page`, `Slice`, and `Optional` return types +* DTO Projections + +**Limitations** + +* `@Meta` annotations are not evaluated. +* Queries / Aggregations / Updates containing `SpEL` cannot be generated. +* Limited `Collation` detection. +* Reserved parameter names (must not be used in method signature) `finder`, `filterQuery`, `countQuery`, `deleteQuery`, `remover` `updateDefinition`, `aggregation`, `aggregationPipeline`, `aggregationUpdate`, `aggregationOptions`, `updater`, `results`, `fields`. + +**Excluded methods** + +* `CrudRepository` and other base interface methods +* Querydsl and Query by Example methods +* Methods whose implementation would be overly complex +* Query Methods obtaining MQL from a file +** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) +** Dynamic projections +** Geospatial Queries From 829383022d92905229986edc64f07e935f6bb13c Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 15 Apr 2025 10:55:01 +0200 Subject: [PATCH 45/74] Fix nullable annotations. Transition nullable annotations previously missed to jspecify. See: #4874 --- .../data/mongodb/core/CollectionOptions.java | 2 +- .../data/mongodb/core/IndexConverters.java | 5 ++--- .../core/MongoClientSettingsFactoryBean.java | 2 +- .../mongodb/core/SortingQueryCursorPreparer.java | 2 +- .../mongodb/core/aggregation/LookupOperation.java | 14 +++++--------- .../encryption/MongoEncryptionConverter.java | 5 ++--- .../mongodb/core/encryption/EncryptionOptions.java | 2 +- .../data/mongodb/core/index/GeospatialIndex.java | 2 +- .../mongodb/core/index/SearchIndexDefinition.java | 2 +- .../data/mongodb/core/index/SearchIndexInfo.java | 2 +- .../data/mongodb/core/index/VectorIndex.java | 2 +- .../mongodb/core/schema/JsonSchemaProperty.java | 2 +- .../mongodb/core/schema/QueryCharacteristics.java | 2 +- .../core/index/VectorIndexIntegrationTests.java | 5 ++--- .../VersionedPersonRepositoryIntegrationTests.java | 2 +- 15 files changed, 22 insertions(+), 29 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java index da1cdfa335..f4d1891703 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java @@ -30,6 +30,7 @@ import org.bson.BsonBinarySubType; import org.bson.BsonNull; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty; @@ -43,7 +44,6 @@ import org.springframework.data.util.Optionals; import org.springframework.lang.CheckReturnValue; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 2008b85f60..9f9295bba3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java @@ -18,10 +18,10 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import com.mongodb.client.model.Collation; @@ -124,8 +124,7 @@ private static Converter getIndexDefinitionIndexO }; } - @Nullable - public static Collation fromDocument(@Nullable Document source) { + public static @Nullable Collation fromDocument(@Nullable Document source) { if (source == null) { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java index 4400db1ea7..813d3a4a04 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java @@ -25,8 +25,8 @@ import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java index c69fb4ad15..1652dca259 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SortingQueryCursorPreparer.java @@ -16,7 +16,7 @@ package org.springframework.data.mongodb.core; import org.bson.Document; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link CursorPreparer} that exposes its {@link Document sort document}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java index a0e0cc03cd..52cd36b5bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/LookupOperation.java @@ -18,12 +18,12 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.aggregation.ExposedFields.ExposedField; import org.springframework.data.mongodb.core.aggregation.FieldsExposingAggregationOperation.InheritsFieldsAggregationOperation; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let; import org.springframework.data.mongodb.core.aggregation.VariableOperators.Let.ExpressionVariable; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,17 +42,13 @@ public class LookupOperation implements FieldsExposingAggregationOperation, Inhe private final String from; - @Nullable // - private final Field localField; + private final @Nullable Field localField; - @Nullable // - private final Field foreignField; + private final @Nullable Field foreignField; - @Nullable // - private final Let let; + private final @Nullable Let let; - @Nullable // - private final AggregationPipeline pipeline; + private final @Nullable AggregationPipeline pipeline; private final ExposedField as; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java index 5bc100c48a..ecab645fe5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/encryption/MongoEncryptionConverter.java @@ -33,6 +33,7 @@ import org.bson.Document; import org.bson.types.Binary; +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.data.mongodb.core.convert.MongoConversionContext; import org.springframework.data.mongodb.core.convert.MongoConversionContext.OperatorContext; @@ -45,7 +46,6 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.Queryable; import org.springframework.data.mongodb.util.BsonUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -72,9 +72,8 @@ public MongoEncryptionConverter(Encryption encryption, En this.keyResolver = keyResolver; } - @Nullable @Override - public Object read(Object value, MongoConversionContext context) { + public @Nullable Object read(Object value, MongoConversionContext context) { Object decrypted = EncryptingConverter.super.read(value, context); return decrypted instanceof BsonValue bsonValue ? BsonUtils.toJavaType(bsonValue) : decrypted; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java index 73a66e4a8a..7a3e8a2c76 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/encryption/EncryptionOptions.java @@ -18,7 +18,7 @@ import java.util.Map; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java index b88acb06a2..c1ce25776b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java @@ -18,8 +18,8 @@ import java.util.Optional; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Collation; -import org.springframework.lang.Nullable; import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java index 9d4315beae..e3ea12baa9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexDefinition.java @@ -16,11 +16,11 @@ package org.springframework.data.mongodb.core.index; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Definition for an Atlas Search Index (Search Index or Vector Index). diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java index 1a657ecf0b..6da94dd130 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/SearchIndexInfo.java @@ -18,12 +18,12 @@ import java.util.function.Supplier; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.Lazy; import org.springframework.data.util.TypeInformation; -import org.springframework.lang.Nullable; /** * Index information for a MongoDB Search Index. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java index d56801c528..aa8daa8c39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/VectorIndex.java @@ -20,13 +20,13 @@ import java.util.function.Consumer; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.convert.QueryMapper; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.util.TypeInformation; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java index a854c6184a..20d735ee03 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/JsonSchemaProperty.java @@ -18,6 +18,7 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.ArrayJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.BooleanJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.DateJsonSchemaProperty; @@ -32,7 +33,6 @@ import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.UntypedJsonSchemaProperty; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.NumericJsonSchemaObject; import org.springframework.data.mongodb.core.schema.TypedJsonSchemaObject.ObjectJsonSchemaObject; -import org.springframework.lang.Nullable; /** * A {@literal property} or {@literal patternProperty} within a {@link JsonSchemaObject} of {@code type : 'object'}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java index 4ec775c5e7..9283bf4afa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/schema/QueryCharacteristics.java @@ -22,9 +22,9 @@ import org.bson.BsonNull; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; -import org.springframework.lang.Nullable; /** * Encapsulation of individual {@link QueryCharacteristic query characteristics} used to define queries that can be diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java index dcd447f81a..387f075cb5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/VectorIndexIntegrationTests.java @@ -22,6 +22,7 @@ import java.util.List; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -34,7 +35,6 @@ import org.springframework.data.mongodb.test.util.AtlasContainer; import org.springframework.data.mongodb.test.util.MongoTestTemplate; import org.springframework.data.mongodb.test.util.MongoTestUtils; -import org.springframework.lang.Nullable; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; @@ -200,8 +200,7 @@ void createsVectorIndexWithFilters() throws InterruptedException { }); } - @Nullable - private Document readRawIndexInfo(String name) { + private @Nullable Document readRawIndexInfo(String name) { AggregateIterable indexes = template.execute(Movie.class, collection -> { return collection.aggregate(List.of(new Document("$listSearchIndexes", new Document("name", name)))); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java index f4e1e0282e..917a1094d8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VersionedPersonRepositoryIntegrationTests.java @@ -19,6 +19,7 @@ import org.bson.Document; import org.bson.types.ObjectId; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -34,7 +35,6 @@ import org.springframework.data.mongodb.test.util.MongoClientExtension; import org.springframework.data.mongodb.test.util.MongoTestUtils; import org.springframework.data.repository.CrudRepository; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; From 01c586c9458a76a383ea82b2803c53d7c0561baf Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 16 Apr 2025 07:38:07 +0200 Subject: [PATCH 46/74] Remove package-info from empty directory. JMX support has been removed but unfortunately when merging changes for jspecify the package-info.java file sneaked back in. See: #4940 --- .../springframework/data/mongodb/monitor/package-info.java | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java deleted file mode 100644 index 40073d6022..0000000000 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/monitor/package-info.java +++ /dev/null @@ -1,6 +0,0 @@ -/** - * MongoDB specific JMX monitoring support. - */ -@org.jspecify.annotations.NullMarked -package org.springframework.data.mongodb.monitor; - From 344a4d92c7100e0248572f1138d8437c2a057c2d Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:21 +0200 Subject: [PATCH 47/74] Prepare 5.0 M2 (2025.1.0). See #4884 --- pom.xml | 20 ++++---------------- src/main/resources/notice.txt | 2 +- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 6c55b87172..6d9b4efb27 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M2 @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-SNAPSHOT + 4.0.0-M2 5.5.0 1.19 @@ -157,20 +157,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 84a338e8d6..392eac521c 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 5.0 M1 (2025.1.0) +Spring Data MongoDB 5.0 M2 (2025.1.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). From 4802deb141ebb41fee0097fbacafc5caf3ef07e0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:29:38 +0200 Subject: [PATCH 48/74] Release version 5.0 M2 (2025.1.0). See #4884 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 6d9b4efb27..4b21ec021a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M2 pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..98c80c9f54 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M2 ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ad3c1338ec..a5b878cffb 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M2 ../pom.xml From bf760a57e07581cc1ba91030400fcfc79e464dc6 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:03 +0200 Subject: [PATCH 49/74] Prepare next development iteration. See #4884 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 4b21ec021a..6d9b4efb27 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M2 + 5.0.0-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 98c80c9f54..fc88571622 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M2 + 5.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index a5b878cffb..ad3c1338ec 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M2 + 5.0.0-SNAPSHOT ../pom.xml From ba3445c5bef240bcc406a78e7fb57a8cf3156187 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 22 Apr 2025 14:32:04 +0200 Subject: [PATCH 50/74] After release cleanups. See #4884 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 6d9b4efb27..95fc8379d9 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M2 + 4.0.0-SNAPSHOT @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-M2 + 4.0.0-SNAPSHOT 5.5.0 1.19 @@ -157,8 +157,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone + From 5d4b8d8230729322e9c99186d5a3f6aebf7a8610 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 7 May 2025 15:53:11 +0200 Subject: [PATCH 51/74] Adopt to AOT changes in Commons. Closes: #4964 --- spring-data-mongodb/pom.xml | 7 + ...MongoRepositoryConfigurationExtension.java | 9 +- ...MongoRepositoryConfigurationExtension.java | 10 +- .../support/MongoRepositoryFactory.java | 42 ++-- .../support/MongoRepositoryFactoryBean.java | 23 ++- .../MongoRepositoryFragmentsContributor.java | 78 +++++++ .../support/QuerydslContributor.java | 70 +++++++ .../ReactiveMongoRepositoryFactory.java | 45 ++-- .../ReactiveMongoRepositoryFactoryBean.java | 23 ++- ...veMongoRepositoryFragmentsContributor.java | 78 +++++++ .../support/ReactiveQuerydslContributor.java | 73 +++++++ .../aot/AotContributionIntegrationTests.java | 109 ++++++++++ .../aot/MongoRepositoryContributorTests.java | 11 +- .../aot/MongoRepositoryMetadataTests.java | 192 ++++++++++++++++++ ...activeAotContributionIntegrationTests.java | 117 +++++++++++ .../aot/TestMongoAotRepositoryContext.java | 12 +- ...positoryFragmentsContributorUnitTests.java | 93 +++++++++ ...positoryFragmentsContributorUnitTests.java | 93 +++++++++ 18 files changed, 1026 insertions(+), 59 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index ad3c1338ec..6f34da5660 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -140,6 +140,13 @@ true + + net.javacrumbs.json-unit + json-unit-assertj + 4.1.0 + test + + diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java index 9db7be0069..48b4000750 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/MongoRepositoryConfigurationExtension.java @@ -23,10 +23,11 @@ import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.data.config.ParsingUtils; -import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.mongodb.repository.MongoRepository; +import org.springframework.data.mongodb.repository.aot.AotMongoRepositoryPostProcessor; import org.springframework.data.mongodb.repository.support.MongoRepositoryFactoryBean; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtensionSupport; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; @@ -55,6 +56,12 @@ public String getModulePrefix() { return "mongo"; } + @Override + public String getRepositoryBaseClassName() { + return SimpleMongoRepository.class.getName(); + } + + @Override public String getRepositoryFactoryBeanClassName() { return MongoRepositoryFactoryBean.class.getName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java index 817cc397c2..457e889bef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/config/ReactiveMongoRepositoryConfigurationExtension.java @@ -23,10 +23,12 @@ import org.springframework.data.config.ParsingUtils; import org.springframework.data.mongodb.repository.ReactiveMongoRepository; import org.springframework.data.mongodb.repository.support.ReactiveMongoRepositoryFactoryBean; +import org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository; import org.springframework.data.repository.config.AnnotationRepositoryConfigurationSource; import org.springframework.data.repository.config.RepositoryConfigurationExtension; import org.springframework.data.repository.config.XmlRepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryMetadata; + import org.w3c.dom.Element; /** @@ -47,7 +49,13 @@ public String getModuleName() { return "Reactive MongoDB"; } - public String getRepositoryFactoryClassName() { + @Override + public String getRepositoryBaseClassName() { + return SimpleReactiveMongoRepository.class.getName(); + } + + @Override + public String getRepositoryFactoryBeanClassName() { return ReactiveMongoRepositoryFactoryBean.class.getName(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index e1abcdc2ab..4ff89c9fdb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -15,15 +15,13 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; -import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -35,7 +33,6 @@ import org.springframework.data.mongodb.repository.query.StringBasedAggregation; import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.querydsl.QuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; @@ -60,6 +57,7 @@ public class MongoRepositoryFactory extends RepositoryFactorySupport { private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final MongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; + private MongoRepositoryFragmentsContributor fragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT; /** * Creates a new {@link MongoRepositoryFactory} with the given {@link MongoOperations}. @@ -76,6 +74,17 @@ public MongoRepositoryFactory(MongoOperations mongoOperations) { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to be used. Defaults to + * {@link MongoRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(MongoRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override public void setBeanClassLoader(@Nullable ClassLoader classLoader) { @@ -99,33 +108,18 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata } /** - * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. Typically - * adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl. + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. + * Typically, adds a {@link QuerydslMongoPredicateExecutor} if the repository interface uses Querydsl. *

- * Can be overridden by subclasses to customize {@link RepositoryFragments}. + * Built-in fragment contribution can be customized by configuring {@link MongoRepositoryFragmentsContributor}. * * @param metadata repository metadata. * @param operations the MongoDB operations manager. - * @return + * @return {@link RepositoryFragments} to be added to the repository. * @since 3.2.1 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, MongoOperations operations) { - - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - if (metadata.isReactiveRepository()) { - throw new InvalidDataAccessApiUsageException( - "Cannot combine Querydsl and reactive repository support in a single interface"); - } - - return RepositoryFragments - .just(new QuerydslMongoPredicateExecutor<>(getEntityInformation(metadata.getDomainType()), operations)); - } - - return RepositoryFragments.empty(); + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType()), operations); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java index cec54de0bb..18c7c5a13c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryBean.java @@ -30,12 +30,14 @@ * {@link org.springframework.beans.factory.FactoryBean} to create {@link MongoRepository} instances. * * @author Oliver Gierke + * @author Mark Paluch */ @SuppressWarnings("NullAway") public class MongoRepositoryFactoryBean, S, ID extends Serializable> extends RepositoryFactoryBeanSupport { private @Nullable MongoOperations operations; + private MongoRepositoryFragmentsContributor repositoryFragmentsContributor = MongoRepositoryFragmentsContributor.DEFAULT; private boolean createIndexesForQueryMethods = false; private boolean mappingContextConfigured = false; @@ -57,6 +59,22 @@ public void setMongoOperations(MongoOperations operations) { this.operations = operations; } + @Override + public MongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor(MongoRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + /** * Configures whether to automatically create indexes for the properties referenced in a query method. * @@ -76,7 +94,8 @@ public void setMappingContext(MappingContext mappingContext) { @Override protected RepositoryFactorySupport createRepositoryFactory() { - RepositoryFactorySupport factory = getFactoryInstance(operations); + MongoRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); if (createIndexesForQueryMethods) { factory.addQueryCreationListener( @@ -92,7 +111,7 @@ protected RepositoryFactorySupport createRepositoryFactory() { * @param operations * @return */ - protected RepositoryFactorySupport getFactoryInstance(MongoOperations operations) { + protected MongoRepositoryFactory getFactoryInstance(MongoOperations operations) { return new MongoRepositoryFactory(operations); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..6d4a409724 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 5.0 + * @see QuerydslMongoPredicateExecutor + */ +public interface MongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + MongoRepositoryFragmentsContributor DEFAULT = QuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code MongoRepositoryFragmentsContributor} that first applies this contributor to its inputs, + * and then applies the {@code after} contributor concatenating effectively both results. If evaluation of either + * contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default MongoRepositoryFragmentsContributor andThen(MongoRepositoryFragmentsContributor after) { + + Assert.notNull(after, "MongoRepositoryFragmentsContributor must not be null"); + + return new MongoRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + return MongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return MongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add + * MongoDB-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java new file mode 100644 index 0000000000..e8460f3697 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/QuerydslContributor.java @@ -0,0 +1,70 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository implements + * {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 5.0 + * @see QuerydslMongoPredicateExecutor + */ +enum QuerydslContributor implements MongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + + if (isQuerydslRepository(metadata)) { + + QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(QuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.structural(QuerydslPredicateExecutor.class, QuerydslMongoPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT && QuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index ae8561bc17..c113c70a5b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -15,13 +15,12 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.springframework.data.querydsl.QuerydslUtils.*; - import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.ReactiveMongoOperations; @@ -34,13 +33,11 @@ import org.springframework.data.mongodb.repository.query.ReactiveStringBasedAggregation; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery; import org.springframework.data.projection.ProjectionFactory; -import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.RepositoryMetadata; import org.springframework.data.repository.core.support.ReactiveRepositoryFactorySupport; import org.springframework.data.repository.core.support.RepositoryComposition.RepositoryFragments; -import org.springframework.data.repository.core.support.RepositoryFragment; import org.springframework.data.repository.query.QueryLookupStrategy; import org.springframework.data.repository.query.QueryLookupStrategy.Key; import org.springframework.data.repository.query.QueryMethodValueEvaluationContextAccessor; @@ -61,6 +58,7 @@ public class ReactiveMongoRepositoryFactory extends ReactiveRepositoryFactorySup private final CrudMethodMetadataPostProcessor crudMethodMetadataPostProcessor = new CrudMethodMetadataPostProcessor(); private final ReactiveMongoOperations operations; private final MappingContext, MongoPersistentProperty> mappingContext; + private ReactiveMongoRepositoryFragmentsContributor fragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT; @Nullable private QueryMethodValueEvaluationContextAccessor accessor; /** @@ -78,6 +76,17 @@ public ReactiveMongoRepositoryFactory(ReactiveMongoOperations mongoOperations) { addRepositoryProxyPostProcessor(crudMethodMetadataPostProcessor); } + /** + * Configures the {@link ReactiveMongoRepositoryFragmentsContributor} to be used. Defaults to + * {@link ReactiveMongoRepositoryFragmentsContributor#DEFAULT}. + * + * @param fragmentsContributor + * @since 5.0 + */ + public void setFragmentsContributor(ReactiveMongoRepositoryFragmentsContributor fragmentsContributor) { + this.fragmentsContributor = fragmentsContributor; + } + @Override public void setBeanClassLoader(@Nullable ClassLoader classLoader) { @@ -96,24 +105,20 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { return SimpleReactiveMongoRepository.class; } + /** + * Creates {@link RepositoryFragments} based on {@link RepositoryMetadata} to add Mongo-specific extensions. + * Typically, adds a {@link ReactiveQuerydslContributor} if the repository interface uses Querydsl. + *

+ * Built-in fragment contribution can be customized by configuring + * {@link ReactiveMongoRepositoryFragmentsContributor}. + * + * @param metadata repository metadata. + * @return {@link RepositoryFragments} to be added to the repository. + */ @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - - RepositoryFragments fragments = RepositoryFragments.empty(); - - boolean isQueryDslRepository = QUERY_DSL_PRESENT - && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); - - if (isQueryDslRepository) { - - MongoEntityInformation entityInformation = getEntityInformation(metadata.getDomainType(), - metadata); - - fragments = fragments.append(RepositoryFragment - .implemented(instantiateClass(ReactiveQuerydslMongoPredicateExecutor.class, entityInformation, operations))); - } - - return fragments; + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType(), metadata), + operations); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java index e3d71325f9..40de5213aa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactoryBean.java @@ -40,6 +40,7 @@ public class ReactiveMongoRepositoryFactoryBean, S, extends RepositoryFactoryBeanSupport { private @Nullable ReactiveMongoOperations operations; + private ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT; private boolean createIndexesForQueryMethods = false; private boolean mappingContextConfigured = false; @@ -61,6 +62,23 @@ public void setReactiveMongoOperations(@Nullable ReactiveMongoOperations operati this.operations = operations; } + @Override + public ReactiveMongoRepositoryFragmentsContributor getRepositoryFragmentsContributor() { + return repositoryFragmentsContributor; + } + + /** + * Configures the {@link MongoRepositoryFragmentsContributor} to contribute built-in fragment functionality to the + * repository. + * + * @param repositoryFragmentsContributor must not be {@literal null}. + * @since 5.0 + */ + public void setRepositoryFragmentsContributor( + ReactiveMongoRepositoryFragmentsContributor repositoryFragmentsContributor) { + this.repositoryFragmentsContributor = repositoryFragmentsContributor; + } + /** * Configures whether to automatically create indexes for the properties referenced in a query method. * @@ -81,7 +99,8 @@ public void setMappingContext(MappingContext mappingContext) { @SuppressWarnings("NullAway") protected RepositoryFactorySupport createRepositoryFactory() { - RepositoryFactorySupport factory = getFactoryInstance(operations); + ReactiveMongoRepositoryFactory factory = getFactoryInstance(operations); + factory.setFragmentsContributor(repositoryFragmentsContributor); if (createIndexesForQueryMethods) { factory.addQueryCreationListener(new IndexEnsuringQueryCreationListener( @@ -97,7 +116,7 @@ protected RepositoryFactorySupport createRepositoryFactory() { * @param operations * @return */ - protected RepositoryFactorySupport getFactoryInstance(ReactiveMongoOperations operations) { + protected ReactiveMongoRepositoryFactory getFactoryInstance(ReactiveMongoOperations operations) { return new ReactiveMongoRepositoryFactory(operations); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java new file mode 100644 index 0000000000..fdf3c3649e --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributor.java @@ -0,0 +1,78 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; +import org.springframework.util.Assert; + +/** + * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing fragments based on the repository. + *

+ * Implementations must define a no-args constructor. + * + * @author Mark Paluch + * @since 5.0 + * @see ReactiveQuerydslMongoPredicateExecutor + */ +public interface ReactiveMongoRepositoryFragmentsContributor extends RepositoryFragmentsContributor { + + ReactiveMongoRepositoryFragmentsContributor DEFAULT = ReactiveQuerydslContributor.INSTANCE; + + /** + * Returns a composed {@code ReactiveMongoRepositoryFragmentsContributor} that first applies this contributor to its + * inputs, and then applies the {@code after} contributor concatenating effectively both results. If evaluation of + * either contributors throws an exception, it is relayed to the caller of the composed contributor. + * + * @param after the contributor to apply after this contributor is applied. + * @return a composed contributor that first applies this contributor and then applies the {@code after} contributor. + */ + default ReactiveMongoRepositoryFragmentsContributor andThen(ReactiveMongoRepositoryFragmentsContributor after) { + + Assert.notNull(after, "ReactiveMongoRepositoryFragmentsContributor must not be null"); + + return new ReactiveMongoRepositoryFragmentsContributor() { + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + return ReactiveMongoRepositoryFragmentsContributor.this.contribute(metadata, entityInformation, operations) + .append(after.contribute(metadata, entityInformation, operations)); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return ReactiveMongoRepositoryFragmentsContributor.this.describe(metadata).append(after.describe(metadata)); + } + }; + } + + /** + * Creates {@link RepositoryComposition.RepositoryFragments} based on {@link RepositoryMetadata} to add + * MongoDB-specific extensions. + * + * @param metadata repository metadata. + * @param entityInformation must not be {@literal null}. + * @param operations must not be {@literal null}. + * @return {@link RepositoryComposition.RepositoryFragments} to be added to the repository. + */ + RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations); + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java new file mode 100644 index 0000000000..2cea75cb44 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslContributor.java @@ -0,0 +1,73 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import static org.springframework.data.querydsl.QuerydslUtils.*; + +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.core.support.RepositoryFragmentsContributor; + +/** + * Reactive MongoDB-specific {@link RepositoryFragmentsContributor} contributing Querydsl fragments if a repository + * implements {@link QuerydslPredicateExecutor}. + * + * @author Mark Paluch + * @since 5.0 + * @see ReactiveQuerydslMongoPredicateExecutor + */ +enum ReactiveQuerydslContributor implements ReactiveMongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + + if (isQuerydslRepository(metadata)) { + + ReactiveQuerydslPredicateExecutor executor = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, + operations); + + return RepositoryComposition.RepositoryFragments + .of(RepositoryFragment.implemented(ReactiveQuerydslPredicateExecutor.class, executor)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + + if (isQuerydslRepository(metadata)) { + return RepositoryComposition.RepositoryFragments.of(RepositoryFragment + .structural(ReactiveQuerydslPredicateExecutor.class, ReactiveQuerydslMongoPredicateExecutor.class)); + } + + return RepositoryComposition.RepositoryFragments.empty(); + } + + private static boolean isQuerydslRepository(RepositoryMetadata metadata) { + return QUERY_DSL_PRESENT + && ReactiveQuerydslPredicateExecutor.class.isAssignableFrom(metadata.getRepositoryInterface()); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java new file mode 100644 index 0000000000..b46b1dfb50 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotContributionIntegrationTests.java @@ -0,0 +1,109 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.mockito.Mockito.*; + +import example.aot.User; +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.mock.env.MockPropertySource; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for AOT processing of imperative repositories. + * + * @author Mark Paluch + */ +class AotContributionIntegrationTests { + + @EnableMongoRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = QuerydslUserRepository.class) }) + static class AotConfiguration extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mock(MongoClient.class); + } + + @Override + protected String getDatabaseName() { + return ""; + } + } + + interface QuerydslUserRepository extends UserRepository, QuerydslPredicateExecutor { + + } + + @Test // GH-3830 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + QuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", QuerydslUserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "IMPERATIVE"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.QuerydslPredicateExecutor").containsEntry( + "fragment", "org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index 1ec0c3609b..94b58200b4 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -15,9 +15,7 @@ */ package org.springframework.data.mongodb.repository.aot; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatException; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.*; import example.aot.User; import example.aot.UserProjection; @@ -32,6 +30,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -55,7 +54,7 @@ * @author Christoph Strobl */ @ExtendWith(MongoClientExtension.class) -@SpringJUnitConfig(classes = MongoRepositoryContributorTests.JpaRepositoryContributorConfiguration.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorTests.MongoRepositoryContributorConfiguration.class) public class MongoRepositoryContributorTests { private static final String DB_NAME = "aot-repo-tests"; @@ -64,9 +63,9 @@ public class MongoRepositoryContributorTests { @Autowired UserRepository fragment; @Configuration - static class JpaRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { - public JpaRepositoryContributorConfiguration() { + public MongoRepositoryContributorConfiguration() { super(UserRepository.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java new file mode 100644 index 0000000000..fd0b051c1e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java @@ -0,0 +1,192 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.support.AbstractApplicationContext; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Integration tests for the {@link UserRepository} JSON metadata via {@link MongoRepositoryContributor}. + * + * @author Mark Paluch + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryMetadataTests.MongoRepositoryContributorConfiguration.class) +class MongoRepositoryMetadataTests { + + @Configuration + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public MongoRepositoryContributorConfiguration() { + super(UserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return mock(MongoOperations.class); + } + + } + + @Autowired AbstractApplicationContext context; + + @Test // GH-3830 + void shouldDocumentBase() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", UserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "IMPERATIVE"); + } + + @Test // GH-3830 + void shouldDocumentDerivedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'countUsersByLastname')].query").isArray().element(0).isObject() + .containsEntry("filter", "{'lastname':?0}"); + } + + @Test // GH-3830 + void shouldDocumentSortedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findByLastnameStartingWithOrderByUsername')].query") // + .isArray().element(0).isObject() // + .containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}") + .containsEntry("sort", "{'username':{'$numberInt':'1'}}"); + } + + @Test // GH-3830 + void shouldDocumentPagedQuery() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findPageOfUsersByLastnameStartingWith')].query").isArray() + .element(0).isObject().containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}"); + } + + @Test // GH-3830 + @Disabled("No support for expressions yet") + void shouldDocumentQueryWithExpression() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findValueExpressionNamedByEmailAddress')].query").isArray() + .first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1"); + } + + @Test // GH-3830 + void shouldDocumentAggregation() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findAllLastnames')].query").isArray().element(0).isObject() + .containsEntry("pipeline", + "[{ '$match' : { 'last_name' : { '$ne' : null } } }, { '$project': { '_id' : '$last_name' } }]"); + } + + @Test // GH-3830 + void shouldDocumentPipelineUpdate() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findAndIncrementVisitsViaPipelineByLastname')].query").isArray() + .element(0).isObject().containsEntry("filter", "{'lastname':?0}").containsEntry("update-pipeline", + "[{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }]"); + } + + @Test // GH-3830 + void shouldDocumentBaseFragment() throws IOException { + + Resource resource = getResource(); + + assertThat(resource).isNotNull(); + assertThat(resource.exists()).isTrue(); + + String json = resource.getContentAsString(StandardCharsets.UTF_8); + System.out.println(json); + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository"); + } + + private Resource getResource() { + + String location = UserRepository.class.getPackageName().replace('.', '/') + "/" + + UserRepository.class.getSimpleName() + ".json"; + return new UrlResource(context.getBeanFactory().getBeanClassLoader().getResource(location)); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java new file mode 100644 index 0000000000..565b8a2052 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.mockito.Mockito.*; + +import example.aot.User; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.aot.AotContext; +import org.springframework.data.mongodb.config.AbstractMongoClientConfiguration; +import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.CrudRepository; +import org.springframework.mock.env.MockPropertySource; + +import com.mongodb.client.MongoClient; + +/** + * Integration tests for AOT processing of reactive repositories. + * + * @author Mark Paluch + */ +class ReactiveAotContributionIntegrationTests { + + @EnableReactiveMongoRepositories(considerNestedRepositories = true, includeFilters = { + @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, value = ReactiveQuerydslUserRepository.class) }) + static class AotConfiguration extends AbstractMongoClientConfiguration { + + @Override + public MongoClient mongoClient() { + return mock(MongoClient.class); + } + + @Override + protected String getDatabaseName() { + return ""; + } + } + + interface ReactiveQuerydslUserRepository + extends CrudRepository, ReactiveQuerydslPredicateExecutor { + + Flux findUserNoArgumentsBy(); + + Mono findOneByUsername(String username); + + } + + @Test // GH-3830 + void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { + + TestGenerationContext generationContext = generate(AotConfiguration.class); + + InputStreamSource metadata = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.RESOURCE, + ReactiveQuerydslUserRepository.class.getName().replace('.', '/') + ".json"); + + InputStreamResource isr = new InputStreamResource(metadata); + String json = isr.getContentAsString(StandardCharsets.UTF_8); + + assertThatJson(json).isObject() // + .containsEntry("name", ReactiveQuerydslUserRepository.class.getName()) // + .containsEntry("module", "MongoDB") // + .containsEntry("type", "REACTIVE"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'findBy')].fragment").isArray().first().isObject() + .containsEntry("interface", "org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor") + .containsEntry("fragment", + "org.springframework.data.mongodb.repository.support.ReactiveQuerydslMongoPredicateExecutor"); + + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() + .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleReactiveMongoRepository"); + } + + private static TestGenerationContext generate(Class... configurationClasses) { + + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.getEnvironment().getPropertySources() + .addFirst(new MockPropertySource().withProperty(AotContext.GENERATED_REPOSITORIES_ENABLED, "true")); + context.register(configurationClasses); + + ApplicationContextAotGenerator generator = new ApplicationContextAotGenerator(); + + TestGenerationContext generationContext = new TestGenerationContext(); + generator.processAheadOfTime(context, generationContext); + return generationContext; + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 2349524fab..7cc47d9566 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -21,12 +21,13 @@ import java.util.Set; import org.jspecify.annotations.Nullable; -import org.springframework.core.env.Environment; -import org.springframework.core.env.StandardEnvironment; -import org.springframework.core.test.tools.ClassFile; + import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ClassPathResource; +import org.springframework.core.test.tools.ClassFile; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.repository.config.AotRepositoryContext; import org.springframework.data.repository.core.RepositoryInformation; @@ -64,6 +65,11 @@ public String getBeanName() { return "dummyRepository"; } + @Override + public String getModuleName() { + return "MongoDB"; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..6d38c5ba5e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.User; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link MongoRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class MongoRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + MongoOperations operations = mock(MongoOperations.class); + when(operations.getConverter()).thenReturn(converter); + + MongoRepositoryFragmentsContributor contributor = MongoRepositoryFragmentsContributor.DEFAULT + .andThen(MyMongoRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(QuerydslMongoPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyMongoRepositoryFragmentsContributor implements MongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, MongoOperations operations) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, QuerydslPredicateExecutor {} + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java new file mode 100644 index 0000000000..d3cc84672a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.support; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.util.Iterator; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.User; +import org.springframework.data.mongodb.repository.query.MongoEntityInformation; +import org.springframework.data.querydsl.ReactiveQuerydslPredicateExecutor; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AbstractRepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFragment; + +/** + * Unit tests for {@link ReactiveMongoRepositoryFragmentsContributor}. + * + * @author Mark Paluch + */ +class ReactiveMongoRepositoryFragmentsContributorUnitTests { + + @Test // GH-3279 + void composedContributorShouldCreateFragments() { + + MongoMappingContext mappingContext = new MongoMappingContext(); + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext); + ReactiveMongoOperations operations = mock(ReactiveMongoOperations.class); + when(operations.getConverter()).thenReturn(converter); + + ReactiveMongoRepositoryFragmentsContributor contributor = ReactiveMongoRepositoryFragmentsContributor.DEFAULT + .andThen(MyMongoRepositoryFragmentsContributor.INSTANCE); + + RepositoryComposition.RepositoryFragments fragments = contributor.contribute( + AbstractRepositoryMetadata.getMetadata(QuerydslUserRepository.class), + new MappingMongoEntityInformation<>(mappingContext.getPersistentEntity(User.class)), operations); + + assertThat(fragments).hasSize(2); + + Iterator> iterator = fragments.iterator(); + + RepositoryFragment querydsl = iterator.next(); + assertThat(querydsl.getImplementationClass()).contains(ReactiveQuerydslMongoPredicateExecutor.class); + + RepositoryFragment additional = iterator.next(); + assertThat(additional.getImplementationClass()).contains(MyFragment.class); + } + + enum MyMongoRepositoryFragmentsContributor implements ReactiveMongoRepositoryFragmentsContributor { + + INSTANCE; + + @Override + public RepositoryComposition.RepositoryFragments contribute(RepositoryMetadata metadata, + MongoEntityInformation entityInformation, ReactiveMongoOperations operations) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + + @Override + public RepositoryComposition.RepositoryFragments describe(RepositoryMetadata metadata) { + return RepositoryComposition.RepositoryFragments.just(new MyFragment()); + } + } + + static class MyFragment { + + } + + interface QuerydslUserRepository extends Repository, ReactiveQuerydslPredicateExecutor {} + +} From b2d9547b71e7f39bb31d145ce59a86bb3fc53787 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 9 May 2025 09:29:29 +0200 Subject: [PATCH 52/74] Update issue reference in tests. Original Pull Request: #4964 --- .../aot/MongoRepositoryMetadataTests.java | 23 +++++++++---------- ...activeAotContributionIntegrationTests.java | 7 +++--- ...positoryFragmentsContributorUnitTests.java | 8 +++---- ...positoryFragmentsContributorUnitTests.java | 8 +++---- 4 files changed, 22 insertions(+), 24 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java index fd0b051c1e..67017720eb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.aot; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; import example.aot.UserRepository; @@ -27,7 +27,6 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -63,7 +62,7 @@ MongoOperations mongoOperations() { @Autowired AbstractApplicationContext context; - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentBase() throws IOException { Resource resource = getResource(); @@ -79,7 +78,7 @@ void shouldDocumentBase() throws IOException { .containsEntry("type", "IMPERATIVE"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentDerivedQuery() throws IOException { Resource resource = getResource(); @@ -93,7 +92,7 @@ void shouldDocumentDerivedQuery() throws IOException { .containsEntry("filter", "{'lastname':?0}"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentSortedQuery() throws IOException { Resource resource = getResource(); @@ -109,7 +108,7 @@ void shouldDocumentSortedQuery() throws IOException { .containsEntry("sort", "{'username':{'$numberInt':'1'}}"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentPagedQuery() throws IOException { Resource resource = getResource(); @@ -123,7 +122,7 @@ void shouldDocumentPagedQuery() throws IOException { .element(0).isObject().containsEntry("filter", "{'lastname':{'$regex':/^\\Q?0\\E/}}"); } - @Test // GH-3830 + @Test // GH-4964 @Disabled("No support for expressions yet") void shouldDocumentQueryWithExpression() throws IOException { @@ -138,7 +137,7 @@ void shouldDocumentQueryWithExpression() throws IOException { .first().isObject().containsEntry("query", "select u from User u where u.emailAddress = :__$synthetic$__1"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentAggregation() throws IOException { Resource resource = getResource(); @@ -153,7 +152,7 @@ void shouldDocumentAggregation() throws IOException { "[{ '$match' : { 'last_name' : { '$ne' : null } } }, { '$project': { '_id' : '$last_name' } }]"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentPipelineUpdate() throws IOException { Resource resource = getResource(); @@ -168,7 +167,7 @@ void shouldDocumentPipelineUpdate() throws IOException { "[{ '$set' : { 'visits' : { '$ifNull' : [ {'$add' : [ '$visits', ?1 ] }, ?1 ] } } }]"); } - @Test // GH-3830 + @Test // GH-4964 void shouldDocumentBaseFragment() throws IOException { Resource resource = getResource(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java index 565b8a2052..24b89345a8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/ReactiveAotContributionIntegrationTests.java @@ -15,8 +15,8 @@ */ package org.springframework.data.mongodb.repository.aot; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; -import static org.mockito.Mockito.*; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; +import static org.mockito.Mockito.mock; import example.aot.User; import reactor.core.publisher.Flux; @@ -26,7 +26,6 @@ import java.nio.charset.StandardCharsets; import org.junit.jupiter.api.Test; - import org.springframework.aot.generate.GeneratedFiles; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; @@ -75,7 +74,7 @@ interface ReactiveQuerydslUserRepository } - @Test // GH-3830 + @Test // GH-4964 void shouldGenerateMetadataForBaseRepositoryAndQuerydslFragment() throws IOException { TestGenerationContext generationContext = generate(AotConfiguration.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java index 6d38c5ba5e..564115fed0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFragmentsContributorUnitTests.java @@ -15,13 +15,13 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.Iterator; import org.junit.jupiter.api.Test; - import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -42,7 +42,7 @@ */ class MongoRepositoryFragmentsContributorUnitTests { - @Test // GH-3279 + @Test // GH-4964 void composedContributorShouldCreateFragments() { MongoMappingContext mappingContext = new MongoMappingContext(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java index d3cc84672a..065ff27654 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFragmentsContributorUnitTests.java @@ -15,13 +15,13 @@ */ package org.springframework.data.mongodb.repository.support; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; import java.util.Iterator; import org.junit.jupiter.api.Test; - import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -42,7 +42,7 @@ */ class ReactiveMongoRepositoryFragmentsContributorUnitTests { - @Test // GH-3279 + @Test // GH-4964 void composedContributorShouldCreateFragments() { MongoMappingContext mappingContext = new MongoMappingContext(); From fb3382fac335f29c7cb0c363af371041fe028f6f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 17 Apr 2025 12:29:58 +0200 Subject: [PATCH 53/74] Add support for fluent QueryResultConverter. Closes: #4949 --- .../mongodb/core/EntityResultConverter.java | 33 ++++++ .../core/ExecutableAggregationOperation.java | 16 ++- ...ExecutableAggregationOperationSupport.java | 28 +++-- .../mongodb/core/ExecutableFindOperation.java | 53 ++++++++- .../core/ExecutableFindOperationSupport.java | 76 ++++++++++--- .../data/mongodb/core/MongoTemplate.java | 106 ++++++++++++++---- .../mongodb/core/QueryResultConverter.java | 85 ++++++++++++++ .../core/ReactiveAggregationOperation.java | 15 ++- .../ReactiveAggregationOperationSupport.java | 26 +++-- .../mongodb/core/ReactiveFindOperation.java | 45 +++++++- .../core/ReactiveFindOperationSupport.java | 71 +++++++++--- .../mongodb/core/ReactiveMongoTemplate.java | 67 ++++++++--- ...eAggregationOperationSupportUnitTests.java | 19 +++- .../ExecutableFindOperationSupportTests.java | 35 ++++++ .../mongodb/core/MongoTemplateUnitTests.java | 14 +-- ...eAggregationOperationSupportUnitTests.java | 9 +- .../ReactiveFindOperationSupportTests.java | 38 +++++++ .../core/ReactiveMongoTemplateUnitTests.java | 16 +-- .../core/aggregation/AggregationTests.java | 57 +++++++++- .../aggregation/ReactiveAggregationTests.java | 24 ++++ 20 files changed, 723 insertions(+), 110 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java new file mode 100644 index 0000000000..c04ae9d603 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/EntityResultConverter.java @@ -0,0 +1,33 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import org.bson.Document; + +enum EntityResultConverter implements QueryResultConverter { + + INSTANCE; + + @Override + public Object mapDocument(Document document, ConversionResultSupplier reader) { + return reader.get(); + } + + @Override + public QueryResultConverter andThen(QueryResultConverter after) { + return (QueryResultConverter) after; + } +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java index 67ed188655..e4becc491a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java @@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.lang.Contract; /** * {@link ExecutableAggregationOperation} allows creation and execution of MongoDB aggregation operations in a fluent @@ -45,7 +46,7 @@ public interface ExecutableAggregationOperation { /** * Start creating an aggregation operation that returns results mapped to the given domain type.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different - * input type for he aggregation. + * input type for the aggregation. * * @param domainType must not be {@literal null}. * @return new instance of {@link ExecutableAggregation}. @@ -76,10 +77,23 @@ interface AggregationWithCollection { * Trigger execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ interface TerminatingAggregation { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingAggregation}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingAggregation map(QueryResultConverter converter); + /** * Apply pipeline operations as specified and get all matching elements. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java index d28afada0a..13dc8cd436 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupport.java @@ -44,25 +44,28 @@ public ExecutableAggregation aggregateAndReturn(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableAggregationSupport<>(template, domainType, null, null); + return new ExecutableAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableAggregationSupport + static class ExecutableAggregationSupport implements AggregationWithAggregation, ExecutableAggregation, TerminatingAggregation { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; + private final QueryResultConverter resultConverter; private final @Nullable Aggregation aggregation; private final @Nullable String collection; - public ExecutableAggregationSupport(MongoTemplate template, Class domainType, @Nullable Aggregation aggregation, + public ExecutableAggregationSupport(MongoTemplate template, Class domainType, + QueryResultConverter resultConverter, @Nullable Aggregation aggregation, @Nullable String collection) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.aggregation = aggregation; this.collection = collection; } @@ -72,7 +75,7 @@ public AggregationWithAggregation inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection); + return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); } @Override @@ -80,21 +83,30 @@ public TerminatingAggregation by(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); - return new ExecutableAggregationSupport<>(template, domainType, aggregation, collection); + return new ExecutableAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); + } + + @Override + public TerminatingAggregation map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ExecutableAggregationSupport<>(template, domainType, this.resultConverter.andThen(converter), + aggregation, collection); } @Override public AggregationResults all() { Assert.notNull(aggregation, "Aggregation must be set first"); - return template.aggregate(aggregation, getCollectionName(aggregation), domainType); + return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, resultConverter); } @Override public Stream stream() { Assert.notNull(aggregation, "Aggregation must be set first"); - return template.aggregateStream(aggregation, getCollectionName(aggregation), domainType); + return template.doAggregateStream(aggregation, getCollectionName(aggregation), domainType, resultConverter, null); } private String getCollectionName(@Nullable Aggregation aggregation) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java index 21cb37ae86..2fff730ad4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java @@ -28,6 +28,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.MongoCollection; @@ -71,9 +72,33 @@ public interface ExecutableFindOperation { * Trigger find execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ - interface TerminatingFind { + interface TerminatingFind extends TerminatingResults, TerminatingProjection { + + } + + /** + * Trigger find execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since x.y + */ + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Get exactly zero or one result. @@ -142,6 +167,16 @@ default Optional first() { */ Window scroll(ScrollPosition scrollPosition); + } + + /** + * Trigger find execution by calling one of the terminating methods. + * + * @author Christoph Strobl + * @since x.y + */ + interface TerminatingProjection { + /** * Get the number of matching elements.
* This method uses an @@ -160,16 +195,30 @@ default Optional first() { * @return {@literal true} if at least one matching element exists. */ boolean exists(); + } /** - * Trigger geonear execution by calling one of the terminating methods. + * Trigger {@code geoNear} execution by calling one of the terminating methods. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ interface TerminatingFindNear { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingFindNear map(QueryResultConverter converter); + /** * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java index af8c80903a..46289ecfa4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupport.java @@ -23,6 +23,7 @@ import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Window; +import org.springframework.data.geo.GeoResults; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.SerializationUtils; @@ -57,7 +58,8 @@ public ExecutableFind query(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableFindSupport<>(template, domainType, domainType, null, ALL_QUERY); + return new ExecutableFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null, + ALL_QUERY); } /** @@ -65,19 +67,22 @@ public ExecutableFind query(Class domainType) { * @author Christoph Strobl * @since 2.0 */ - static class ExecutableFindSupport + static class ExecutableFindSupport implements ExecutableFind, FindWithCollection, FindWithProjection, FindWithQuery { private final MongoTemplate template; private final Class domainType; - private final Class returnType; + private final Class returnType; + private final QueryResultConverter resultConverter; private final @Nullable String collection; private final Query query; - ExecutableFindSupport(MongoTemplate template, Class domainType, Class returnType, @Nullable String collection, + ExecutableFindSupport(MongoTemplate template, Class domainType, Class returnType, + QueryResultConverter resultConverter, @Nullable String collection, Query query) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.returnType = returnType; this.collection = collection; this.query = query; @@ -89,7 +94,7 @@ public FindWithProjection inCollection(String collection) { Assert.hasText(collection, "Collection name must not be null nor empty"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query); } @Override @@ -98,7 +103,8 @@ public FindWithQuery as(Class returnType) { Assert.notNull(returnType, "ReturnType must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection, + query); } @Override @@ -107,7 +113,16 @@ public TerminatingFind matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ExecutableFindSupport<>(template, domainType, returnType, collection, query); + return new ExecutableFindSupport<>(template, domainType, returnType, resultConverter, collection, query); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ExecutableFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter), + collection, query); } @Override @@ -146,12 +161,13 @@ public Stream stream() { @Override public Window scroll(ScrollPosition scrollPosition) { - return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName()); + return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter, + getCollectionName()); } @Override public TerminatingFindNear near(NearQuery nearQuery) { - return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType); + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter); } @Override @@ -179,17 +195,17 @@ private List doFind(@Nullable CursorPreparer preparer) { Document fieldsObject = query.getFieldsObject(); return template.doFind(template.createDelegate(query), getCollectionName(), queryObject, fieldsObject, domainType, - returnType, getCursorPreparer(query, preparer)); + returnType, resultConverter, getCursorPreparer(query, preparer)); } private List doFindDistinct(String field) { return template.findDistinct(query, field, getCollectionName(), domainType, - returnType == domainType ? (Class) Object.class : returnType); + returnType == domainType ? (Class) Object.class : returnType); } private Stream doStream() { - return template.doStream(query, domainType, getCollectionName(), returnType); + return template.doStream(query, domainType, getCollectionName(), returnType, resultConverter); } private CursorPreparer getCursorPreparer(Query query, @Nullable CursorPreparer preparer) { @@ -203,6 +219,31 @@ private String getCollectionName() { private String asString() { return SerializationUtils.serializeToJsonSafely(query); } + + class TerminatingFindNearSupport implements TerminatingFindNear { + + private final NearQuery nearQuery; + private final QueryResultConverter resultConverter; + + public TerminatingFindNearSupport(NearQuery nearQuery, + QueryResultConverter resultConverter) { + this.nearQuery = nearQuery; + this.resultConverter = resultConverter; + } + + @Override + public TerminatingFindNear map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter)); + } + + @Override + public GeoResults all() { + return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter); + } + } } /** @@ -245,19 +286,19 @@ CursorPreparer limit(int limit) { * @author Christoph Strobl * @since 2.1 */ - static class DistinctOperationSupport implements TerminatingDistinct { + static class DistinctOperationSupport implements TerminatingDistinct { private final String field; - private final ExecutableFindSupport delegate; + private final ExecutableFindSupport delegate; - public DistinctOperationSupport(ExecutableFindSupport delegate, String field) { + public DistinctOperationSupport(ExecutableFindSupport delegate, String field) { this.delegate = delegate; this.field = field; } @Override - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) @Contract("_ -> new") public TerminatingDistinct as(Class resultType) { @@ -272,12 +313,13 @@ public TerminatingDistinct matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new DistinctOperationSupport<>((ExecutableFindSupport) delegate.matching(query), field); + return new DistinctOperationSupport<>((ExecutableFindSupport) delegate.matching(query), field); } @Override public List all() { return delegate.doFindDistinct(field); } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 5c7df76cc5..edf8a069a3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -131,6 +131,7 @@ import org.springframework.data.mongodb.core.validation.Validator; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.CloseableIterator; +import org.springframework.data.util.Lazy; import org.springframework.data.util.Optionals; import org.springframework.lang.Contract; import org.springframework.util.Assert; @@ -516,13 +517,19 @@ public Stream stream(Query query, Class entityType, String collectionN @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Stream doStream(Query query, Class entityType, String collectionName, Class returnType) { + return doStream(query, entityType, collectionName, returnType, QueryResultConverter.entity()); + } + + @SuppressWarnings("ConstantConditions") + Stream doStream(Query query, Class entityType, String collectionName, Class returnType, + QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityType, "Entity type must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); Assert.notNull(returnType, "ReturnType must not be null"); - return execute(collectionName, (CollectionCallback>) collection -> { + return execute(collectionName, (CollectionCallback>) collection -> { MongoPersistentEntity persistentEntity = mappingContext.getPersistentEntity(entityType); @@ -536,8 +543,10 @@ protected Stream doStream(Query query, Class entityType, String collec FindIterable cursor = new QueryCursorPreparer(query, entityType).initiateFind(collection, col -> readPreference.prepare(col).find(mappedQuery, Document.class).projection(mappedFields)); + DocumentCallback resultReader = getResultReader(projection, collectionName, resultConverter); + return new CloseableIterableCursorAdapter<>(cursor, exceptionTranslator, - new ProjectingReadCallback<>(mongoConverter, projection, collectionName)).stream(); + resultReader).stream(); }); } @@ -936,10 +945,11 @@ public Window scroll(Query query, Class entityType) { @Override public Window scroll(Query query, Class entityType, String collectionName) { - return doScroll(query, entityType, entityType, collectionName); + return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName); } - Window doScroll(Query query, Class sourceClass, Class targetClass, String collectionName) { + Window doScroll(Query query, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(collectionName, "CollectionName must not be null"); @@ -947,7 +957,7 @@ Window doScroll(Query query, Class sourceClass, Class targetClass, Assert.notNull(targetClass, "Target type must not be null"); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); - ProjectingReadCallback callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE; if (query.hasKeyset()) { @@ -955,14 +965,14 @@ Window doScroll(Query query, Class sourceClass, Class targetClass, KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query, operations.getIdPropertyName(sourceClass)); - List result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(), + List result = doFind(collectionName, createDelegate(query), keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass, new QueryCursorPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback); return ScrollUtils.createWindow(query, result, sourceClass, operations); } - List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), + List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), sourceClass, new QueryCursorPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback); @@ -1054,6 +1064,11 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col } public GeoResults geoNear(NearQuery near, Class domainType, String collectionName, Class returnType) { + return doGeoNear(near, domainType, collectionName, returnType, QueryResultConverter.entity()); + } + + GeoResults doGeoNear(NearQuery near, Class domainType, String collectionName, Class returnType, + QueryResultConverter resultConverter) { if (near == null) { throw new InvalidDataAccessApiUsageException("NearQuery must not be null"); @@ -1085,15 +1100,15 @@ public GeoResults geoNear(NearQuery near, Class domainType, String col AggregationResults results = aggregate($geoNear, collection, Document.class); EntityProjection projection = operations.introspectProjection(returnType, domainType); - DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); + DocumentCallback> callback = new GeoNearResultDocumentCallback<>(distanceField, + getResultReader(projection, collectionName, resultConverter), near.getMetric()); - List> result = new ArrayList<>(results.getMappedResults().size()); + List> result = new ArrayList<>(results.getMappedResults().size()); BigDecimal aggregate = BigDecimal.ZERO; for (Document element : results) { - GeoResult geoResult = callback.doWith(element); + GeoResult geoResult = callback.doWith(element); aggregate = aggregate.add(BigDecimal.valueOf(geoResult.getDistance().getValue())); result.add(geoResult); } @@ -2060,7 +2075,7 @@ public AggregationResults aggregate(TypedAggregation aggregation, Clas @Override public AggregationResults aggregate(TypedAggregation aggregation, String inputCollectionName, Class outputType) { - return aggregate(aggregation, inputCollectionName, outputType, null); + return aggregate(aggregation, inputCollectionName, outputType, (AggregationOperationContext) null); } @Override @@ -2073,7 +2088,7 @@ public AggregationResults aggregate(Aggregation aggregation, Class inp @Override public AggregationResults aggregate(Aggregation aggregation, String collectionName, Class outputType) { - return aggregate(aggregation, collectionName, outputType, null); + return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity()); } @Override @@ -2204,11 +2219,25 @@ private AggregationResults doAggregate(Aggregation aggregation, String co return doAggregate(aggregation, collectionName, outputType, context.getAggregationOperationContext()); } + AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter) { + + return doAggregate(aggregation, collectionName, outputType, resultConverter, queryOperations + .createAggregation(aggregation, (AggregationOperationContext) null).getAggregationOperationContext()); + } + @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, AggregationOperationContext context) { + return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity(), context); + } + + @SuppressWarnings("ConstantConditions") + AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter, AggregationOperationContext context) { - ReadDocumentCallback callback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback callback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); AggregationOptions options = aggregation.getOptions(); AggregationUtil aggregationUtil = new AggregationUtil(queryMapper, mappingContext); @@ -2287,9 +2316,15 @@ protected AggregationResults doAggregate(Aggregation aggregation, String }); } - @SuppressWarnings({ "ConstantConditions", "NullAway" }) protected Stream aggregateStream(Aggregation aggregation, String collectionName, Class outputType, @Nullable AggregationOperationContext context) { + return doAggregateStream(aggregation, collectionName, outputType, QueryResultConverter.entity(), context); + } + + @SuppressWarnings({ "ConstantConditions", "NullAway" }) + protected Stream doAggregateStream(Aggregation aggregation, String collectionName, Class outputType, + QueryResultConverter resultConverter, + @Nullable AggregationOperationContext context) { Assert.notNull(aggregation, "Aggregation pipeline must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); @@ -2306,7 +2341,8 @@ protected Stream aggregateStream(Aggregation aggregation, String collecti String.format("Streaming aggregation: %s in collection %s", serializeToJsonSafely(pipeline), collectionName)); } - ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback readCallback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); return execute(collectionName, (CollectionCallback>) collection -> { @@ -2670,11 +2706,12 @@ protected List doFind(String collectionName, * * @since 2.0 */ - List doFind(CollectionPreparer> collectionPreparer, String collectionName, - Document query, Document fields, Class sourceClass, Class targetClass, CursorPreparer preparer) { + List doFind(CollectionPreparer> collectionPreparer, String collectionName, + Document query, Document fields, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, CursorPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, projection); @@ -2690,8 +2727,9 @@ List doFind(CollectionPreparer> collectionPr collectionName)); } + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); return executeFindMultiInternal(new FindCallback(collectionPreparer, mappedQuery, mappedFields, null), preparer, - new ProjectingReadCallback<>(mongoConverter, projection, collectionName), collectionName); + callback, collectionName); } /** @@ -3014,6 +3052,16 @@ private void executeQueryInternal(CollectionCallback> col } } + @SuppressWarnings("unchecked") + private DocumentCallback getResultReader(EntityProjection projection, String collectionName, + QueryResultConverter resultConverter) { + + DocumentCallback readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + + return resultConverter == QueryResultConverter.entity() ? (DocumentCallback) readCallback + : new QueryResultConverterCallback(resultConverter, readCallback); + } + public PersistenceExceptionTranslator getExceptionTranslator() { return exceptionTranslator; } @@ -3373,6 +3421,24 @@ public T doWith(Document document) { } } + static final class QueryResultConverterCallback implements DocumentCallback { + + private final QueryResultConverter converter; + private final DocumentCallback delegate; + + QueryResultConverterCallback(QueryResultConverter converter, DocumentCallback delegate) { + this.converter = converter; + this.delegate = delegate; + } + + @Override + public R doWith(Document object) { + + Lazy lazy = Lazy.of(() -> delegate.doWith(object)); + return converter.mapDocument(object, lazy::get); + } + } + /** * {@link DocumentCallback} transforming {@link Document} into the given {@code targetType} or decorating the * {@code sourceType} with a {@literal projection} in case the {@code targetType} is an {@literal interface}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java new file mode 100644 index 0000000000..e271ee23cc --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java @@ -0,0 +1,85 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import org.bson.Document; + +/** + * Converter for MongoDB query results. + *

+ * This is a functional interface that allows for mapping a {@link Document} to a result type. + * {@link #mapDocument(Document, ConversionResultSupplier) row mapping} can obtain upstream a + * {@link ConversionResultSupplier upstream converter} to enrich the final result object. This is useful when e.g. + * wrapping result objects where the wrapper needs to obtain information from the actual {@link Document}. + * + * @param object type accepted by this converter. + * @param the returned result type. + * @author Mark Paluch + * @since x.x + */ +@FunctionalInterface +public interface QueryResultConverter { + + /** + * Returns a function that returns the materialized entity. + * + * @param the type of the input and output entity to the function. + * @return a function that returns the materialized entity. + */ + @SuppressWarnings("unchecked") + static QueryResultConverter entity() { + return (QueryResultConverter) EntityResultConverter.INSTANCE; + } + + /** + * Map a {@link Document} that is read from the MongoDB query/aggregation operation to a query result. + * + * @param document the raw document from the MongoDB query/aggregation result. + * @param reader reader object that supplies an upstream result from an earlier converter. + * @return the mapped result. + */ + R mapDocument(Document document, ConversionResultSupplier reader); + + /** + * Returns a composed function that first applies this function to its input, and then applies the {@code after} + * function to the result. If evaluation of either function throws an exception, it is relayed to the caller of the + * composed function. + * + * @param the type of output of the {@code after} function, and of the composed function. + * @param after the function to apply after this function is applied. + * @return a composed function that first applies this function and then applies the {@code after} function. + */ + default QueryResultConverter andThen(QueryResultConverter after) { + return (row, reader) -> after.mapDocument(row, () -> mapDocument(row, reader)); + } + + /** + * A supplier that converts a {@link Document} into {@code T}. Allows for lazy reading of query results. + * + * @param type of the returned result. + */ + interface ConversionResultSupplier { + + /** + * Obtain the upstream conversion result. + * + * @return the upstream conversion result. + */ + T get(); + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java index 54129e6b5d..883bc65579 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java @@ -18,6 +18,7 @@ import reactor.core.publisher.Flux; import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.lang.Contract; /** * {@link ReactiveAggregationOperation} allows creation and execution of reactive MongoDB aggregation operations in a @@ -44,7 +45,7 @@ public interface ReactiveAggregationOperation { /** * Start creating an aggregation operation that returns results mapped to the given domain type.
* Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to specify a potentially different - * input type for he aggregation. + * input type for the aggregation. * * @param domainType must not be {@literal null}. * @return new instance of {@link ReactiveAggregation}. Never {@literal null}. @@ -73,6 +74,18 @@ interface AggregationOperationWithCollection { */ interface TerminatingAggregationOperation { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingAggregationOperation map(QueryResultConverter converter); + /** * Apply pipeline operations as specified and stream all matching elements.
* diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java index 978aa9634f..fbaff2bc39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupport.java @@ -52,22 +52,25 @@ public ReactiveAggregation aggregateAndReturn(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveAggregationSupport<>(template, domainType, null, null); + return new ReactiveAggregationSupport<>(template, domainType, QueryResultConverter.entity(), null, null); } - static class ReactiveAggregationSupport + static class ReactiveAggregationSupport implements AggregationOperationWithAggregation, ReactiveAggregation, TerminatingAggregationOperation { private final ReactiveMongoTemplate template; - private final Class domainType; + private final Class domainType; + private final QueryResultConverter resultConverter; private final @Nullable Aggregation aggregation; private final @Nullable String collection; - ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, @Nullable Aggregation aggregation, + ReactiveAggregationSupport(ReactiveMongoTemplate template, Class domainType, + QueryResultConverter resultConverter, @Nullable Aggregation aggregation, @Nullable String collection) { this.template = template; this.domainType = domainType; + this.resultConverter = resultConverter; this.aggregation = aggregation; this.collection = collection; } @@ -77,7 +80,7 @@ public AggregationOperationWithAggregation inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection); + return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); } @Override @@ -85,7 +88,16 @@ public TerminatingAggregationOperation by(Aggregation aggregation) { Assert.notNull(aggregation, "Aggregation must not be null"); - return new ReactiveAggregationSupport<>(template, domainType, aggregation, collection); + return new ReactiveAggregationSupport<>(template, domainType, resultConverter, aggregation, collection); + } + + @Override + public TerminatingAggregationOperation map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ReactiveAggregationSupport<>(template, domainType, resultConverter.andThen(converter), aggregation, + collection); } @Override @@ -93,7 +105,7 @@ public Flux all() { Assert.notNull(aggregation, "Aggregation must be set first"); - return template.aggregate(aggregation, getCollectionName(aggregation), domainType); + return template.doAggregate(aggregation, getCollectionName(aggregation), domainType, domainType, resultConverter); } private String getCollectionName(Aggregation aggregation) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java index cba827ffed..24d8c975bb 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java @@ -25,6 +25,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; /** * {@link ReactiveFindOperation} allows creation and execution of reactive MongoDB find operations in a fluent API @@ -66,7 +67,28 @@ public interface ReactiveFindOperation { /** * Compose find execution by calling one of the terminating methods. */ - interface TerminatingFind { + interface TerminatingFind extends TerminatingResults, TerminatingProjection { + + } + + /** + * Compose find execution by calling one of the terminating methods. + * + * @since x.y + */ + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Get exactly zero or one result. @@ -120,6 +142,15 @@ interface TerminatingFind { */ Flux tail(); + } + + /** + * Compose find execution by calling one of the terminating methods. + * + * @since x.y + */ + interface TerminatingProjection { + /** * Get the number of matching elements.
* This method uses an @@ -145,6 +176,18 @@ interface TerminatingFind { */ interface TerminatingFindNear { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingFindNear map(QueryResultConverter converter); + /** * Find all matching elements and return them as {@link org.springframework.data.geo.GeoResult}. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java index 9445dbdadb..38e32dc977 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupport.java @@ -21,8 +21,9 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.data.domain.Window; import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Window; +import org.springframework.data.geo.GeoResult; import org.springframework.data.mongodb.core.CollectionPreparerSupport.ReactiveCollectionPreparerDelegate; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -52,7 +53,7 @@ public ReactiveFind query(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveFindSupport<>(template, domainType, domainType, null, ALL_QUERY); + return new ReactiveFindSupport<>(template, domainType, domainType, QueryResultConverter.entity(), null, ALL_QUERY); } /** @@ -61,21 +62,24 @@ public ReactiveFind query(Class domainType) { * @author Christoph Strobl * @since 2.0 */ - static class ReactiveFindSupport + static class ReactiveFindSupport implements ReactiveFind, FindWithCollection, FindWithProjection, FindWithQuery { private final ReactiveMongoTemplate template; private final Class domainType; - private final Class returnType; + private final Class returnType; + private final QueryResultConverter resultConverter; private final @Nullable String collection; private final Query query; - ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, @Nullable String collection, + ReactiveFindSupport(ReactiveMongoTemplate template, Class domainType, Class returnType, + QueryResultConverter resultConverter, @Nullable String collection, Query query) { this.template = template; this.domainType = domainType; this.returnType = returnType; + this.resultConverter = resultConverter; this.collection = collection; this.query = query; } @@ -85,7 +89,7 @@ public FindWithProjection inCollection(String collection) { Assert.hasText(collection, "Collection name must not be null nor empty"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query); } @Override @@ -93,7 +97,8 @@ public FindWithQuery as(Class returnType) { Assert.notNull(returnType, "ReturnType must not be null"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, QueryResultConverter.entity(), collection, + query); } @Override @@ -101,7 +106,16 @@ public TerminatingFind matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ReactiveFindSupport<>(template, domainType, returnType, collection, query); + return new ReactiveFindSupport<>(template, domainType, returnType, resultConverter, collection, query); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new ReactiveFindSupport<>(template, domainType, returnType, this.resultConverter.andThen(converter), + collection, query); } @Override @@ -141,7 +155,8 @@ public Flux all() { @Override public Mono> scroll(ScrollPosition scrollPosition) { - return template.doScroll(query.with(scrollPosition), domainType, returnType, getCollectionName()); + return template.doScroll(query.with(scrollPosition), domainType, returnType, resultConverter, + getCollectionName()); } @Override @@ -151,7 +166,7 @@ public Flux tail() { @Override public TerminatingFindNear near(NearQuery nearQuery) { - return () -> template.geoNear(nearQuery, domainType, getCollectionName(), returnType); + return new TerminatingFindNearSupport<>(nearQuery, resultConverter); } @Override @@ -178,14 +193,15 @@ private Flux doFind(@Nullable FindPublisherPreparer preparer) { Document fieldsObject = query.getFieldsObject(); return template.doFind(getCollectionName(), ReactiveCollectionPreparerDelegate.of(query), queryObject, - fieldsObject, domainType, returnType, preparer != null ? preparer : getCursorPreparer(query)); + fieldsObject, domainType, returnType, resultConverter, + preparer != null ? preparer : getCursorPreparer(query)); } - @SuppressWarnings("unchecked") + @SuppressWarnings({ "unchecked", "rawtypes" }) private Flux doFindDistinct(String field) { return template.findDistinct(query, field, getCollectionName(), domainType, - returnType == domainType ? (Class) Object.class : returnType); + returnType == domainType ? (Class) Object.class : returnType); } private FindPublisherPreparer getCursorPreparer(Query query) { @@ -200,10 +216,36 @@ private String asString() { return SerializationUtils.serializeToJsonSafely(query); } + class TerminatingFindNearSupport implements TerminatingFindNear { + + private final NearQuery nearQuery; + private final QueryResultConverter resultConverter; + + public TerminatingFindNearSupport(NearQuery nearQuery, + QueryResultConverter resultConverter) { + this.nearQuery = nearQuery; + this.resultConverter = resultConverter; + } + + @Override + public TerminatingFindNear map(QueryResultConverter converter) { + + Assert.notNull(converter, "QueryResultConverter must not be null"); + + return new TerminatingFindNearSupport<>(nearQuery, this.resultConverter.andThen(converter)); + } + + @Override + public Flux> all() { + return template.doGeoNear(nearQuery, domainType, getCollectionName(), returnType, resultConverter); + } + } + /** * @author Christoph Strobl * @since 2.1 */ + @SuppressWarnings({ "unchecked", "rawtypes" }) static class DistinctOperationSupport implements TerminatingDistinct { private final String field; @@ -224,12 +266,11 @@ public TerminatingDistinct as(Class resultType) { } @Override - @SuppressWarnings("unchecked") public TerminatingDistinct matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new DistinctOperationSupport<>((ReactiveFindSupport) delegate.matching(query), field); + return new DistinctOperationSupport<>((ReactiveFindSupport) delegate.matching(query), field); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 325a96dc85..80b842a5ee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -889,10 +889,11 @@ public Mono> scroll(Query query, Class entityType) { @Override public Mono> scroll(Query query, Class entityType, String collectionName) { - return doScroll(query, entityType, entityType, collectionName); + return doScroll(query, entityType, entityType, QueryResultConverter.entity(), collectionName); } - Mono> doScroll(Query query, Class sourceClass, Class targetClass, String collectionName) { + Mono> doScroll(Query query, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(collectionName, "CollectionName must not be null"); @@ -900,7 +901,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC Assert.notNull(targetClass, "Target type must not be null"); EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); - ProjectingReadCallback callback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); int limit = query.isLimited() ? query.getLimit() + 1 : Integer.MAX_VALUE; if (query.hasKeyset()) { @@ -908,7 +909,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC KeysetScrollQuery keysetPaginationQuery = ScrollUtils.createKeysetPaginationQuery(query, operations.getIdPropertyName(sourceClass)); - Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), + Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), keysetPaginationQuery.query(), keysetPaginationQuery.fields(), sourceClass, new QueryFindPublisherPreparer(query, keysetPaginationQuery.sort(), limit, 0, sourceClass), callback) .collectList(); @@ -916,7 +917,7 @@ Mono> doScroll(Query query, Class sourceClass, Class targetC return result.map(it -> ScrollUtils.createWindow(query, it, sourceClass, operations)); } - Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + Mono> result = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), query.getFieldsObject(), sourceClass, new QueryFindPublisherPreparer(query, query.getSortObject(), limit, query.getSkip(), sourceClass), callback) .collectList(); @@ -1015,6 +1016,11 @@ public Flux aggregate(Aggregation aggregation, String collectionName, Cla protected Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, Class outputType) { + return doAggregate(aggregation, collectionName, inputType, outputType, QueryResultConverter.entity()); + } + + Flux doAggregate(Aggregation aggregation, String collectionName, @Nullable Class inputType, + Class outputType, QueryResultConverter resultConverter) { Assert.notNull(aggregation, "Aggregation pipeline must not be null"); Assert.hasText(collectionName, "Collection name must not be null or empty"); @@ -1030,13 +1036,14 @@ protected Flux doAggregate(Aggregation aggregation, String collectionName serializeToJsonSafely(ctx.getAggregationPipeline()), collectionName)); } - ReadDocumentCallback readCallback = new ReadDocumentCallback<>(mongoConverter, outputType, collectionName); + DocumentCallback readCallback = new QueryResultConverterCallback<>(resultConverter, + new ReadDocumentCallback<>(mongoConverter, outputType, collectionName)); return execute(collectionName, collection -> aggregateAndMap(collection, ctx.getAggregationPipeline(), ctx.isOutOrMerge(), options, readCallback, ctx.getInputType())); } private Flux aggregateAndMap(MongoCollection collection, List pipeline, - boolean isOutOrMerge, AggregationOptions options, ReadDocumentCallback readCallback, + boolean isOutOrMerge, AggregationOptions options, DocumentCallback readCallback, @Nullable Class inputType) { ReactiveCollectionPreparerDelegate collectionPreparer = ReactiveCollectionPreparerDelegate.of(options); @@ -1082,9 +1089,14 @@ public Flux> geoNear(NearQuery near, Class entityClass, Stri return geoNear(near, entityClass, collectionName, entityClass); } - @SuppressWarnings("unchecked") protected Flux> geoNear(NearQuery near, Class entityClass, String collectionName, Class returnType) { + return doGeoNear(near, entityClass, collectionName, returnType, QueryResultConverter.entity()); + } + + @SuppressWarnings("unchecked") + Flux> doGeoNear(NearQuery near, Class entityClass, String collectionName, Class returnType, + QueryResultConverter resultConverter) { if (near == null) { throw new InvalidDataAccessApiUsageException("NearQuery must not be null"); @@ -1098,8 +1110,8 @@ protected Flux> geoNear(NearQuery near, Class entityClass, S String distanceField = operations.nearQueryDistanceFieldName(entityClass); EntityProjection projection = operations.introspectProjection(returnType, entityClass); - GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, - new ProjectingReadCallback<>(mongoConverter, projection, collection), near.getMetric()); + GeoNearResultDocumentCallback callback = new GeoNearResultDocumentCallback<>(distanceField, + getResultReader(projection, collectionName, resultConverter), near.getMetric()); Builder optionsBuilder = AggregationOptions.builder(); if (near.hasReadPreference()) { @@ -2428,11 +2440,12 @@ CollectionPreparer> createCollectionPreparer(Query que * * @since 2.0 */ - Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, - Document query, Document fields, Class sourceClass, Class targetClass, FindPublisherPreparer preparer) { + Flux doFind(String collectionName, CollectionPreparer> collectionPreparer, + Document query, Document fields, Class sourceClass, Class targetClass, + QueryResultConverter resultConverter, FindPublisherPreparer preparer) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(sourceClass); - EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); + EntityProjection projection = operations.introspectProjection(targetClass, sourceClass); QueryContext queryContext = queryOperations.createQueryContext(new BasicQuery(query, fields)); Document mappedFields = queryContext.getMappedFields(entity, projection); @@ -2444,7 +2457,7 @@ Flux doFind(String collectionName, CollectionPreparer(mongoConverter, projection, collectionName), collectionName); + getResultReader(projection, collectionName, resultConverter), collectionName); } protected CreateCollectionOptions convertToCreateCollectionOptions(@Nullable CollectionOptions collectionOptions) { @@ -2752,6 +2765,16 @@ private Flux executeFindMultiInternal(ReactiveCollectionQueryCallback DocumentCallback getResultReader(EntityProjection projection, String collectionName, + QueryResultConverter resultConverter) { + + DocumentCallback readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); + + return resultConverter == QueryResultConverter.entity() ? (DocumentCallback) readCallback + : new QueryResultConverterCallback(resultConverter, readCallback); + } + /** * Exception translation {@link Function} intended for {@link Flux#onErrorMap(Function)} usage. * @@ -3111,6 +3134,22 @@ interface ReactiveCollectionQueryCallback extends ReactiveCollectionCallback< FindPublisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException; } + static final class QueryResultConverterCallback implements DocumentCallback { + + private final QueryResultConverter converter; + private final DocumentCallback delegate; + + QueryResultConverterCallback(QueryResultConverter converter, DocumentCallback delegate) { + this.converter = converter; + this.delegate = delegate; + } + + @Override + public Mono doWith(Document object) { + return delegate.doWith(object).map(it -> converter.mapDocument(object, () -> it)); + } + } + /** * Simple {@link DocumentCallback} that will transform {@link Document} into the given target type using the given * {@link EntityReader}. diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java index 05f0695839..80373562c8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableAggregationOperationSupportUnitTests.java @@ -33,6 +33,7 @@ * Unit tests for {@link ExecutableAggregationOperationSupport}. * * @author Christoph Strobl + * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) public class ExecutableAggregationOperationSupportUnitTests { @@ -72,7 +73,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -86,7 +88,8 @@ void aggregateWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -101,7 +104,8 @@ void aggregateWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } @@ -112,7 +116,8 @@ void aggregateStreamWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).stream(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("star-wars"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -126,7 +131,8 @@ void aggregateStreamWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -141,7 +147,8 @@ void aggregateStreamWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregateStream(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregateStream(any(Aggregation.class), eq("person"), captor.capture(), + eq(QueryResultConverter.entity()), any()); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index eac248e69a..3f7e167bd2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -21,7 +21,9 @@ import static org.springframework.data.mongodb.test.util.DirtiesStateExtension.*; import java.util.Date; +import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Stream; import org.bson.BsonString; @@ -170,6 +172,16 @@ void findAllByWithProjection() { .hasOnlyElementsOfType(Jedi.class).hasSize(1); } + @Test // GH- + void findAllByWithConverter() { + + List> result = template.query(Person.class).as(Jedi.class) + .matching(query(where("firstname").is("luke"))).map((document, reader) -> Optional.of(reader.get())).all(); + + assertThat(result).hasOnlyElementsOfType(Optional.class).hasSize(1); + assertThat(result).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(1); + } + @Test // DATAMONGO-1563 void findBy() { assertThat(template.query(Person.class).matching(query(where("firstname").is("luke"))).one()).contains(luke); @@ -260,6 +272,15 @@ void streamAllWithProjection() { } } + @Test // GH- + void streamAllWithConverter() { + + try (Stream> stream = template.query(Person.class).as(Jedi.class) + .map((document, reader) -> Optional.of(reader.get())).stream()) { + assertThat(stream).extracting(Optional::get).hasOnlyElementsOfType(Jedi.class).hasSize(2); + } + } + @Test // DATAMONGO-1733 void streamAllReturningResultsAsClosedInterfaceProjection() { @@ -315,6 +336,20 @@ void findAllNearByWithCollectionAndProjection() { assertThat(results.getContent().get(0).getContent().getId()).isEqualTo("alderan"); } + @Test // GH- + void findAllNearByWithConverter() { + + GeoResults> results = template.query(Object.class).inCollection(STAR_WARS_PLANETS).as(Human.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get())) + .all(); + + assertThat(results.getContent()).hasSize(2); + assertThat(results.getContent().get(0).getDistance()).isNotNull(); + assertThat(results.getContent().get(0).getContent()).isInstanceOf(Optional.class); + assertThat(results.getContent().get(0).getContent().get()).isInstanceOf(Human.class); + assertThat(results.getContent().get(0).getContent().get().getId()).isEqualTo("alderan"); + } + @Test // DATAMONGO-1733 void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index b5892c2ca0..40d86fce55 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -1156,7 +1156,7 @@ void countShouldApplyQueryHintAsIndexNameIfPresent() { void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -1165,7 +1165,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, - PersonProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -1174,7 +1174,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonSpELProjection.class, CursorPreparer.NO_OP_PREPARER); + PersonSpELProjection.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } @@ -1183,7 +1183,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { void appliesFieldsToDtoProjection() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - Jedi.class, CursorPreparer.NO_OP_PREPARER); + Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("firstname", 1))); } @@ -1192,7 +1192,7 @@ void appliesFieldsToDtoProjection() { void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document("bar", 1), Person.class, - Jedi.class, CursorPreparer.NO_OP_PREPARER); + Jedi.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(new Document("bar", 1))); } @@ -1201,7 +1201,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { void doesNotApplyFieldsWhenTargetIsNotAProjection() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - Person.class, CursorPreparer.NO_OP_PREPARER); + Person.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } @@ -1210,7 +1210,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { void doesNotApplyFieldsWhenTargetExtendsDomainType() { template.doFind(CollectionPreparer.identity(), "star-wars", new Document(), new Document(), Person.class, - PersonExtended.class, CursorPreparer.NO_OP_PREPARER); + PersonExtended.class, QueryResultConverter.entity(), CursorPreparer.NO_OP_PREPARER); verify(findIterable).projection(eq(BsonUtils.EMPTY_DOCUMENT)); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java index 9d4ed339b5..83e1b3c272 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveAggregationOperationSupportUnitTests.java @@ -72,7 +72,8 @@ void aggregateWithUntypedAggregationAndExplicitCollection() { opSupport.aggregateAndReturn(Person.class).inCollection("star-wars").by(newAggregation(project("foo"))).all(); ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); - verify(template).aggregate(any(Aggregation.class), eq("star-wars"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("star-wars"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getValue()).isEqualTo(Person.class); } @@ -86,7 +87,8 @@ void aggregateWithUntypedAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Person.class); } @@ -101,7 +103,8 @@ void aggregateWithTypeAggregation() { ArgumentCaptor captor = ArgumentCaptor.forClass(Class.class); verify(template).getCollectionName(captor.capture()); - verify(template).aggregate(any(Aggregation.class), eq("person"), captor.capture()); + verify(template).doAggregate(any(Aggregation.class), eq("person"), captor.capture(), any(Class.class), + eq(QueryResultConverter.entity())); assertThat(captor.getAllValues()).containsExactly(Person.class, Jedi.class); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java index f23e973202..28b77cdfa9 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java @@ -26,6 +26,7 @@ import java.util.Date; import java.util.Objects; +import java.util.Optional; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; @@ -167,6 +168,17 @@ void findAllWithProjection() { .verifyComplete(); } + @Test // GH-… + void findAllWithConverter() { + + template.query(Person.class).as(Jedi.class).map((document, reader) -> Optional.of(reader.get())).all() + .map(Optional::get) // + .map(it -> it.getClass().getName()) // + .as(StepVerifier::create) // + .expectNext(Jedi.class.getName(), Jedi.class.getName()) // + .verifyComplete(); + } + @Test // DATAMONGO-1719 void findAllBy() { @@ -299,6 +311,32 @@ void findAllNearByWithCollectionAndProjection() { .verifyComplete(); } + @Test // GH-… + @DirtiesState + void findAllNearByWithConverter() { + + blocking.indexOps(Planet.class).ensureIndex( + new GeospatialIndex("coordinates").typed(GeoSpatialIndexType.GEO_2DSPHERE).named("planet-coordinate-idx")); + + Planet alderan = new Planet("alderan", new Point(-73.9836, 40.7538)); + Planet dantooine = new Planet("dantooine", new Point(-73.9928, 40.7193)); + + blocking.save(alderan); + blocking.save(dantooine); + + template.query(Object.class).inCollection(STAR_WARS).as(Human.class) + .near(NearQuery.near(-73.9667, 40.78).spherical(true)).map((document, reader) -> Optional.of(reader.get())) // + .all() // + .as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual.getDistance()).isNotNull(); + assertThat(actual.getContent()).isInstanceOf(Optional.class); + assertThat(actual.getContent().get()).isInstanceOf(Human.class); + assertThat(actual.getContent().get().getId()).isEqualTo("alderan"); + }) // + .expectNextCount(1) // + .verifyComplete(); + } + @Test // DATAMONGO-1719 @DirtiesState void findAllNearByReturningGeoResultContentAsClosedInterfaceProjection() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java index 5ba0d947fe..36cf0886ad 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveMongoTemplateUnitTests.java @@ -18,6 +18,7 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import static org.springframework.data.mongodb.core.aggregation.Aggregation.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import reactor.core.publisher.Flux; @@ -54,6 +55,7 @@ import org.mockito.quality.Strictness; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; + import org.springframework.beans.factory.annotation.Value; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; @@ -437,7 +439,7 @@ void geoNearShouldHonorReadConcernFromQuery() { void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -446,7 +448,7 @@ void appliesFieldsWhenInterfaceProjectionIsClosedAndQueryDoesNotDefineFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, - PersonProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -455,7 +457,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsClosedAndQueryDefinesFields() { void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonSpELProjection.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonSpELProjection.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -464,7 +466,7 @@ void doesNotApplyFieldsWhenInterfaceProjectionIsOpen() { void appliesFieldsToDtoProjection() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("firstname", 1))); } @@ -473,7 +475,7 @@ void appliesFieldsToDtoProjection() { void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document("bar", 1), Person.class, - Jedi.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Jedi.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher).projection(eq(new Document("bar", 1))); } @@ -482,7 +484,7 @@ void doesNotApplyFieldsToDtoProjectionWhenQueryDefinesFields() { void doesNotApplyFieldsWhenTargetIsNotAProjection() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - Person.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + Person.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } @@ -491,7 +493,7 @@ void doesNotApplyFieldsWhenTargetIsNotAProjection() { void doesNotApplyFieldsWhenTargetExtendsDomainType() { template.doFind("star-wars", CollectionPreparer.identity(), new Document(), new Document(), Person.class, - PersonExtended.class, FindPublisherPreparer.NO_OP_PREPARER).subscribe(); + PersonExtended.class, QueryResultConverter.entity(), FindPublisherPreparer.NO_OP_PREPARER).subscribe(); verify(findPublisher, never()).projection(any()); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 99579b34a7..95a29fe8ba 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -37,6 +37,7 @@ import java.util.Date; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Scanner; import java.util.stream.Stream; @@ -287,6 +288,60 @@ void shouldAggregateEmptyCollectionAndStream() { } } + @Test // GH- + void shouldAggregateAsStreamWithConverter() { + + MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); + + coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql")); + coll.insertOne(createDocument("Doc2")); + + Aggregation aggregation = newAggregation(// + project("tags"), // + unwind("tags"), // + group("tags") // + .count().as("n"), // + project("n") // + .and("tag").previousOperation(), // + sort(DESC, "n") // + ); + + try (Stream> stream = mongoTemplate.aggregateAndReturn(TagCount.class) + .inCollection(INPUT_COLLECTION).by(aggregation).map((document, reader) -> Optional.of(reader.get())).stream()) { + + List tagCount = stream.flatMap(Optional::stream).toList(); + + assertThat(tagCount).hasSize(3); + } + } + + @Test // GH- + void shouldAggregateWithConverter() { + + MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); + + coll.insertOne(createDocument("Doc1", "spring", "mongodb", "nosql")); + coll.insertOne(createDocument("Doc2")); + + Aggregation aggregation = newAggregation(// + project("tags"), // + unwind("tags"), // + group("tags") // + .count().as("n"), // + project("n") // + .and("tag").previousOperation(), // + sort(DESC, "n") // + ); + + AggregationResults> results = mongoTemplate.aggregateAndReturn(TagCount.class) + .inCollection(INPUT_COLLECTION) // + .by(aggregation) // + .map((document, reader) -> Optional.of(reader.get())) // + .all(); + + assertThat(results.getMappedResults()).extracting(Optional::get).hasOnlyElementsOfType(TagCount.class).hasSize(3); + } + @Test // DATAMONGO-1391 void shouldUnwindWithIndex() { @@ -501,7 +556,7 @@ void findStatesWithPopulationOver10MillionAggregationExample() { /* //complex mongodb aggregation framework example from https://docs.mongodb.org/manual/tutorial/aggregation-examples/#largest-and-smallest-cities-by-state - + db.zipcodes.aggregate( { $group: { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java index 55d6bf3b60..62d13a8f27 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java @@ -22,6 +22,7 @@ import reactor.test.StepVerifier; import java.util.Arrays; +import java.util.Optional; import org.bson.Document; import org.junit.After; @@ -115,6 +116,29 @@ public void shouldProjectMultipleDocuments() { }).verifyComplete(); } + @Test // GH-… + public void shouldProjectAndConvertMultipleDocuments() { + + City dresden = new City("Dresden", 100); + City linz = new City("Linz", 101); + City braunschweig = new City("Braunschweig", 102); + City weinheim = new City("Weinheim", 103); + + reactiveMongoTemplate.insertAll(Arrays.asList(dresden, linz, braunschweig, weinheim)).as(StepVerifier::create) + .expectNextCount(4).verifyComplete(); + + Aggregation agg = newAggregation( // + match(where("population").lt(103))); + + reactiveMongoTemplate.aggregateAndReturn(City.class).inCollection("city").by(agg) + .map((document, reader) -> Optional.of(reader.get())) // + .all() // + .collectList() // + .as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual).hasSize(3).extracting(Optional::get).contains(dresden, linz, braunschweig); + }).verifyComplete(); + } + @Test // DATAMONGO-1646 public void shouldAggregateToOutCollection() { From ed7943a949a413f85a9b8ddd15d6d1b53432ce77 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 17 Apr 2025 12:30:05 +0200 Subject: [PATCH 54/74] Polishing. Original Pull Request: #4949 --- .../modules/ROOT/pages/repositories/core-concepts.adoc | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc index 1a4af7a60b..7d31acb2d4 100644 --- a/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc +++ b/src/main/antora/modules/ROOT/pages/repositories/core-concepts.adoc @@ -2,11 +2,3 @@ include::{commons}@data-commons::page$repositories/core-concepts.adoc[] [[mongodb.entity-persistence.state-detection-strategies]] include::{commons}@data-commons::page$is-new-state-detection.adoc[leveloffset=+1] - -[NOTE] -==== -Cassandra provides no means to generate identifiers upon inserting data. -As consequence, entities must be associated with identifier values. -Spring Data defaults to identifier inspection to determine whether an entity is new. -If you want to use xref:mongodb/auditing.adoc[auditing] make sure to either use xref:mongodb/template-crud-operations.adoc#mongo-template.optimistic-locking[Optimistic Locking] or implement `Persistable` for proper entity state detection. -==== From ec49c14bb999babfeb97fd15493ada7b2e5e2258 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 29 Apr 2025 13:32:25 +0200 Subject: [PATCH 55/74] Support QueryResultConverter for delete, replace and update operations. Original Pull Request: #4949 --- .../core/ExecutableRemoveOperation.java | 39 +++++--- .../ExecutableRemoveOperationSupport.java | 23 +++-- .../core/ExecutableUpdateOperation.java | 27 ++++++ .../ExecutableUpdateOperationSupport.java | 50 ++++++---- .../data/mongodb/core/MongoTemplate.java | 91 ++++++++++++++----- ...ExecutableRemoveOperationSupportTests.java | 9 ++ ...ExecutableUpdateOperationSupportTests.java | 24 +++++ .../mongodb/core/MongoTemplateUnitTests.java | 4 +- .../core/QueryResultConverterUnitTests.java | 89 ++++++++++++++++++ 9 files changed, 295 insertions(+), 61 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index a10cd0317f..9f4a0109e7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -19,6 +19,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.result.DeleteResult; @@ -54,11 +55,36 @@ public interface ExecutableRemoveOperation { */ ExecutableRemove remove(Class domainType); + interface TerminatingResults { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); + + /** + * Remove and return all matching documents.
+ * NOTE: The entire list of documents will be fetched before sending the actual delete commands. + * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete + * operation. + * + * @return empty {@link List} if no match found. Never {@literal null}. + */ + List findAndRemove(); + } + /** * @author Christoph Strobl * @since 2.0 */ - interface TerminatingRemove { + interface TerminatingRemove extends TerminatingResults { /** * Remove all documents matching. @@ -73,16 +99,6 @@ interface TerminatingRemove { * @return the {@link DeleteResult}. Never {@literal null}. */ DeleteResult one(); - - /** - * Remove and return all matching documents.
- * NOTE: The entire list of documents will be fetched before sending the actual delete commands. - * Also, {@link org.springframework.context.ApplicationEvent}s will be published for each and every delete - * operation. - * - * @return empty {@link List} if no match found. Never {@literal null}. - */ - List findAndRemove(); } /** @@ -105,7 +121,6 @@ interface RemoveWithCollection extends RemoveWithQuery { RemoveWithQuery inCollection(String collection); } - /** * @author Christoph Strobl * @since 2.0 diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index e53e80b10f..77cd924e5f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -48,26 +48,28 @@ public ExecutableRemove remove(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null); + return new ExecutableRemoveSupport<>(tempate, domainType, ALL_QUERY, null, QueryResultConverter.entity()); } /** * @author Christoph Strobl * @since 2.0 */ - static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { + static class ExecutableRemoveSupport implements ExecutableRemove, RemoveWithCollection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final String collection; + private final QueryResultConverter resultConverter; - public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, - @Nullable String collection) { + public ExecutableRemoveSupport(MongoTemplate template, Class domainType, Query query, + @Nullable String collection, QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; this.query = query; this.collection = collection; + this.resultConverter = resultConverter; } @Override @@ -76,7 +78,7 @@ public RemoveWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -85,7 +87,7 @@ public TerminatingRemove matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ExecutableRemoveSupport<>(template, domainType, query, collection); + return new ExecutableRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -103,7 +105,12 @@ public List findAndRemove() { String collectionName = getCollectionName(); - return template.doFindAndDelete(collectionName, query, domainType); + return template.doFindAndDelete(collectionName, query, domainType, resultConverter); + } + + @Override + public TerminatingResults map(QueryResultConverter converter) { + return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter); } private String getCollectionName() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index 69365459ba..925a1af80d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -18,6 +18,7 @@ import java.util.Optional; import org.jspecify.annotations.Nullable; +import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingResults; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; @@ -25,6 +26,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; import com.mongodb.client.result.UpdateResult; +import org.springframework.lang.Contract; /** * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace @@ -69,6 +71,19 @@ public interface ExecutableUpdateOperation { */ interface TerminatingFindAndModify { + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingFindAndModify map(QueryResultConverter converter); + /** * Find, modify and return the first matching document. * @@ -130,6 +145,18 @@ default Optional findAndReplace() { */ @Nullable T findAndReplaceValue(); + + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since x.y + */ + @Contract("_ -> new") + TerminatingFindAndReplace mapResult(QueryResultConverter converter); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index 75756c6f1e..56bef3815e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -47,7 +47,7 @@ public ExecutableUpdate update(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); + return new ExecutableUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity()); } /** @@ -55,23 +55,25 @@ public ExecutableUpdate update(Class domainType) { * @since 2.0 */ @SuppressWarnings("rawtypes") - static class ExecutableUpdateSupport + static class ExecutableUpdateSupport implements ExecutableUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, TerminatingFindAndReplace, FindAndReplaceWithProjection { private final MongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; @Nullable private final UpdateDefinition update; @Nullable private final String collection; @Nullable private final FindAndModifyOptions findAndModifyOptions; @Nullable private final FindAndReplaceOptions findAndReplaceOptions; @Nullable private final Object replacement; - private final Class targetType; + private final QueryResultConverter resultConverter; + private final Class targetType; - ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, + ExecutableUpdateSupport(MongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, - @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType) { + @Nullable FindAndReplaceOptions findAndReplaceOptions, @Nullable Object replacement, Class targetType, + QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; @@ -82,6 +84,7 @@ static class ExecutableUpdateSupport this.findAndReplaceOptions = findAndReplaceOptions; this.replacement = replacement; this.targetType = targetType; + this.resultConverter = resultConverter; } @Override @@ -91,7 +94,7 @@ public TerminatingUpdate apply(UpdateDefinition update) { Assert.notNull(update, "Update must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -101,7 +104,7 @@ public UpdateWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -111,7 +114,7 @@ public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -121,7 +124,7 @@ public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -131,7 +134,7 @@ public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options Assert.notNull(options, "Options must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - options, replacement, targetType); + options, replacement, targetType, resultConverter); } @Override @@ -143,7 +146,7 @@ public TerminatingReplace withOptions(ReplaceOptions options) { target.upsert(); } return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - target, replacement, targetType); + target, replacement, targetType, resultConverter); } @Override @@ -153,7 +156,7 @@ public UpdateWithUpdate matching(Query query) { Assert.notNull(query, "Query must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -163,7 +166,7 @@ public FindAndReplaceWithOptions as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, resultType); + findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity()); } @Override @@ -181,13 +184,26 @@ public UpdateResult upsert() { return doUpdate(true, true); } + @Override + public TerminatingFindAndModify map(QueryResultConverter converter) { + + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + } + + @Override + public TerminatingFindAndReplace mapResult(QueryResultConverter converter) { + return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + } + @Override @SuppressWarnings("NullAway") public @Nullable T findAndModifyValue() { return template.findAndModify(query, update, findAndModifyOptions != null ? findAndModifyOptions : new FindAndModifyOptions(), targetType, - getCollectionName()); + getCollectionName(), resultConverter); } @Override @@ -195,8 +211,8 @@ public UpdateResult upsert() { public @Nullable T findAndReplaceValue() { return (T) template.findAndReplace(query, replacement, - findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), domainType, - getCollectionName(), targetType); + findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.empty(), (Class) domainType, + getCollectionName(), targetType, (QueryResultConverter) resultConverter); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index edf8a069a3..5c9f382829 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -122,6 +122,7 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -1143,7 +1144,13 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp @Nullable @Override public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, - Class entityClass, String collectionName) { + Class entityClass, String collectionName) { + return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); + } + + + T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(update, "Update must not be null"); @@ -1163,12 +1170,17 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp } return doFindAndModify(createDelegate(query), collectionName, query.getQueryObject(), query.getFieldsObject(), - getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, resultConverter); } @Override public @Nullable T findAndReplace(Query query, S replacement, FindAndReplaceOptions options, - Class entityType, String collectionName, Class resultType) { + Class entityType, String collectionName, Class resultType) { + return findAndReplace(query, replacement, options, entityType, collectionName, resultType, QueryResultConverter.entity()); + } + + public @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1195,8 +1207,9 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - T saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, - queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection); + + R saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, + queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection, resultConverter); if (saved != null) { maybeEmitEvent(new AfterSaveEvent<>(saved, mappedReplacement, collectionName)); @@ -2187,17 +2200,48 @@ protected UpdateResult replace(Query query, Class entityType, T replac */ @SuppressWarnings("NullAway") protected List doFindAndDelete(String collectionName, Query query, Class entityClass) { + return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity()); + } + + protected List doFindAndDelete(String collectionName, Query query, Class entityClass, QueryResultConverter resultConverter) { + + List ids = new ArrayList<>(); + + - List result = find(query, entityClass, collectionName); +// QueryResultConverter tmpConverter = new QueryResultConverter() { +// @Override +// public S mapDocument(Document document, ConversionResultSupplier reader) { +// ids.add(document.get("_id")); +// return reader.get(); +// } +// }.andThen(resultConverter); + +// DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, tmpConverter); + + QueryResultConverterCallback callback = new QueryResultConverterCallback(resultConverter, new ProjectingReadCallback(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) { + @Override + public Object doWith(Document object) { + ids.add(object.get("_id")); + return super.doWith(object); + } + }; + + List result = doFind(collectionName, createDelegate(query), query.getQueryObject(), query.getFieldsObject(), entityClass, + new QueryCursorPreparer(query, entityClass), callback); if (!CollectionUtils.isEmpty(result)) { - Query byIdInQuery = operations.getByIdInQuery(result); + Criteria[] criterias = ids.stream() // + .map(it -> Criteria.where("_id").is(it)) // + .toArray(Criteria[]::new); + + Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias)); if (query.hasReadPreference()) { - byIdInQuery.withReadPreference(query.getReadPreference()); + removeQuery.withReadPreference(query.getReadPreference()); } - remove(byIdInQuery, entityClass, collectionName); + remove(removeQuery, entityClass, collectionName); } return result; @@ -2819,9 +2863,9 @@ Document getMappedValidator(Validator validator, Class domainType) { } @SuppressWarnings("ConstantConditions") - protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, - Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, - @Nullable FindAndModifyOptions options) { + protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, + Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, + @Nullable FindAndModifyOptions options, QueryResultConverter resultConverter) { if (options == null) { options = new FindAndModifyOptions(); @@ -2843,10 +2887,12 @@ Document getMappedValidator(Validator validator, Class domainType) { serializeToJsonSafely(mappedUpdate), collectionName)); } + DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, resultConverter); + return executeFindOneInternal( new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + callback, collectionName); } /** @@ -2865,15 +2911,15 @@ Document getMappedValidator(Validator validator, Class domainType) { * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, - com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { - EntityProjection projection = operations.introspectProjection(resultType, entityType); + EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, collation, - entityType, replacement, options, projection); + entityType, replacement, options, projection, QueryResultConverter.entity()); } CollectionPreparerDelegate createDelegate(Query query) { @@ -2908,10 +2954,10 @@ CollectionPreparer> createCollectionPreparer(Query que * @since 3.4 */ @Nullable - private T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + private R doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, - com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, - FindAndReplaceOptions options, EntityProjection projection) { + com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, + FindAndReplaceOptions options, EntityProjection projection, QueryResultConverter resultConverter) { if (LOGGER.isDebugEnabled()) { LOGGER @@ -2922,8 +2968,9 @@ private T doFindAndReplace(CollectionPreparer collectionPreparer, String co serializeToJsonSafely(mappedSort), entityType, serializeToJsonSafely(replacement), collectionName)); } + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, mappedSort, - replacement, collation, options), new ProjectingReadCallback<>(mongoConverter, projection, collectionName), + replacement, collation, options),callback, collectionName); } @@ -3421,7 +3468,7 @@ public T doWith(Document document) { } } - static final class QueryResultConverterCallback implements DocumentCallback { + static class QueryResultConverterCallback implements DocumentCallback { private final QueryResultConverter converter; private final DocumentCallback delegate; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java index 621e2a0764..54a4773005 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +109,14 @@ void removeAndReturnAllMatching() { assertThat(result).containsExactly(han); } + @Test // GH-0 + void removeAndReturnAllMatchingWithResultConverter() { + + List> result = template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, converted) -> Optional.of(converted.get())).findAndRemove(); + + assertThat(result).containsExactly(Optional.of(han)); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index e7f50dab53..86c4693c30 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -185,6 +185,17 @@ void findAndModifyWithOptions() { assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } + @Test // GH- + void findAndModifyWithResultConverter() { + + Optional result = template.update(Person.class).matching(queryHan()) + .apply(new Update().set("firstname", "Han")).withOptions(FindAndModifyOptions.options().returnNew(true)) + .map((raw, converted) -> Optional.of(converted.get())) + .findAndModifyValue(); + + assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); + } + @Test // DATAMONGO-1563 void upsert() { @@ -282,6 +293,19 @@ void findAndReplaceWithProjection() { assertThat(result.getName()).isEqualTo(han.firstname); } + @Test // GH- + void findAndReplaceWithResultConverter() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + Optional result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) // + .mapResult((raw, converted) -> Optional.of(converted.get())) + .findAndReplaceValue(); + + assertThat(result.get()).isInstanceOf(Jedi.class).extracting(Jedi::getName).isEqualTo(han.firstname); + } + private Query queryHan() { return query(where("id").is(han.getId())); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java index 40d86fce55..ef72548fac 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoTemplateUnitTests.java @@ -437,8 +437,8 @@ void findAllAndRemoveShouldRemoveDocumentsReturedByFindQuery() { verify(collection, times(1)).deleteMany(queryCaptor.capture(), any()); - Document idField = DocumentTestUtils.getAsDocument(queryCaptor.getValue(), "_id"); - assertThat((List) idField.get("$in")).containsExactly(Integer.valueOf(0), Integer.valueOf(1)); + List ors = DocumentTestUtils.getAsDBList(queryCaptor.getValue(), "$or"); + assertThat(ors).containsExactlyInAnyOrder(new Document("_id", 0), new Document("_id", 1)); } @Test // DATAMONGO-566 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java new file mode 100644 index 0000000000..c94a9fab0c --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java @@ -0,0 +1,89 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.core; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.bson.Document; +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.core.QueryResultConverter.ConversionResultSupplier; + +/** + * @author Christoph Strobl + */ +class QueryResultConverterUnitTests { + + public static final ConversionResultSupplier ERROR_SUPPLIER = () -> { + throw new IllegalStateException("must not read conversion result"); + }; + + @Test // GH- + void converterDoesNotEagerlyRetrieveConversionResultFromSupplier() { + + QueryResultConverter converter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return "done"; + } + }; + + assertThat(converter.mapDocument(new Document(), ERROR_SUPPLIER)).isEqualTo("done"); + } + + @Test // GH- + void converterPassesOnConversionResultToNextStage() { + + Document source = new Document("value", "10"); + + QueryResultConverter stagedConverter = new QueryResultConverter() { + + @Override + public String mapDocument(Document document, ConversionResultSupplier reader) { + return document.get("value", "-1"); + } + }.andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(reader.get()); + } + }); + + assertThat(stagedConverter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } + + @Test // GH- + void entityConverterDelaysConversion() { + + Document source = new Document("value", "10"); + + QueryResultConverter converter = QueryResultConverter. entity() + .andThen(new QueryResultConverter() { + + @Override + public Integer mapDocument(Document document, ConversionResultSupplier reader) { + + assertThat(document).isEqualTo(source); + return Integer.valueOf(document.get("value", "20")); + } + }); + + assertThat(converter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); + } +} From 51d64c37682ba80f784d713472f586735bd29594 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 09:06:49 +0200 Subject: [PATCH 56/74] Polishing. Fix generics. Update since tags. Add GH ticket references. Use literal null instead of code null in javadoc. Original Pull Request: #4949 --- .../core/ExecutableAggregationOperation.java | 2 +- .../mongodb/core/ExecutableFindOperation.java | 10 +-- .../core/ExecutableRemoveOperation.java | 2 +- .../core/ExecutableUpdateOperation.java | 12 +-- .../ExecutableUpdateOperationSupport.java | 11 +-- .../data/mongodb/core/MongoOperations.java | 4 +- .../data/mongodb/core/MongoTemplate.java | 83 +++++-------------- .../mongodb/core/QueryResultConverter.java | 2 +- .../core/ReactiveAggregationOperation.java | 2 +- .../mongodb/core/ReactiveFindOperation.java | 10 +-- .../mongodb/core/ReactiveMongoOperations.java | 4 +- .../mongodb/core/ReactiveMongoTemplate.java | 18 +--- .../data/mongodb/core/geo/GeoJsonModule.java | 8 +- .../MongoPersistentEntityIndexResolver.java | 4 +- .../repository/query/MongoQueryMethod.java | 6 +- .../ExecutableFindOperationSupportTests.java | 6 +- ...ExecutableRemoveOperationSupportTests.java | 2 +- ...ExecutableUpdateOperationSupportTests.java | 4 +- .../core/QueryResultConverterUnitTests.java | 11 ++- .../ReactiveFindOperationSupportTests.java | 2 +- .../core/aggregation/AggregationTests.java | 4 +- .../aggregation/ReactiveAggregationTests.java | 2 +- 22 files changed, 77 insertions(+), 132 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java index e4becc491a..57813a75ba 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableAggregationOperation.java @@ -89,7 +89,7 @@ interface TerminatingAggregation { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingAggregation}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingAggregation map(QueryResultConverter converter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java index 2fff730ad4..43c0d521c3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableFindOperation.java @@ -84,7 +84,7 @@ interface TerminatingFind extends TerminatingResults, TerminatingProjectio * * @author Christoph Strobl * @author Mark Paluch - * @since x.y + * @since 5.0 */ interface TerminatingResults { @@ -95,7 +95,7 @@ interface TerminatingResults { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingResults}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingResults map(QueryResultConverter converter); @@ -157,7 +157,7 @@ default Optional first() { *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct - * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators. + * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators. * * @param scrollPosition the scroll position. * @return a window of the resulting elements. @@ -173,7 +173,7 @@ default Optional first() { * Trigger find execution by calling one of the terminating methods. * * @author Christoph Strobl - * @since x.y + * @since 5.0 */ interface TerminatingProjection { @@ -214,7 +214,7 @@ interface TerminatingFindNear { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingFindNear}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingFindNear map(QueryResultConverter converter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index 9f4a0109e7..de591a0fa8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -64,7 +64,7 @@ interface TerminatingResults { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link ExecutableFindOperation.TerminatingResults}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingResults map(QueryResultConverter converter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java index 925a1af80d..e671b7b7ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperation.java @@ -18,15 +18,15 @@ import java.util.Optional; import org.jspecify.annotations.Nullable; -import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingResults; + import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.lang.Contract; import com.mongodb.client.result.UpdateResult; -import org.springframework.lang.Contract; /** * {@link ExecutableUpdateOperation} allows creation and execution of MongoDB update / findAndModify / findAndReplace @@ -71,7 +71,6 @@ public interface ExecutableUpdateOperation { */ interface TerminatingFindAndModify { - /** * Map the query result to a different type using {@link QueryResultConverter}. * @@ -79,7 +78,7 @@ interface TerminatingFindAndModify { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingFindAndModify}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingFindAndModify map(QueryResultConverter converter); @@ -153,10 +152,11 @@ default Optional findAndReplace() { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingFindAndModify}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") - TerminatingFindAndReplace mapResult(QueryResultConverter converter); + TerminatingFindAndReplace map(QueryResultConverter converter); + } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java index 56bef3815e..dc9ce5cacc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupport.java @@ -185,16 +185,9 @@ public UpdateResult upsert() { } @Override - public TerminatingFindAndModify map(QueryResultConverter converter) { - - return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); - } - - @Override - public TerminatingFindAndReplace mapResult(QueryResultConverter converter) { + public ExecutableUpdateSupport map(QueryResultConverter converter) { return new ExecutableUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 7fdd157528..6753f31c1a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -822,7 +822,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. @@ -847,7 +847,7 @@ MapReduceResults mapReduce(Query query, String inputCollectionName, Strin *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 5c9f382829..e5c7f95102 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -15,22 +15,12 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; +import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import java.io.IOException; import java.math.BigDecimal; import java.math.RoundingMode; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Scanner; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.function.BiPredicate; import java.util.stream.Collectors; @@ -40,8 +30,8 @@ import org.apache.commons.logging.LogFactory; import org.bson.Document; import org.bson.conversions.Bson; - import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -106,18 +96,7 @@ 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.mongodb.core.mapping.event.AfterConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback; -import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; +import org.springframework.data.mongodb.core.mapping.event.*; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.mapreduce.MapReduceResults; import org.springframework.data.mongodb.core.query.BasicQuery; @@ -157,21 +136,7 @@ import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.CreateViewOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.EstimatedDocumentCountOptions; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndReplaceOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.ReturnDocument; -import com.mongodb.client.model.TimeSeriesGranularity; -import com.mongodb.client.model.TimeSeriesOptions; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.ValidationAction; -import com.mongodb.client.model.ValidationLevel; -import com.mongodb.client.model.ValidationOptions; +import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; @@ -1148,7 +1113,6 @@ public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOp return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); } - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName, QueryResultConverter resultConverter) { @@ -1179,7 +1143,7 @@ T findAndModify(Query query, UpdateDefinition update, FindAndModifyOption return findAndReplace(query, replacement, options, entityType, collectionName, resultType, QueryResultConverter.entity()); } - public @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + @Nullable R findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); @@ -1207,7 +1171,6 @@ T findAndModify(Query query, UpdateDefinition update, FindAndModifyOption maybeEmitEvent(new BeforeSaveEvent<>(replacement, mappedReplacement, collectionName)); maybeCallBeforeSave(replacement, mappedReplacement, collectionName); - R saved = doFindAndReplace(collectionPreparer, collectionName, mappedQuery, mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, mappedReplacement, options, projection, resultConverter); @@ -2203,25 +2166,15 @@ protected List doFindAndDelete(String collectionName, Query query, Class< return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity()); } - protected List doFindAndDelete(String collectionName, Query query, Class entityClass, QueryResultConverter resultConverter) { + List doFindAndDelete(String collectionName, Query query, Class entityClass, + QueryResultConverter resultConverter) { List ids = new ArrayList<>(); - - -// QueryResultConverter tmpConverter = new QueryResultConverter() { -// @Override -// public S mapDocument(Document document, ConversionResultSupplier reader) { -// ids.add(document.get("_id")); -// return reader.get(); -// } -// }.andThen(resultConverter); - -// DocumentCallback callback = getResultReader(EntityProjection.nonProjecting(entityClass), collectionName, tmpConverter); - - QueryResultConverterCallback callback = new QueryResultConverterCallback(resultConverter, new ProjectingReadCallback(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) { + QueryResultConverterCallback callback = new QueryResultConverterCallback<>(resultConverter, + new ProjectingReadCallback<>(getConverter(), EntityProjection.nonProjecting(entityClass), collectionName)) { @Override - public Object doWith(Document object) { + public T doWith(Document object) { ids.add(object.get("_id")); return super.doWith(object); } @@ -2366,7 +2319,7 @@ protected Stream aggregateStream(Aggregation aggregation, String collecti } @SuppressWarnings({ "ConstantConditions", "NullAway" }) - protected Stream doAggregateStream(Aggregation aggregation, String collectionName, Class outputType, + Stream doAggregateStream(Aggregation aggregation, String collectionName, Class outputType, QueryResultConverter resultConverter, @Nullable AggregationOperationContext context) { @@ -2412,7 +2365,8 @@ protected Stream doAggregateStream(Aggregation aggregation, String col cursor = cursor.maxTime(options.getMaxTime().toMillis(), TimeUnit.MILLISECONDS); } - Class domainType = aggregation instanceof TypedAggregation typedAggregation ? typedAggregation.getInputType() + Class domainType = aggregation instanceof TypedAggregation typedAggregation + ? typedAggregation.getInputType() : null; Optionals.firstNonEmpty(options::getCollation, // @@ -2863,7 +2817,8 @@ Document getMappedValidator(Validator validator, Class domainType) { } @SuppressWarnings("ConstantConditions") - protected @Nullable T doFindAndModify(CollectionPreparer collectionPreparer, String collectionName, + @Nullable T doFindAndModify(CollectionPreparer> collectionPreparer, + String collectionName, Document query, @Nullable Document fields, @Nullable Document sort, Class entityClass, UpdateDefinition update, @Nullable FindAndModifyOptions options, QueryResultConverter resultConverter) { @@ -2911,7 +2866,8 @@ Document getMappedValidator(Validator validator, Class domainType) { * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. */ @Nullable - protected T doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + protected T doFindAndReplace(CollectionPreparer> collectionPreparer, + String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, Class resultType) { @@ -2954,7 +2910,8 @@ CollectionPreparer> createCollectionPreparer(Query que * @since 3.4 */ @Nullable - private R doFindAndReplace(CollectionPreparer collectionPreparer, String collectionName, + private R doFindAndReplace(CollectionPreparer> collectionPreparer, + String collectionName, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.@Nullable Collation collation, Class entityType, Document replacement, FindAndReplaceOptions options, EntityProjection projection, QueryResultConverter resultConverter) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java index e271ee23cc..ca93940a9c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/QueryResultConverter.java @@ -28,7 +28,7 @@ * @param object type accepted by this converter. * @param the returned result type. * @author Mark Paluch - * @since x.x + * @since 5.0 */ @FunctionalInterface public interface QueryResultConverter { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java index 883bc65579..99c94b19e4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveAggregationOperation.java @@ -81,7 +81,7 @@ interface TerminatingAggregationOperation { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingAggregationOperation map(QueryResultConverter converter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java index 24d8c975bb..eaa9da4a37 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveFindOperation.java @@ -74,7 +74,7 @@ interface TerminatingFind extends TerminatingResults, TerminatingProjectio /** * Compose find execution by calling one of the terminating methods. * - * @since x.y + * @since 5.0 */ interface TerminatingResults { @@ -85,7 +85,7 @@ interface TerminatingResults { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link TerminatingResults}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingResults map(QueryResultConverter converter); @@ -117,7 +117,7 @@ interface TerminatingResults { *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable * {@link org.springframework.data.domain.Sort sort properties} as MongoDB does not support criteria to reconstruct - * a query result from absent document fields or {@code null} values through {@code $gt/$lt} operators. + * a query result from absent document fields or {@literal null} values through {@code $gt/$lt} operators. * * @param scrollPosition the scroll position. * @return a scroll of the resulting elements. @@ -147,7 +147,7 @@ interface TerminatingResults { /** * Compose find execution by calling one of the terminating methods. * - * @since x.y + * @since 5.0 */ interface TerminatingProjection { @@ -183,7 +183,7 @@ interface TerminatingFindNear { * @param converter the converter, must not be {@literal null}. * @return new instance of {@link ExecutableFindOperation.TerminatingFindNear}. * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. - * @since x.y + * @since 5.0 */ @Contract("_ -> new") TerminatingFindNear map(QueryResultConverter converter); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index ebec41e3aa..14f6ee2631 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -513,7 +513,7 @@ Mono> createView(String name, String source, Aggregati *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. @@ -538,7 +538,7 @@ Mono> createView(String name, String source, Aggregati *

* When using {@link KeysetScrollPosition}, make sure to use non-nullable {@link org.springframework.data.domain.Sort * sort properties} as MongoDB does not support criteria to reconstruct a query result from absent document fields or - * {@code null} values through {@code $gt/$lt} operators. + * {@literal null} values through {@code $gt/$lt} operators. * * @param query the query class that specifies the criteria used to find a document and also an optional fields * specification. Must not be {@literal null}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 80b842a5ee..eb372c6b90 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -15,7 +15,7 @@ */ package org.springframework.data.mongodb.core; -import static org.springframework.data.mongodb.core.query.SerializationUtils.serializeToJsonSafely; +import static org.springframework.data.mongodb.core.query.SerializationUtils.*; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -47,6 +47,7 @@ import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -109,18 +110,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes; -import org.springframework.data.mongodb.core.mapping.event.AfterConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterLoadEvent; -import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeConvertEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeDeleteEvent; -import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent; -import org.springframework.data.mongodb.core.mapping.event.MongoMappingEvent; -import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback; -import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback; -import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback; +import org.springframework.data.mongodb.core.mapping.event.*; import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; @@ -2772,7 +2762,7 @@ private DocumentCallback getResultReader(EntityProjection projec DocumentCallback readCallback = new ProjectingReadCallback<>(mongoConverter, projection, collectionName); return resultConverter == QueryResultConverter.entity() ? (DocumentCallback) readCallback - : new QueryResultConverterCallback(resultConverter, readCallback); + : new QueryResultConverterCallback<>(resultConverter, readCallback); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java index cfae6761a8..5c458329ab 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/GeoJsonModule.java @@ -19,7 +19,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; + import org.jspecify.annotations.Nullable; + import org.springframework.data.geo.Point; import com.fasterxml.jackson.core.JsonParser; @@ -163,7 +165,7 @@ private static abstract class GeoJsonDeserializer> extends * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. * * @param node can be {@literal null}. - * @return {@literal null} when given a {@code null} value. + * @return {@literal null} when given a {@literal null} value. */ protected @Nullable GeoJsonPoint toGeoJsonPoint(@Nullable ArrayNode node) { @@ -179,7 +181,7 @@ private static abstract class GeoJsonDeserializer> extends * {@literal x - coordinate} and {@code node.[1]} is {@literal y}. * * @param node can be {@literal null}. - * @return {@literal null} when given a {@code null} value. + * @return {@literal null} when given a {@literal null} value. */ protected @Nullable Point toPoint(@Nullable ArrayNode node) { @@ -194,7 +196,7 @@ private static abstract class GeoJsonDeserializer> extends * Get the points nested within given {@link ArrayNode}. * * @param node can be {@literal null}. - * @return {@literal empty list} when given a {@code null} value. + * @return {@literal empty list} when given a {@literal null} value. */ protected List toPoints(@Nullable ArrayNode node) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java index 3f63009ae2..b7beaaa3e1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java @@ -110,7 +110,7 @@ public Iterable resolveIndexFor(TypeInformation * {@link GeospatialIndex}. The given {@literal root} has therefore to be annotated with {@link Document}. * * @param root must not be null. - * @return List of {@link IndexDefinitionHolder}. Will never be {@code null}. + * @return List of {@link IndexDefinitionHolder}. Will never be {@literal null}. * @throws IllegalArgumentException in case of missing {@link Document} annotation marking root entities. */ public List resolveIndexForEntity(MongoPersistentEntity root) { @@ -189,7 +189,7 @@ private void potentiallyAddIndexForProperty(MongoPersistentEntity root, Mongo * @param collection * @param guard * @return List of {@link IndexDefinitionHolder} representing indexes for given type and its referenced property - * types. Will never be {@code null}. + * types. Will never be {@literal null}. */ private List resolveIndexForClass(TypeInformation type, String dotPath, Path path, String collection, CycleGuard guard) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 52c5e32555..6c90746fe7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -191,7 +191,7 @@ public boolean isGeoNearQuery() { } /** - * Returns the {@link Query} annotation that is applied to the method or {@code null} if none available. + * Returns the {@link Query} annotation that is applied to the method or {@literal null} if none available. * * @return */ @@ -217,7 +217,7 @@ public boolean hasQueryMetaAttributes() { } /** - * Returns the {@link Meta} annotation that is applied to the method or {@code null} if not available. + * Returns the {@link Meta} annotation that is applied to the method or {@literal null} if not available. * * @return * @since 1.6 @@ -228,7 +228,7 @@ Meta getMetaAnnotation() { } /** - * Returns the {@link Tailable} annotation that is applied to the method or {@code null} if not available. + * Returns the {@link Tailable} annotation that is applied to the method or {@literal null} if not available. * * @return * @since 2.0 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java index 3f7e167bd2..835367990a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableFindOperationSupportTests.java @@ -172,7 +172,7 @@ void findAllByWithProjection() { .hasOnlyElementsOfType(Jedi.class).hasSize(1); } - @Test // GH- + @Test // GH-4949 void findAllByWithConverter() { List> result = template.query(Person.class).as(Jedi.class) @@ -272,7 +272,7 @@ void streamAllWithProjection() { } } - @Test // GH- + @Test // GH-4949 void streamAllWithConverter() { try (Stream> stream = template.query(Person.class).as(Jedi.class) @@ -336,7 +336,7 @@ void findAllNearByWithCollectionAndProjection() { assertThat(results.getContent().get(0).getContent().getId()).isEqualTo("alderan"); } - @Test // GH- + @Test // GH-4949 void findAllNearByWithConverter() { GeoResults> results = template.query(Object.class).inCollection(STAR_WARS_PLANETS).as(Human.class) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java index 54a4773005..fe19672068 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupportTests.java @@ -109,7 +109,7 @@ void removeAndReturnAllMatching() { assertThat(result).containsExactly(han); } - @Test // GH-0 + @Test // GH-4949 void removeAndReturnAllMatchingWithResultConverter() { List> result = template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, converted) -> Optional.of(converted.get())).findAndRemove(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index 86c4693c30..3e7fa6c2bc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -293,14 +293,14 @@ void findAndReplaceWithProjection() { assertThat(result.getName()).isEqualTo(han.firstname); } - @Test // GH- + @Test // GH-4949 void findAndReplaceWithResultConverter() { Person luke = new Person(); luke.firstname = "Luke"; Optional result = template.update(Person.class).matching(queryHan()).replaceWith(luke).as(Jedi.class) // - .mapResult((raw, converted) -> Optional.of(converted.get())) + .map((raw, converted) -> Optional.of(converted.get())) .findAndReplaceValue(); assertThat(result.get()).isInstanceOf(Jedi.class).extracting(Jedi::getName).isEqualTo(han.firstname); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java index c94a9fab0c..107b942161 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/QueryResultConverterUnitTests.java @@ -15,13 +15,16 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import org.bson.Document; import org.junit.jupiter.api.Test; + import org.springframework.data.mongodb.core.QueryResultConverter.ConversionResultSupplier; /** + * Unit tests for {@link QueryResultConverter}. + * * @author Christoph Strobl */ class QueryResultConverterUnitTests { @@ -30,7 +33,7 @@ class QueryResultConverterUnitTests { throw new IllegalStateException("must not read conversion result"); }; - @Test // GH- + @Test // GH-4949 void converterDoesNotEagerlyRetrieveConversionResultFromSupplier() { QueryResultConverter converter = new QueryResultConverter() { @@ -44,7 +47,7 @@ public String mapDocument(Document document, ConversionResultSupplier assertThat(converter.mapDocument(new Document(), ERROR_SUPPLIER)).isEqualTo("done"); } - @Test // GH- + @Test // GH-4949 void converterPassesOnConversionResultToNextStage() { Document source = new Document("value", "10"); @@ -68,7 +71,7 @@ public Integer mapDocument(Document document, ConversionResultSupplier r assertThat(stagedConverter.mapDocument(source, ERROR_SUPPLIER)).isEqualTo(10); } - @Test // GH- + @Test // GH-4949 void entityConverterDelaysConversion() { Document source = new Document("value", "10"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java index 28b77cdfa9..7f015874d0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java @@ -168,7 +168,7 @@ void findAllWithProjection() { .verifyComplete(); } - @Test // GH-… + @Test // GH-4949 void findAllWithConverter() { template.query(Person.class).as(Jedi.class).map((document, reader) -> Optional.of(reader.get())).all() diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java index 95a29fe8ba..1495dec1c6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/AggregationTests.java @@ -288,7 +288,7 @@ void shouldAggregateEmptyCollectionAndStream() { } } - @Test // GH- + @Test // GH-4949 void shouldAggregateAsStreamWithConverter() { MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); @@ -315,7 +315,7 @@ void shouldAggregateAsStreamWithConverter() { } } - @Test // GH- + @Test // GH-4949 void shouldAggregateWithConverter() { MongoCollection coll = mongoTemplate.getCollection(INPUT_COLLECTION); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java index 62d13a8f27..92c1d2cd97 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/ReactiveAggregationTests.java @@ -116,7 +116,7 @@ public void shouldProjectMultipleDocuments() { }).verifyComplete(); } - @Test // GH-… + @Test // GH-4949 public void shouldProjectAndConvertMultipleDocuments() { City dresden = new City("Dresden", 100); From 50aca14d1fad0accdc854494e5bcba02b4687cc6 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 9 May 2025 10:39:37 +0200 Subject: [PATCH 57/74] Support QueryResultConverter for reactive delete, replace and update operations. Original Pull Request: #4949 --- .../core/ExecutableRemoveOperation.java | 4 + .../ExecutableRemoveOperationSupport.java | 1 + .../mongodb/core/ReactiveMongoTemplate.java | 90 +++++++++++++++---- .../mongodb/core/ReactiveRemoveOperation.java | 31 +++++-- .../core/ReactiveRemoveOperationSupport.java | 25 ++++-- .../mongodb/core/ReactiveUpdateOperation.java | 25 ++++++ .../core/ReactiveUpdateOperationSupport.java | 40 +++++---- .../ReactiveRemoveOperationSupportTests.java | 16 +++- .../ReactiveUpdateOperationSupportTests.java | 32 ++++++- .../ROOT/pages/mongodb/template-api.adoc | 13 +++ 10 files changed, 225 insertions(+), 52 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java index de591a0fa8..c29a448f1c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperation.java @@ -55,6 +55,10 @@ public interface ExecutableRemoveOperation { */ ExecutableRemove remove(Class domainType); + /** + * @author Christoph Strobl + * @since 5.0 + */ interface TerminatingResults { /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java index 77cd924e5f..7817a7c8af 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ExecutableRemoveOperationSupport.java @@ -109,6 +109,7 @@ public List findAndRemove() { } @Override + @SuppressWarnings({"unchecked", "rawtypes"}) public TerminatingResults map(QueryResultConverter converter) { return new ExecutableRemoveSupport<>(template, (Class) domainType, query, collection, converter); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index eb372c6b90..663264f326 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -47,7 +47,6 @@ import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; - import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -114,6 +113,7 @@ import org.springframework.data.mongodb.core.mapreduce.MapReduceOptions; import org.springframework.data.mongodb.core.query.BasicQuery; import org.springframework.data.mongodb.core.query.Collation; +import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Meta; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; @@ -1137,9 +1137,8 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } - @Override - public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, - Class entityClass, String collectionName) { + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(options, "Options must not be null "); Assert.notNull(entityClass, "Entity class must not be null"); @@ -1156,13 +1155,27 @@ public Mono findAndModify(Query query, UpdateDefinition update, FindAndMo } return doFindAndModify(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), - query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse); + query.getFieldsObject(), getMappedSortObject(query, entityClass), entityClass, update, optionsToUse, + resultConverter); + } + + @Override + public Mono findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + Class entityClass, String collectionName) { + return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); } @Override - @SuppressWarnings("NullAway") public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, Class entityType, String collectionName, Class resultType) { + return findAndReplace(query, replacement, options, entityType, collectionName, resultType, + QueryResultConverter.entity()); + } + + @SuppressWarnings("NullAway") + public Mono findAndReplace(Query query, S replacement, FindAndReplaceOptions options, + Class entityType, String collectionName, Class resultType, + QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); Assert.notNull(replacement, "Replacement must not be null"); @@ -1199,9 +1212,9 @@ public Mono findAndReplace(Query query, S replacement, FindAndReplaceO mapped.getCollection())); }).flatMap(it -> { - Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, + Mono afterFindAndReplace = doFindAndReplace(it.getCollection(), collectionPreparer, mappedQuery, mappedFields, mappedSort, queryContext.getCollation(entityType).orElse(null), entityType, it.getTarget(), - options, projection); + options, projection, resultConverter); return afterFindAndReplace.flatMap(saved -> { maybeEmitEvent(new AfterSaveEvent<>(saved, it.getTarget(), it.getCollection())); return maybeCallAfterSave(saved, it.getTarget(), it.getCollection()); @@ -2280,6 +2293,43 @@ protected Flux doFindAndDelete(String collectionName, Query query, Class< .flatMapSequential(deleteResult -> Flux.fromIterable(list))); } + Flux doFindAndDelete(String collectionName, Query query, Class entityClass, + QueryResultConverter resultConverter) { + + List ids = new ArrayList<>(); + ProjectingReadCallback readCallback = new ProjectingReadCallback(getConverter(), + EntityProjection.nonProjecting(entityClass), collectionName); + + QueryResultConverterCallback callback = new QueryResultConverterCallback<>(resultConverter, readCallback) { + + @Override + public Mono doWith(Document object) { + ids.add(object.get("_id")); + return super.doWith(object); + } + }; + + Flux flux = doFind(collectionName, ReactiveCollectionPreparerDelegate.of(query), query.getQueryObject(), + query.getFieldsObject(), entityClass, + new QueryFindPublisherPreparer(query, query.getSortObject(), query.getLimit(), query.getSkip(), entityClass), + callback); + + return Flux.from(flux).collectList().filter(it -> !it.isEmpty()).flatMapMany(list -> { + + Criteria[] criterias = ids.stream() // + .map(it -> Criteria.where("_id").is(it)) // + .toArray(Criteria[]::new); + + Query removeQuery = new Query(criterias.length == 1 ? criterias[0] : new Criteria().orOperator(criterias)); + if (query.hasReadPreference()) { + removeQuery.withReadPreference(query.getReadPreference()); + } + + return Flux.from(remove(removeQuery, entityClass, collectionName)) + .flatMapSequential(deleteResult -> Flux.fromIterable(list)); + }); + } + /** * Create the specified collection using the provided options * @@ -2487,9 +2537,10 @@ protected Mono doFindAndRemove(String collectionName, new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); } - protected Mono doFindAndModify(String collectionName, + Mono doFindAndModify(String collectionName, CollectionPreparer> collectionPreparer, Document query, Document fields, - @Nullable Document sort, Class entityClass, UpdateDefinition update, FindAndModifyOptions options) { + @Nullable Document sort, Class entityClass, UpdateDefinition update, FindAndModifyOptions options, + QueryResultConverter resultConverter) { MongoPersistentEntity entity = mappingContext.getPersistentEntity(entityClass); UpdateContext updateContext = queryOperations.updateSingleContext(update, query, false); @@ -2508,10 +2559,13 @@ protected Mono doFindAndModify(String collectionName, serializeToJsonSafely(mappedUpdate), collectionName)); } + EntityProjection projection = EntityProjection.nonProjecting(entityClass); + DocumentCallback callback = getResultReader(projection, collectionName, resultConverter); + return executeFindOneInternal( new FindAndModifyCallback(collectionPreparer, mappedQuery, fields, sort, mappedUpdate, update.getArrayFilters().stream().map(ArrayFilter::asDocument).collect(Collectors.toList()), options), - new ReadDocumentCallback<>(this.mongoConverter, entityClass, collectionName), collectionName); + callback, collectionName); }); } @@ -2540,7 +2594,7 @@ protected Mono doFindAndReplace(String collectionName, EntityProjection projection = operations.introspectProjection(resultType, entityType); return doFindAndReplace(collectionName, collectionPreparer, mappedQuery, mappedFields, mappedSort, collation, - entityType, replacement, options, projection); + entityType, replacement, options, projection, QueryResultConverter.entity()); } /** @@ -2560,10 +2614,11 @@ protected Mono doFindAndReplace(String collectionName, * {@literal false} and {@link FindAndReplaceOptions#isUpsert() upsert} is {@literal false}. * @since 3.4 */ - private Mono doFindAndReplace(String collectionName, + private Mono doFindAndReplace(String collectionName, CollectionPreparer> collectionPreparer, Document mappedQuery, Document mappedFields, Document mappedSort, com.mongodb.client.model.Collation collation, Class entityType, Document replacement, - FindAndReplaceOptions options, EntityProjection projection) { + FindAndReplaceOptions options, EntityProjection projection, + QueryResultConverter resultConverter) { return Mono.defer(() -> { @@ -2575,9 +2630,10 @@ private Mono doFindAndReplace(String collectionName, serializeToJsonSafely(replacement), collectionName)); } + DocumentCallback resultReader = getResultReader(projection, collectionName, resultConverter); + return executeFindOneInternal(new FindAndReplaceCallback(collectionPreparer, mappedQuery, mappedFields, - mappedSort, replacement, collation, options), - new ProjectingReadCallback<>(this.mongoConverter, projection, collectionName), collectionName); + mappedSort, replacement, collation, options), resultReader, collectionName); }); } @@ -3124,7 +3180,7 @@ interface ReactiveCollectionQueryCallback extends ReactiveCollectionCallback< FindPublisher doInCollection(MongoCollection collection) throws MongoException, DataAccessException; } - static final class QueryResultConverterCallback implements DocumentCallback { + static class QueryResultConverterCallback implements DocumentCallback { private final QueryResultConverter converter; private final DocumentCallback delegate; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java index 378f13d917..dd515cb37c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperation.java @@ -20,6 +20,7 @@ import org.springframework.data.mongodb.core.query.CriteriaDefinition; import org.springframework.data.mongodb.core.query.Query; +import org.springframework.lang.Contract; import com.mongodb.client.result.DeleteResult; @@ -56,16 +57,22 @@ public interface ReactiveRemoveOperation { ReactiveRemove remove(Class domainType); /** - * Compose remove execution by calling one of the terminating methods. + * @author Christoph Strobl + * @since 5.0 */ - interface TerminatingRemove { + interface TerminatingResults { /** - * Remove all documents matching. + * Map the query result to a different type using {@link QueryResultConverter}. * - * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}. + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link ExecutableFindOperation.TerminatingResults}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 */ - Mono all(); + @Contract("_ -> new") + TerminatingResults map(QueryResultConverter converter); /** * Remove and return all matching documents.
@@ -78,6 +85,20 @@ interface TerminatingRemove { Flux findAndRemove(); } + /** + * Compose remove execution by calling one of the terminating methods. + */ + interface TerminatingRemove extends TerminatingResults { + + /** + * Remove all documents matching. + * + * @return {@link Mono} emitting the {@link DeleteResult}. Never {@literal null}. + */ + Mono all(); + + } + /** * Collection override (optional). */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java index 5c935ec628..f77b5296d7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupport.java @@ -15,10 +15,10 @@ */ package org.springframework.data.mongodb.core; -import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import org.jspecify.annotations.Nullable; import org.springframework.data.mongodb.core.query.Query; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -47,22 +47,25 @@ public ReactiveRemove remove(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null); + return new ReactiveRemoveSupport<>(template, domainType, ALL_QUERY, null, QueryResultConverter.entity()); } - static class ReactiveRemoveSupport implements ReactiveRemove, RemoveWithCollection { + static class ReactiveRemoveSupport implements ReactiveRemove, RemoveWithCollection { private final ReactiveMongoTemplate template; - private final Class domainType; + private final Class domainType; private final Query query; private final @Nullable String collection; + private final QueryResultConverter resultConverter; - ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable String collection) { + ReactiveRemoveSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable String collection, + QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; this.query = query; this.collection = collection; + this.resultConverter = resultConverter; } @Override @@ -70,7 +73,7 @@ public RemoveWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); - return new ReactiveRemoveSupport<>(template, domainType, query, collection); + return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -78,7 +81,7 @@ public TerminatingRemove matching(Query query) { Assert.notNull(query, "Query must not be null"); - return new ReactiveRemoveSupport<>(template, domainType, query, collection); + return new ReactiveRemoveSupport<>(template, domainType, query, collection, resultConverter); } @Override @@ -94,7 +97,13 @@ public Flux findAndRemove() { String collectionName = getCollectionName(); - return template.doFindAndDelete(collectionName, query, domainType); + return template.doFindAndDelete(collectionName, query, domainType, resultConverter); + } + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public TerminatingResults map(QueryResultConverter converter) { + return new ReactiveRemoveSupport<>(template, (Class) domainType, query, collection, converter); } private String getCollectionName() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java index 51f75f3265..c9f92029cc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperation.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import org.jetbrains.annotations.Contract; import reactor.core.publisher.Mono; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; @@ -64,6 +65,18 @@ public interface ReactiveUpdateOperation { */ interface TerminatingFindAndModify { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndModify map(QueryResultConverter converter); + /** * Find, modify and return the first matching document. * @@ -97,6 +110,18 @@ interface TerminatingReplace { */ interface TerminatingFindAndReplace extends TerminatingReplace { + /** + * Map the query result to a different type using {@link QueryResultConverter}. + * + * @param {@link Class type} of the result. + * @param converter the converter, must not be {@literal null}. + * @return new instance of {@link TerminatingFindAndModify}. + * @throws IllegalArgumentException if {@link QueryResultConverter converter} is {@literal null}. + * @since 5.0 + */ + @Contract("_ -> new") + TerminatingFindAndReplace map(QueryResultConverter converter); + /** * Find, replace and return the first matching document. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java index 75bfeef314..876a7a5aa2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupport.java @@ -47,10 +47,10 @@ public ReactiveUpdate update(Class domainType) { Assert.notNull(domainType, "DomainType must not be null"); - return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType); + return new ReactiveUpdateSupport<>(template, domainType, ALL_QUERY, null, null, null, null, null, domainType, QueryResultConverter.entity()); } - static class ReactiveUpdateSupport + static class ReactiveUpdateSupport implements ReactiveUpdate, UpdateWithCollection, UpdateWithQuery, TerminatingUpdate, FindAndReplaceWithOptions, FindAndReplaceWithProjection, TerminatingFindAndReplace { @@ -62,11 +62,12 @@ static class ReactiveUpdateSupport private final @Nullable FindAndModifyOptions findAndModifyOptions; private final @Nullable FindAndReplaceOptions findAndReplaceOptions; private final @Nullable Object replacement; - private final Class targetType; + private final Class targetType; + private final QueryResultConverter resultConverter; ReactiveUpdateSupport(ReactiveMongoTemplate template, Class domainType, Query query, @Nullable UpdateDefinition update, @Nullable String collection, @Nullable FindAndModifyOptions findAndModifyOptions, @Nullable FindAndReplaceOptions findAndReplaceOptions, - @Nullable Object replacement, Class targetType) { + @Nullable Object replacement, Class targetType, QueryResultConverter resultConverter) { this.template = template; this.domainType = domainType; @@ -77,6 +78,7 @@ static class ReactiveUpdateSupport this.findAndReplaceOptions = findAndReplaceOptions; this.replacement = replacement; this.targetType = targetType; + this.resultConverter = resultConverter; } @Override @@ -85,7 +87,7 @@ public TerminatingUpdate apply(org.springframework.data.mongodb.core.query.Up Assert.notNull(update, "Update must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -94,7 +96,7 @@ public UpdateWithQuery inCollection(String collection) { Assert.hasText(collection, "Collection must not be null nor empty"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -108,14 +110,14 @@ public Mono upsert() { } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings({"unchecked", "rawtypes", "NullAway"}) public Mono findAndModify() { String collectionName = getCollectionName(); return template.findAndModify(query, update, - findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), targetType, - collectionName); + findAndModifyOptions != null ? findAndModifyOptions : FindAndModifyOptions.none(), (Class) targetType, + collectionName, resultConverter); } @Override @@ -126,7 +128,7 @@ public Mono findAndReplace() { return template.findAndReplace(query, replacement, findAndReplaceOptions != null ? findAndReplaceOptions : FindAndReplaceOptions.none(), (Class) domainType, - getCollectionName(), targetType); + getCollectionName(), targetType, resultConverter); } @Override @@ -135,7 +137,7 @@ public UpdateWithUpdate matching(Query query) { Assert.notNull(query, "Query must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -149,7 +151,7 @@ public TerminatingFindAndModify withOptions(FindAndModifyOptions options) { Assert.notNull(options, "Options must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, options, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -158,7 +160,7 @@ public FindAndReplaceWithProjection replaceWith(T replacement) { Assert.notNull(replacement, "Replacement must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, targetType); + findAndReplaceOptions, replacement, targetType, resultConverter); } @Override @@ -167,7 +169,7 @@ public FindAndReplaceWithProjection withOptions(FindAndReplaceOptions options Assert.notNull(options, "Options must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, options, - replacement, targetType); + replacement, targetType, resultConverter); } @Override @@ -178,7 +180,7 @@ public TerminatingReplace withOptions(ReplaceOptions options) { target.upsert(); } return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - target, replacement, targetType); + target, replacement, targetType, resultConverter); } @Override @@ -187,7 +189,13 @@ public FindAndReplaceWithOptions as(Class resultType) { Assert.notNull(resultType, "ResultType must not be null"); return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, - findAndReplaceOptions, replacement, resultType); + findAndReplaceOptions, replacement, resultType, QueryResultConverter.entity()); + } + + @Override + public ReactiveUpdateSupport map(QueryResultConverter converter) { + return new ReactiveUpdateSupport<>(template, domainType, query, update, collection, findAndModifyOptions, + findAndReplaceOptions, replacement, targetType, this.resultConverter.andThen(converter)); } @Override diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java index 5659869705..cfdc5fe1a1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveRemoveOperationSupportTests.java @@ -15,13 +15,14 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import reactor.test.StepVerifier; import java.util.Objects; +import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -108,6 +109,15 @@ void removeAndReturnAllMatching() { .expectNext(han).verifyComplete(); } + @Test // GH-4949 + void removeConvertAndReturnAllMatching() { + + template.remove(Person.class).matching(query(where("firstname").is("han"))).map((raw, it) -> Optional.of(it.get())) + .findAndRemove().as(StepVerifier::create).expectNext(Optional.of(han)).verifyComplete(); + + template.findById(han.id, Person.class).as(StepVerifier::create).verifyComplete(); + } + @org.springframework.data.mongodb.core.mapping.Document(collection = STAR_WARS) static class Person { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java index 3ac99c2b6d..bef67501b3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveUpdateOperationSupportTests.java @@ -15,13 +15,15 @@ */ package org.springframework.data.mongodb.core; -import static org.assertj.core.api.Assertions.*; -import static org.springframework.data.mongodb.core.query.Criteria.*; -import static org.springframework.data.mongodb.core.query.Query.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; import reactor.test.StepVerifier; import java.util.Objects; +import java.util.Optional; import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; @@ -175,6 +177,18 @@ void findAndModifyWithDifferentDomainTypeAndCollection() { "Han"); } + @Test // GH-4949 + void findAndModifyWithWithResultConversion() { + + template.update(Jedi.class).inCollection(STAR_WARS).matching(query(where("_id").is(han.getId()))) + .apply(new Update().set("name", "Han")).map((raw, it) -> Optional.of(it.get())).findAndModify() + .as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual.get().getName()).isEqualTo("han")) + .verifyComplete(); + + assertThat(blocking.findOne(queryHan(), Person.class)).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", + "Han"); + } + @Test // DATAMONGO-1719 void findAndModifyWithOptions() { @@ -225,6 +239,18 @@ void findAndReplaceWithProjection() { }).verifyComplete(); } + @Test // GH-4949 + void findAndReplaceWithResultConversion() { + + Person luke = new Person(); + luke.firstname = "Luke"; + + template.update(Person.class).matching(queryHan()).replaceWith(luke).map((raw, it) -> Optional.of(it.get())).findAndReplace() // + .as(StepVerifier::create).consumeNextWith(it -> { + assertThat(it.get().getFirstname()).isEqualTo(han.firstname); + }).verifyComplete(); + } + @Test // DATAMONGO-1827 void findAndReplaceWithCollection() { diff --git a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc index f2a7a19bd6..ece61559ec 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/template-api.adoc @@ -116,6 +116,19 @@ WARNING: Projections must not be applied to xref:mongodb/mapping/document-refere You can switch between retrieving a single entity and retrieving multiple entities as a `List` or a `Stream` through the terminating methods: `first()`, `one()`, `all()`, or `stream()`. +Results can be contextually post-processed by using a `QueryResultConverter` that has access to both the raw result `Document` and the already mapped object by calling `map(...)` as outlined below. + +[source,java] +==== +---- +List> result = template.query(Person.class) + .as(Jedi.class) + .matching(query(where("firstname").is("luke"))) + .map((document, reader) -> Optional.of(reader.get())) + .all(); +---- +==== + When writing a geo-spatial query with `near(NearQuery)`, the number of terminating methods is altered to include only the methods that are valid for running a `geoNear` command in MongoDB (fetching entities as a `GeoResult` within `GeoResults`), as the following example shows: [tabs] From a0dc9d0cedd418403cf00989af5228b744b334bb Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 9 May 2025 09:03:32 +0200 Subject: [PATCH 58/74] Add missing issue reference to tests. Also reduce logging to war for AOT generation. Original Pull Request: #4949 --- .../mongodb/core/ExecutableUpdateOperationSupportTests.java | 2 +- .../data/mongodb/core/ReactiveFindOperationSupportTests.java | 2 +- spring-data-mongodb/src/test/resources/logback.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java index 3e7fa6c2bc..46732b1a29 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ExecutableUpdateOperationSupportTests.java @@ -185,7 +185,7 @@ void findAndModifyWithOptions() { assertThat(result.get()).isNotEqualTo(han).hasFieldOrPropertyWithValue("firstname", "Han"); } - @Test // GH- + @Test // GH-4949 void findAndModifyWithResultConverter() { Optional result = template.update(Person.class).matching(queryHan()) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java index 7f015874d0..d51696dd74 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveFindOperationSupportTests.java @@ -311,7 +311,7 @@ void findAllNearByWithCollectionAndProjection() { .verifyComplete(); } - @Test // GH-… + @Test // GH-4949 @DirtiesState void findAllNearByWithConverter() { diff --git a/spring-data-mongodb/src/test/resources/logback.xml b/spring-data-mongodb/src/test/resources/logback.xml index 6ad8c163ec..55e4309a36 100644 --- a/spring-data-mongodb/src/test/resources/logback.xml +++ b/spring-data-mongodb/src/test/resources/logback.xml @@ -20,7 +20,7 @@ - + From d850b9ef06dd6b821b2d2ed1f54d44b7530e8cea Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 8 May 2025 11:33:59 +0200 Subject: [PATCH 59/74] Use `LocalVariableNameFactory` to avoid parameter name clashes. Closes #4965 --- .../MongoAotRepositoryFragmentSupport.java | 3 + .../repository/aot/MongoCodeBlocks.java | 152 ++++++++++-------- .../aot/MongoRepositoryContributor.java | 120 ++++++-------- .../aot/MongoRepositoryContributorTests.java | 5 +- .../aot/MongoRepositoryMetadataTests.java | 9 +- .../aot/TestMongoAotRepositoryContext.java | 150 ++++++++--------- 6 files changed, 219 insertions(+), 220 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index 1537b6c722..df635fcd28 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -35,7 +35,10 @@ import org.springframework.util.ObjectUtils; /** + * Support class for MongoDB AOT repository fragments. + * * @author Christoph Strobl + * @since 5.0 */ public class MongoAotRepositoryFragmentSupport { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index 8338ffe6bf..7afa2a5f53 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -246,20 +246,24 @@ CodeBlock build() { builder.add("\n"); String updateReference = updateVariableName; - builder.addStatement("$T<$T> updater = $L.update($T.class)", ExecutableUpdate.class, - context.getRepositoryInformation().getDomainType(), mongoOpsRef, - context.getRepositoryInformation().getDomainType()); + Class domainType = context.getRepositoryInformation().getDomainType(); + builder.addStatement("$T<$T> $L = $L.update($T.class)", ExecutableUpdate.class, domainType, + context.localVariable("updater"), mongoOpsRef, domainType); Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); if (ReflectionUtils.isVoid(returnType)) { - builder.addStatement("updater.matching($L).apply($L).all()", queryVariableName, updateReference); + builder.addStatement("$L.matching($L).apply($L).all()", context.localVariable("updater"), queryVariableName, + updateReference); } else if (ClassUtils.isAssignable(Long.class, returnType)) { - builder.addStatement("return updater.matching($L).apply($L).all().getModifiedCount()", queryVariableName, + builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()", + context.localVariable("updater"), queryVariableName, updateReference); } else { - builder.addStatement("$T modifiedCount = updater.matching($L).apply($L).all().getModifiedCount()", Long.class, + builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class, + context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName, updateReference); - builder.addStatement("return $T.convertNumberToTargetClass(modifiedCount, $T.class)", NumberUtils.class, + builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class, + context.localVariable("modifiedCount"), returnType); } @@ -314,24 +318,29 @@ CodeBlock build() { Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); - builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef, + builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); if (!queryMethod.isCollectionQuery()) { builder.addStatement( - "return $T.<$T>firstElement(convertSimpleRawResults($T.class, results.getMappedResults()))", - CollectionUtils.class, returnType, returnType); + "return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", + CollectionUtils.class, returnType, returnType, context.localVariable("results")); } else { - builder.addStatement("return convertSimpleRawResults($T.class, results.getMappedResults())", returnType); + builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType, + context.localVariable("results")); } } else { if (queryMethod.isSliceQuery()) { - builder.addStatement("$T results = $L.aggregate($L, $T.class)", AggregationResults.class, mongoOpsRef, + builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); - builder.addStatement("boolean hasNext = results.getMappedResults().size() > $L.getPageSize()", - context.getPageableParameterName()); + builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()", + context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName()); builder.addStatement( - "return new $T<>(hasNext ? results.getMappedResults().subList(0, $L.getPageSize()) : results.getMappedResults(), $L, hasNext)", - SliceImpl.class, context.getPageableParameterName(), context.getPageableParameterName()); + "return new $T<>($L ? $L.getMappedResults().subList(0, $L.getPageSize()) : $L.getMappedResults(), $L, $L)", + SliceImpl.class, context.localVariable("hasNext"), context.localVariable("results"), + context.getPageableParameterName(), context.localVariable("results"), context.getPageableParameterName(), + context.localVariable("hasNext")); } else { builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, aggregationVariableName, outputType); @@ -368,18 +377,19 @@ CodeBlock build() { Builder builder = CodeBlock.builder(); boolean isProjecting = context.getReturnedType().isProjecting(); + Class domainType = context.getRepositoryInformation().getDomainType(); Object actualReturnType = isProjecting ? context.getActualReturnType().getType() - : context.getRepositoryInformation().getDomainType(); + : domainType; builder.add("\n"); if (isProjecting) { - builder.addStatement("$T<$T> finder = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, - mongoOpsRef, context.getRepositoryInformation().getDomainType(), actualReturnType); + builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType); } else { - builder.addStatement("$T<$T> finder = $L.query($T.class)", FindWithQuery.class, actualReturnType, mongoOpsRef, - context.getRepositoryInformation().getDomainType()); + builder.addStatement("$T<$T> $L = $L.query($T.class)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType); } String terminatingMethod; @@ -395,13 +405,14 @@ CodeBlock build() { } if (queryMethod.isPageQuery()) { - builder.addStatement("return new $T(finder, $L).execute($L)", PagedExecution.class, + builder.addStatement("return new $T($L, $L).execute($L)", PagedExecution.class, context.localVariable("finder"), context.getPageableParameterName(), query.name()); } else if (queryMethod.isSliceQuery()) { - builder.addStatement("return new $T(finder, $L).execute($L)", SlicedExecution.class, - context.getPageableParameterName(), query.name()); + builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class, + context.localVariable("finder"), context.getPageableParameterName(), query.name()); } else { - builder.addStatement("return finder.matching($L).$L", query.name(), terminatingMethod); + builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), + terminatingMethod); } return builder.build(); @@ -415,7 +426,7 @@ static class AggregationCodeBlockBuilder { private final MongoQueryMethod queryMethod; private AggregationInteraction source; - private List arguments; + private final List arguments; private String aggregationVariableName; private boolean pipelineOnly; @@ -449,7 +460,7 @@ CodeBlock build() { CodeBlock.Builder builder = CodeBlock.builder(); builder.add("\n"); - String pipelineName = aggregationVariableName + (pipelineOnly ? "" : "Pipeline"); + String pipelineName = context.localVariable(aggregationVariableName + (pipelineOnly ? "" : "Pipeline")); builder.add(pipeline(pipelineName)); if (!pipelineOnly) { @@ -486,8 +497,7 @@ private CodeBlock pipeline(String pipelineVariableName) { } Builder builder = CodeBlock.builder(); - String stagesVariableName = "stages"; - builder.add(aggregationStages(stagesVariableName, source.stages(), stageCount, arguments)); + builder.add(aggregationStages(context.localVariable("stages"), source.stages(), stageCount, arguments)); if (mightBeSorted) { builder.add(sortingStage(sortParameter)); @@ -502,7 +512,7 @@ private CodeBlock pipeline(String pipelineVariableName) { } builder.addStatement("$T $L = createPipeline($L)", AggregationPipeline.class, pipelineVariableName, - stagesVariableName); + context.localVariable("stages")); return builder.build(); } @@ -533,7 +543,8 @@ private CodeBlock aggregationOptions(String aggregationVariableName) { if (!options.isEmpty()) { Builder optionsBuilder = CodeBlock.builder(); - optionsBuilder.add("$T aggregationOptions = $T.builder()\n", AggregationOptions.class, + optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class, + context.localVariable("aggregationOptions"), AggregationOptions.class); optionsBuilder.indent(); for (CodeBlock optionBlock : options) { @@ -544,67 +555,81 @@ private CodeBlock aggregationOptions(String aggregationVariableName) { optionsBuilder.unindent(); builder.add(optionsBuilder.build()); - builder.addStatement("$L = $L.withOptions(aggregationOptions)", aggregationVariableName, - aggregationVariableName); + builder.addStatement("$L = $L.withOptions($L)", aggregationVariableName, aggregationVariableName, + context.localVariable("aggregationOptions")); } return builder.build(); } - private static CodeBlock aggregationStages(String stageListVariableName, Iterable stages, int stageCount, + private CodeBlock aggregationStages(String stageListVariableName, Iterable stages, int stageCount, List arguments) { Builder builder = CodeBlock.builder(); builder.addStatement("$T<$T> $L = new $T($L)", List.class, Object.class, stageListVariableName, ArrayList.class, stageCount); int stageCounter = 0; + for (String stage : stages) { - String stageName = "stage_%s".formatted(stageCounter++); + String stageName = context.localVariable("stage_%s".formatted(stageCounter++)); builder.add(renderExpressionToDocument(stage, stageName, arguments)); - builder.addStatement("stages.add($L)", stageName); + builder.addStatement("$L.add($L)", context.localVariable("stages"), stageName); } + return builder.build(); } - private static CodeBlock sortingStage(String sortProvider) { + private CodeBlock sortingStage(String sortProvider) { Builder builder = CodeBlock.builder(); - builder.beginControlFlow("if($L.isSorted())", sortProvider); - builder.addStatement("$T sortDocument = new $T()", Document.class, Document.class); - builder.beginControlFlow("for ($T order : $L)", Order.class, sortProvider); - builder.addStatement("sortDocument.append(order.getProperty(), order.isAscending() ? 1 : -1);"); + + builder.beginControlFlow("if ($L.isSorted())", sortProvider); + builder.addStatement("$T $L = new $T()", Document.class, context.localVariable("sortDocument"), Document.class); + builder.beginControlFlow("for ($T $L : $L)", Order.class, context.localVariable("order"), sortProvider); + builder.addStatement("$L.append($L.getProperty(), $L.isAscending() ? 1 : -1);", + context.localVariable("sortDocument"), context.localVariable("order"), context.localVariable("order")); builder.endControlFlow(); - builder.addStatement("stages.add(new $T($S, sortDocument))", Document.class, "$sort"); + builder.addStatement("stages.add(new $T($S, $L))", Document.class, "$sort", + context.localVariable("sortDocument")); builder.endControlFlow(); + return builder.build(); } - private static CodeBlock pagingStage(String pageableProvider, boolean slice) { + private CodeBlock pagingStage(String pageableProvider, boolean slice) { Builder builder = CodeBlock.builder(); + builder.add(sortingStage(pageableProvider + ".getSort()")); - builder.beginControlFlow("if($L.isPaged())", pageableProvider); - builder.beginControlFlow("if($L.getOffset() > 0)", pageableProvider); - builder.addStatement("stages.add($T.skip($L.getOffset()))", Aggregation.class, pageableProvider); + builder.beginControlFlow("if ($L.isPaged())", pageableProvider); + builder.beginControlFlow("if ($L.getOffset() > 0)", pageableProvider); + builder.addStatement("$L.add($T.skip($L.getOffset()))", context.localVariable("stages"), Aggregation.class, + pageableProvider); builder.endControlFlow(); if (slice) { - builder.addStatement("stages.add($T.limit($L.getPageSize() + 1))", Aggregation.class, pageableProvider); + builder.addStatement("$L.add($T.limit($L.getPageSize() + 1))", context.localVariable("stages"), + Aggregation.class, pageableProvider); } else { - builder.addStatement("stages.add($T.limit($L.getPageSize()))", Aggregation.class, pageableProvider); + builder.addStatement("$L.add($T.limit($L.getPageSize()))", context.localVariable("stages"), Aggregation.class, + pageableProvider); } builder.endControlFlow(); return builder.build(); } - private static CodeBlock limitingStage(String limitProvider) { + private CodeBlock limitingStage(String limitProvider) { Builder builder = CodeBlock.builder(); - builder.beginControlFlow("if($L.isLimited())", limitProvider); - builder.addStatement("stages.add($T.limit($L.max()))", Aggregation.class, limitProvider); + + builder.beginControlFlow("if ($L.isLimited())", limitProvider); + builder.addStatement("$L.add($T.limit($L.max()))", context.localVariable("stages"), Aggregation.class, + limitProvider); builder.endControlFlow(); + return builder.build(); } + } @NullUnmarked @@ -614,7 +639,7 @@ static class QueryCodeBlockBuilder { private final MongoQueryMethod queryMethod; private QueryInteraction source; - private List arguments; + private final List arguments; private String queryVariableName; QueryCodeBlockBuilder(AotQueryMethodGenerationContext context, MongoQueryMethod queryMethod) { @@ -697,17 +722,10 @@ private CodeBlock renderExpressionToQuery(@Nullable String source, String variab builder.addStatement("$T $L = new $T(new $T())", BasicQuery.class, variableName, BasicQuery.class, Document.class); } else if (!containsPlaceholder(source)) { - - String tmpVarName = "%sString".formatted(variableName); - builder.addStatement("String $L = $S", tmpVarName, source); - - builder.addStatement("$T $L = new $T($T.parse($L))", BasicQuery.class, variableName, BasicQuery.class, - Document.class, tmpVarName); + builder.addStatement("$T $L = new $T($T.parse($S))", BasicQuery.class, variableName, BasicQuery.class, + Document.class, source); } else { - - String tmpVarName = "%sString".formatted(variableName); - builder.addStatement("String $L = $S", tmpVarName, source); - builder.addStatement("$T $L = createQuery($L, new $T[]{ $L })", BasicQuery.class, variableName, tmpVarName, + builder.addStatement("$T $L = createQuery($S, new $T[]{ $L })", BasicQuery.class, variableName, source, Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); } @@ -757,15 +775,9 @@ private static CodeBlock renderExpressionToDocument(@Nullable String source, Str if (!StringUtils.hasText(source)) { builder.addStatement("$T $L = new $T()", Document.class, variableName, Document.class); } else if (!containsPlaceholder(source)) { - - String tmpVarName = "%sString".formatted(variableName); - builder.addStatement("String $L = $S", tmpVarName, source); - builder.addStatement("$T $L = $T.parse($L)", Document.class, variableName, Document.class, tmpVarName); + builder.addStatement("$T $L = $T.parse($S)", Document.class, variableName, Document.class, source); } else { - - String tmpVarName = "%sString".formatted(variableName); - builder.addStatement("String $L = $S", tmpVarName, source); - builder.addStatement("$T $L = bindParameters($L, new $T[]{ $L })", Document.class, variableName, tmpVarName, + builder.addStatement("$T $L = bindParameters($S, new $T[]{ $L })", Document.class, variableName, source, Object.class, StringUtils.collectionToDelimitedString(arguments, ", ")); } return builder.build(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index def03c7973..3ef1f5ac91 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -15,13 +15,7 @@ */ package org.springframework.data.mongodb.repository.aot; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.aggregationExecutionBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.deleteExecutionBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.queryExecutionBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateBlockBuilder; -import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.updateExecutionBlockBuilder; +import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*; import java.lang.reflect.Method; import java.util.regex.Pattern; @@ -29,16 +23,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.Query; import org.springframework.data.mongodb.repository.Update; -import org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.QueryCodeBlockBuilder; import org.springframework.data.mongodb.repository.query.MongoQueryMethod; +import org.springframework.data.repository.aot.generate.AotRepositoryClassBuilder; import org.springframework.data.repository.aot.generate.AotRepositoryConstructorBuilder; -import org.springframework.data.repository.aot.generate.AotRepositoryFragmentMetadata; import org.springframework.data.repository.aot.generate.MethodContributor; import org.springframework.data.repository.aot.generate.RepositoryContributor; import org.springframework.data.repository.config.AotRepositoryContext; @@ -48,7 +42,6 @@ import org.springframework.data.repository.query.parser.PartTree; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; -import org.springframework.javapoet.TypeSpec.Builder; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -60,7 +53,7 @@ */ public class MongoRepositoryContributor extends RepositoryContributor { - private static final Log logger = LogFactory.getLog(RepositoryContributor.class); + private static final Log logger = LogFactory.getLog(MongoRepositoryContributor.class); private final AotQueryCreator queryCreator; private final MongoMappingContext mappingContext; @@ -73,9 +66,8 @@ public MongoRepositoryContributor(AotRepositoryContext repositoryContext) { } @Override - protected void customizeClass(RepositoryInformation information, AotRepositoryFragmentMetadata metadata, - Builder builder) { - builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class)); + protected void customizeClass(AotRepositoryClassBuilder classBuilder) { + classBuilder.customize(builder -> builder.superclass(TypeName.get(MongoAotRepositoryFragmentSupport.class))); } @Override @@ -85,70 +77,60 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB constructorBuilder.addParameter("context", TypeName.get(RepositoryFactoryBeanSupport.FragmentCreationContext.class), false); - constructorBuilder.customize((repositoryInformation, builder) -> { + constructorBuilder.customize((builder) -> { builder.addStatement("super(operations, context)"); }); } @Override @SuppressWarnings("NullAway") - protected @Nullable MethodContributor contributeQueryMethod(Method method, - RepositoryInformation repositoryInformation) { + protected @Nullable MethodContributor contributeQueryMethod(Method method) { - MongoQueryMethod queryMethod = new MongoQueryMethod(method, repositoryInformation, getProjectionFactory(), + MongoQueryMethod queryMethod = new MongoQueryMethod(method, getRepositoryInformation(), getProjectionFactory(), mappingContext); - if (backoff(queryMethod)) { - return null; + if (queryMethod.hasAnnotatedAggregation()) { + AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation()); + return aggregationMethodContributor(queryMethod, aggregation); } - try { - if (queryMethod.hasAnnotatedAggregation()) { - - AggregationInteraction aggregation = new AggregationInteraction(queryMethod.getAnnotatedAggregation()); - return aggregationMethodContributor(queryMethod, aggregation); - } - - QueryInteraction query = createStringQuery(repositoryInformation, queryMethod, - AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount()); + QueryInteraction query = createStringQuery(getRepositoryInformation(), queryMethod, + AnnotatedElementUtils.findMergedAnnotation(method, Query.class), method.getParameterCount()); - if (queryMethod.hasAnnotatedQuery()) { - if (StringUtils.hasText(queryMethod.getAnnotatedQuery()) - && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) { + if (queryMethod.hasAnnotatedQuery()) { + if (StringUtils.hasText(queryMethod.getAnnotatedQuery()) + && Pattern.compile("[\\?:][#$]\\{.*\\}").matcher(queryMethod.getAnnotatedQuery()).find()) { - if (logger.isDebugEnabled()) { - logger.debug( - "Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName())); - } - return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query); + if (logger.isDebugEnabled()) { + logger.debug( + "Skipping AOT generation for [%s]. SpEL expressions are not supported".formatted(method.getName())); } + return MethodContributor.forQueryMethod(queryMethod).metadataOnly(query); } + } - if (query.isDelete()) { - return deleteMethodContributor(queryMethod, query); - } + if (backoff(queryMethod)) { + return null; + } - if (queryMethod.isModifyingQuery()) { + if (query.isDelete()) { + return deleteMethodContributor(queryMethod, query); + } - Update updateSource = queryMethod.getUpdateSource(); - if (StringUtils.hasText(updateSource.value())) { - UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value())); - return updateMethodContributor(queryMethod, update); - } - if (!ObjectUtils.isEmpty(updateSource.pipeline())) { - AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); - return aggregationUpdateMethodContributor(queryMethod, update); - } - } + if (queryMethod.isModifyingQuery()) { - return queryMethodContributor(queryMethod, query); - } catch (RuntimeException codeGenerationError) { - if (logger.isErrorEnabled()) { - logger.error("Failed to generate code for [%s] [%s]".formatted(repositoryInformation.getRepositoryInterface(), - method.getName()), codeGenerationError); + Update updateSource = queryMethod.getUpdateSource(); + if (StringUtils.hasText(updateSource.value())) { + UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value())); + return updateMethodContributor(queryMethod, update); + } + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); + return aggregationUpdateMethodContributor(queryMethod, update); } } - return null; + + return queryMethodContributor(queryMethod, query); } @SuppressWarnings("NullAway") @@ -193,7 +175,6 @@ private static MethodContributor aggregationMethodContributor( return MethodContributor.forQueryMethod(queryMethod).withMetadata(aggregation).contribute(context -> { CodeBlock.Builder builder = CodeBlock.builder(); - builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); builder.add(aggregationBlockBuilder(context, queryMethod).stages(aggregation) .usingAggregationVariableName("aggregation").build()); @@ -209,15 +190,14 @@ private static MethodContributor updateMethodContributor(Mongo return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { CodeBlock.Builder builder = CodeBlock.builder(); - builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); // update filter - String filterVariableName = update.name(); + String filterVariableName = context.localVariable(update.name()); builder.add(queryBlockBuilder(context, queryMethod).filter(update.getFilter()) .usingQueryVariableName(filterVariableName).build()); // update definition - String updateVariableName = "updateDefinition"; + String updateVariableName = context.localVariable("updateDefinition"); builder.add( updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName).build()); @@ -233,10 +213,9 @@ private static MethodContributor aggregationUpdateMethodContri return MethodContributor.forQueryMethod(queryMethod).withMetadata(update).contribute(context -> { CodeBlock.Builder builder = CodeBlock.builder(); - builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); // update filter - String filterVariableName = update.name(); + String filterVariableName = context.localVariable(update.name()); QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(update.getFilter()); builder.add(queryCodeBlockBuilder.usingQueryVariableName(filterVariableName).build()); @@ -245,11 +224,12 @@ private static MethodContributor aggregationUpdateMethodContri builder.add(aggregationBlockBuilder(context, queryMethod).stages(update) .usingAggregationVariableName(updateVariableName).pipelineOnly(true).build()); - builder.addStatement("$T aggregationUpdate = $T.from($L.getOperations())", AggregationUpdate.class, + builder.addStatement("$T $L = $T.from($L.getOperations())", AggregationUpdate.class, + context.localVariable("aggregationUpdate"), AggregationUpdate.class, updateVariableName); builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) - .referencingUpdate("aggregationUpdate").build()); + .referencingUpdate(context.localVariable("aggregationUpdate")).build()); return builder.build(); }); } @@ -260,11 +240,11 @@ private static MethodContributor deleteMethodContributor(Mongo return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { CodeBlock.Builder builder = CodeBlock.builder(); - builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); - builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build()); - builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(query.name()).build()); + String queryVariableName = context.localVariable(query.name()); + builder.add(queryCodeBlockBuilder.usingQueryVariableName(queryVariableName).build()); + builder.add(deleteExecutionBlockBuilder(context, queryMethod).referencing(queryVariableName).build()); return builder.build(); }); } @@ -275,12 +255,12 @@ private static MethodContributor queryMethodContributor(MongoQ return MethodContributor.forQueryMethod(queryMethod).withMetadata(query).contribute(context -> { CodeBlock.Builder builder = CodeBlock.builder(); - builder.add(context.codeBlocks().logDebug("invoking [%s]".formatted(context.getMethod().getName()))); QueryCodeBlockBuilder queryCodeBlockBuilder = queryBlockBuilder(context, queryMethod).filter(query); - builder.add(queryCodeBlockBuilder.usingQueryVariableName(query.name()).build()); + builder.add(queryCodeBlockBuilder.usingQueryVariableName(context.localVariable(query.name())).build()); builder.add(queryExecutionBlockBuilder(context, queryMethod).forQuery(query).build()); return builder.build(); }); } + } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index 94b58200b4..d5c388751d 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -51,11 +51,14 @@ import com.mongodb.client.MongoClient; /** + * Integration tests for the {@link UserRepository} AOT fragment. + * * @author Christoph Strobl + * @author Mark Paluch */ @ExtendWith(MongoClientExtension.class) @SpringJUnitConfig(classes = MongoRepositoryContributorTests.MongoRepositoryContributorConfiguration.class) -public class MongoRepositoryContributorTests { +class MongoRepositoryContributorTests { private static final String DB_NAME = "aot-repo-tests"; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java index 67017720eb..aa069a2710 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryMetadataTests.java @@ -15,9 +15,9 @@ */ package org.springframework.data.mongodb.repository.aot; -import static net.javacrumbs.jsonunit.assertj.JsonAssertions.assertThatJson; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; +import static net.javacrumbs.jsonunit.assertj.JsonAssertions.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; import example.aot.UserRepository; @@ -27,6 +27,7 @@ import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -176,7 +177,7 @@ void shouldDocumentBaseFragment() throws IOException { assertThat(resource.exists()).isTrue(); String json = resource.getContentAsString(StandardCharsets.UTF_8); - System.out.println(json); + assertThatJson(json).inPath("$.methods[?(@.name == 'existsById')].fragment").isArray().first().isObject() .containsEntry("fragment", "org.springframework.data.mongodb.repository.support.SimpleMongoRepository"); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 7cc47d9566..8100a67a64 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -38,81 +38,81 @@ */ class TestMongoAotRepositoryContext implements AotRepositoryContext { - private final StubRepositoryInformation repositoryInformation; - private final Environment environment = new StandardEnvironment(); - - TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { - this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); - } - - @Override - public ConfigurableListableBeanFactory getBeanFactory() { - return null; - } - - @Override - public TypeIntrospector introspectType(String typeName) { - return null; - } - - @Override - public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { - return null; - } - - @Override - public String getBeanName() { - return "dummyRepository"; - } - - @Override - public String getModuleName() { - return "MongoDB"; + private final StubRepositoryInformation repositoryInformation; + private final Environment environment = new StandardEnvironment(); + + TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); + } + + @Override + public ConfigurableListableBeanFactory getBeanFactory() { + return null; + } + + @Override + public TypeIntrospector introspectType(String typeName) { + return null; + } + + @Override + public IntrospectedBeanDefinition introspectBeanDefinition(String beanName) { + return null; + } + + @Override + public String getBeanName() { + return "dummyRepository"; + } + + @Override + public String getModuleName() { + return "MongoDB"; + } + + @Override + public Set getBasePackages() { + return Set.of("org.springframework.data.dummy.repository.aot"); + } + + @Override + public Set> getIdentifyingAnnotations() { + return Set.of(Document.class); + } + + @Override + public RepositoryInformation getRepositoryInformation() { + return repositoryInformation; + } + + @Override + public Set> getResolvedAnnotations() { + return Set.of(); + } + + @Override + public Set> getResolvedTypes() { + return Set.of(); + } + + public List getRequiredContextFiles() { + return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); + } + + static ClassFile classFileForType(Class type) { + + String name = type.getName(); + ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); + + try { + return ClassFile.of(name, cpr.getContentAsByteArray()); + } catch (IOException e) { + throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); } + } - @Override - public Set getBasePackages() { - return Set.of("org.springframework.data.dummy.repository.aot"); - } - - @Override - public Set> getIdentifyingAnnotations() { - return Set.of(Document.class); - } - - @Override - public RepositoryInformation getRepositoryInformation() { - return repositoryInformation; - } - - @Override - public Set> getResolvedAnnotations() { - return Set.of(); - } - - @Override - public Set> getResolvedTypes() { - return Set.of(); - } - - public List getRequiredContextFiles() { - return List.of(classFileForType(repositoryInformation.getRepositoryBaseClass())); - } - - static ClassFile classFileForType(Class type) { - - String name = type.getName(); - ClassPathResource cpr = new ClassPathResource(name.replaceAll("\\.", "/") + ".class"); - - try { - return ClassFile.of(name, cpr.getContentAsByteArray()); - } catch (IOException e) { - throw new IllegalArgumentException("Cannot open [%s].".formatted(cpr.getPath())); - } - } - - @Override - public Environment getEnvironment() { - return environment; - } + @Override + public Environment getEnvironment() { + return environment; + } } From eab7aae16c18e46de143c94c85ff00b37b8261fd Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 10 Apr 2025 11:49:05 +0200 Subject: [PATCH 60/74] Add support for returning SearchResult from repository query methods. Closes: #4960 --- .../data/mongodb/core/MongoTemplate.java | 8 +- .../mongodb/core/ReactiveMongoTemplate.java | 2 +- .../core/aggregation/AggregationResults.java | 1 + .../mongodb/core/convert/GeoConverters.java | 4 +- .../data/mongodb/core/geo/Sphere.java | 2 +- .../core/mapping/MongoSimpleTypes.java | 1 + .../data/mongodb/core/query/NearQuery.java | 11 +- .../data/mongodb/repository/VectorSearch.java | 123 ++++++ .../repository/aot/AotQueryCreator.java | 17 + .../aot/MongoRepositoryContributor.java | 6 +- .../repository/query/AbstractMongoQuery.java | 4 +- .../query/ConvertingParameterAccessor.java | 20 +- .../query/MongoParameterAccessor.java | 1 + .../repository/query/MongoParameters.java | 9 +- .../MongoParametersParameterAccessor.java | 22 +- .../repository/query/MongoQueryCreator.java | 29 +- .../repository/query/MongoQueryExecution.java | 88 ++++ .../repository/query/MongoQueryMethod.java | 19 + .../repository/query/PartTreeMongoQuery.java | 4 +- .../query/ReactiveMongoQueryExecution.java | 59 +++ .../query/ReactivePartTreeMongoQuery.java | 2 +- .../ReactiveVectorSearchAggregation.java | 123 ++++++ .../query/VectorSearchAggregation.java | 112 +++++ .../query/VectorSearchDelegate.java | 396 ++++++++++++++++++ .../support/MongoRepositoryFactory.java | 3 + .../ReactiveMongoRepositoryFactory.java | 3 + .../GeoNearOperationUnitTests.java | 4 +- .../core/convert/GeoConvertersUnitTests.java | 8 +- .../MappingMongoConverterUnitTests.java | 5 +- .../core/geo/GeoSpatial2DSphereTests.java | 4 +- .../core/query/MetricConversionUnitTests.java | 11 +- .../core/query/NearQueryUnitTests.java | 26 +- ...tractPersonRepositoryIntegrationTests.java | 26 +- .../mongodb/repository/PersonRepository.java | 1 + .../ReactiveMongoRepositoryTests.java | 8 +- .../repository/ReactiveVectorSearchTests.java | 224 ++++++++++ .../mongodb/repository/VectorSearchTests.java | 286 +++++++++++++ ...oParametersParameterAccessorUnitTests.java | 51 ++- .../query/MongoParametersUnitTests.java | 21 + .../query/MongoQueryCreatorUnitTests.java | 13 +- .../query/MongoQueryExecutionUnitTests.java | 3 +- .../query/MongoQueryMethodUnitTests.java | 1 + .../ReactiveMongoQueryExecutionUnitTests.java | 6 +- .../ReactiveMongoQueryMethodUnitTests.java | 2 +- .../query/StubParameterAccessor.java | 18 + .../VectorSearchAggregationUnitTests.java | 108 +++++ .../query/VectorSearchDelegateUnitTests.java | 130 ++++++ .../ReactiveFindOperationExtensionsTests.kt | 6 +- src/main/antora/modules/ROOT/nav.adoc | 1 + .../mongodb/repositories/vector-search.adoc | 8 + .../partials/vector-search-intro-include.adoc | 1 + ...ector-search-method-annotated-include.adoc | 23 + .../vector-search-method-derived-include.adoc | 21 + .../partials/vector-search-model-include.adoc | 15 + .../vector-search-repository-include.adoc | 19 + .../vector-search-scoring-include.adoc | 32 ++ 56 files changed, 2047 insertions(+), 104 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java create mode 100644 src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc create mode 100644 src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index e5c7f95102..db3dcc1263 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -1079,7 +1079,7 @@ GeoResults doGeoNear(NearQuery near, Class domainType, String colle result.add(geoResult); } - Distance avgDistance = new Distance( + Distance avgDistance = Distance.of( result.size() == 0 ? 0 : aggregate.divide(new BigDecimal(result.size()), RoundingMode.HALF_UP).doubleValue(), near.getMetric()); @@ -2688,7 +2688,9 @@ protected List doFind(String collectionName, if (LOGGER.isDebugEnabled()) { - Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp ? getMappedSortObject(sqcp.getSortObject(), entity) : null; + Document mappedSort = preparer instanceof SortingQueryCursorPreparer sqcp + ? getMappedSortObject(sqcp.getSortObject(), entity) + : null; LOGGER.debug(String.format("find using query: %s fields: %s sort: %s for class: %s in collection: %s", serializeToJsonSafely(mappedQuery), mappedFields, serializeToJsonSafely(mappedSort), entityClass, collectionName)); @@ -3623,7 +3625,7 @@ public GeoResult doWith(Document object) { T doWith = delegate.doWith(object); - return new GeoResult<>(doWith, new Distance(distance, metric)); + return new GeoResult<>(doWith, Distance.of(distance, metric)); } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 663264f326..935c55fc9e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -3312,7 +3312,7 @@ public Mono> doWith(Document object) { double distance = getDistance(object); - return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, new Distance(distance, metric))); + return delegate.doWith(object).map(doWith -> new GeoResult<>(doWith, Distance.of(distance, metric))); } double getDistance(Document object) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java index f5a861cddd..7b27739229 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationResults.java @@ -105,4 +105,5 @@ public Document getRawResults() { Object object = rawResults.get("serverUsed"); return object instanceof String stringValue ? stringValue : null; } + } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java index ae73ab68bd..b595ab688f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/GeoConverters.java @@ -270,7 +270,7 @@ enum DocumentToCircleConverter implements Converter { Assert.notNull(center, "Center must not be null"); Assert.notNull(radius, "Radius must not be null"); - Distance distance = new Distance(toPrimitiveDoubleValue(radius)); + Distance distance = Distance.of(toPrimitiveDoubleValue(radius)); if (source.containsKey("metric")) { @@ -335,7 +335,7 @@ enum DocumentToSphereConverter implements Converter { Assert.notNull(center, "Center must not be null"); Assert.notNull(radius, "Radius must not be null"); - Distance distance = new Distance(toPrimitiveDoubleValue(radius)); + Distance distance = Distance.of(toPrimitiveDoubleValue(radius)); if (source.containsKey("metric")) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java index 47be645869..d3ca840d6b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/geo/Sphere.java @@ -63,7 +63,7 @@ public Sphere(Point center, Distance radius) { * @param radius */ public Sphere(Point center, double radius) { - this(center, new Distance(radius)); + this(center, Distance.of(radius)); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java index 3b3a520bc3..6b4d9b9e9b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/mapping/MongoSimpleTypes.java @@ -29,6 +29,7 @@ import org.bson.types.Decimal128; import org.bson.types.ObjectId; import org.bson.types.Symbol; + import org.springframework.data.mapping.model.SimpleTypeHolder; import com.mongodb.DBRef; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java index 6dad07b8cb..88d7dc5c1d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/query/NearQuery.java @@ -19,6 +19,7 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Pageable; import org.springframework.data.geo.CustomMetric; import org.springframework.data.geo.Distance; @@ -329,7 +330,7 @@ public NearQuery with(Pageable pageable) { */ @Contract("_ -> this") public NearQuery maxDistance(double maxDistance) { - return maxDistance(new Distance(maxDistance, getMetric())); + return maxDistance(Distance.of(maxDistance, getMetric())); } /** @@ -345,7 +346,7 @@ public NearQuery maxDistance(double maxDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); - return maxDistance(new Distance(maxDistance, metric)); + return maxDistance(Distance.of(maxDistance, metric)); } /** @@ -388,7 +389,7 @@ public NearQuery maxDistance(Distance distance) { */ @Contract("_ -> this") public NearQuery minDistance(double minDistance) { - return minDistance(new Distance(minDistance, getMetric())); + return minDistance(Distance.of(minDistance, getMetric())); } /** @@ -405,7 +406,7 @@ public NearQuery minDistance(double minDistance, Metric metric) { Assert.notNull(metric, "Metric must not be null"); - return minDistance(new Distance(minDistance, metric)); + return minDistance(Distance.of(minDistance, metric)); } /** @@ -611,7 +612,7 @@ public NearQuery withReadPreference(ReadPreference readPreference) { * Get the {@link ReadConcern} to use. Will return the underlying {@link #query(Query) queries} * {@link Query#getReadConcern() ReadConcern} if present or the one defined on the {@link NearQuery#readConcern} * itself. - * + * * @return can be {@literal null} if none set. * @since 4.1 * @see ReadConcernAware diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java new file mode 100644 index 0000000000..336889f719 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/VectorSearch.java @@ -0,0 +1,123 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.core.annotation.AliasFor; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; + +/** + * Annotation to declare Vector Search queries directly on repository methods. Vector Search queries are used to search + * for similar documents based on vector embeddings typically returning + * {@link org.springframework.data.domain.SearchResults} and limited by either a + * {@link org.springframework.data.domain.Score} (within) or a {@link org.springframework.data.domain.Range} of scores + * (between). + *

+ * Vector search must define an index name using the {@link #indexName()} attribute. The index must be created in the + * MongoDB Atlas cluster before executing the query. Any misspelling of the index name will result in returning no + * results. + *

+ * When using pre-filters, you can either define {@link #filter()} or use query derivation to define the pre-filter. + * {@link org.springframework.data.domain.Vector} and distance parameters are considered once these are present. Vector + * search supports sorting and will consider {@link org.springframework.data.domain.Sort} parameters. + * + * @author Mark Paluch + * @since 5.0 + * @see org.springframework.data.domain.Score + * @see org.springframework.data.domain.Vector + * @see org.springframework.data.domain.SearchResults + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.ANNOTATION_TYPE }) +@Documented +@Query +@Hint +public @interface VectorSearch { + + /** + * Configuration whether to use + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN} or + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ENN} for the search. + * + * @return the search type to use. + */ + VectorSearchOperation.SearchType searchType() default VectorSearchOperation.SearchType.DEFAULT; + + /** + * Name of the Atlas Vector Search index to use. Atlas Vector Search doesn't return results if you misspell the index + * name or if the specified index doesn't already exist on the cluster. + * + * @return name of the Atlas Vector Search index to use. + */ + @AliasFor(annotation = Hint.class, value = "indexName") + String indexName(); + + /** + * Indexed vector type field to search. This is defaulted from the domain model using the first Vector property found. + * + * @return an empty String by default. + */ + String path() default ""; + + /** + * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias + * for {@link VectorSearch#filter}. + * + * @return an empty String by default. + */ + @AliasFor(annotation = Query.class) + String value() default ""; + + /** + * Takes a MongoDB JSON (MQL) string defining the pre-filter against indexed fields. Supports Value Expressions. Alias + * for {@link VectorSearch#value}. + * + * @return an empty String by default. + */ + @AliasFor(annotation = Query.class, value = "value") + String filter() default ""; + + /** + * Number of documents to return in the results. This value can't exceed the value of {@link #numCandidates} if you + * specify {@link #numCandidates}. Limit accepts Value Expressions. A Vector Search method cannot define both, + * {@code limit()} and a {@link org.springframework.data.domain.Limit} parameter. Supports Value Expressions. + * + * @return number of documents to return in the results. + */ + String limit() default ""; + + /** + * Number of nearest neighbors to use during the search. Value must be less than or equal to ({@code <=}) + * {@code 10000}. You can't specify a number less than the {@link #limit() number of documents to return}. We + * recommend that you specify a number at least {@code 20} times higher than the {@link #limit() number of documents + * to return} to increase accuracy. + *

+ * This over-request pattern is the recommended way to trade off latency and recall in your ANN searches, and we + * recommend tuning this parameter based on your specific dataset size and query requirements. Required if the query + * uses + * {@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#ANN}/{@link org.springframework.data.mongodb.core.aggregation.VectorSearchOperation.SearchType#DEFAULT}. + * Supports Value Expressions. + * + * @return number of nearest neighbors to use during the search. + */ + String numCandidates() default ""; + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java index 831d21bb44..17c19ad951 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/AotQueryCreator.java @@ -25,8 +25,10 @@ import org.jspecify.annotations.Nullable; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoCustomConversions; @@ -129,6 +131,21 @@ public Range getDistanceRange() { return null; } + @Override + public @Nullable Vector getVector() { + return null; + } + + @Override + public @Nullable Score getScore() { + return null; + } + + @Override + public @Nullable Range getScoreRange() { + return null; + } + @Override public @Nullable Point getGeoNearLocation() { return null; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index 3ef1f5ac91..6d0596815a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -23,7 +23,6 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jspecify.annotations.Nullable; - import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.aggregation.AggregationUpdate; @@ -160,10 +159,11 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor private static boolean backoff(MongoQueryMethod method) { - boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery(); + boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery() + || method.isSearchQuery(); if (skip && logger.isDebugEnabled()) { - logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming or scrolling query" + logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming, search or scrolling query" .formatted(method.getName())); } return skip; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java index f56c2c7a22..596b895ebd 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/AbstractMongoQuery.java @@ -164,7 +164,7 @@ private Query applyAnnotatedReadPreferenceIfPresent(Query query) { } @SuppressWarnings("NullAway") - private MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { + MongoQueryExecution getExecution(ConvertingParameterAccessor accessor, FindWithQuery operation) { if (isDeleteQuery()) { return new DeleteExecution<>(executableRemove, method); @@ -345,7 +345,7 @@ private Document bindParameters(String source, ConvertingParameterAccessor acces * @return never {@literal null}. * @since 3.4 */ - protected ParameterBindingContext prepareBindingContext(String source, ConvertingParameterAccessor accessor) { + protected ParameterBindingContext prepareBindingContext(String source, MongoParameterAccessor accessor) { ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor); return new ParameterBindingContext(accessor::getBindableValue, evaluator); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index d075b67efe..e51d4435a8 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -22,11 +22,14 @@ import java.util.List; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Limit; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; @@ -73,6 +76,11 @@ public PotentiallyConvertingIterator iterator() { return new ConvertingIterator(delegate.iterator()); } + @Override + public Vector getVector() { + return delegate.getVector(); + } + @Override public @Nullable ScrollPosition getScrollPosition() { return delegate.getScrollPosition(); @@ -95,6 +103,16 @@ public Sort getSort() { return getConvertedValue(delegate.getBindableValue(index), null); } + @Override + public @org.jspecify.annotations.Nullable Score getScore() { + return delegate.getScore(); + } + + @Override + public @org.jspecify.annotations.Nullable Range getScoreRange() { + return delegate.getScoreRange(); + } + @Override public @Nullable Range getDistanceRange() { return delegate.getDistanceRange(); @@ -208,7 +226,7 @@ private static Collection asCollection(@Nullable Object source) { if (source instanceof Iterable iterable) { - if(source instanceof Collection collection) { + if (source instanceof Collection collection) { return new ArrayList<>(collection); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java index 00d748f8a9..1b52233eac 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameterAccessor.java @@ -16,6 +16,7 @@ package org.springframework.data.mongodb.repository.query; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Range; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java index cb91ccd8e6..94acef17ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParameters.java @@ -21,8 +21,10 @@ import java.util.List; import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResult; @@ -195,10 +197,6 @@ static int findNearIndexInParameters(Method method) { return index; } - public int getDistanceRangeIndex() { - return -1; - } - /** * Returns the index of the {@link Distance} parameter to be used for max distance in geo queries. * @@ -317,7 +315,8 @@ static class MongoParameter extends Parameter { @Override public boolean isSpecialParameter() { - return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) || isNearParameter() + return super.isSpecialParameter() || Distance.class.isAssignableFrom(getType()) + || Vector.class.isAssignableFrom(getType()) || isNearParameter() || TextCriteria.class.isAssignableFrom(getType()) || Collation.class.isAssignableFrom(getType()); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index 66529dfce9..41cf084d45 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -16,8 +16,10 @@ package org.springframework.data.mongodb.repository.query; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.query.Collation; @@ -55,7 +57,25 @@ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values } @SuppressWarnings("NullAway") - public @Nullable Range getDistanceRange() { + @Override + public Range getScoreRange() { + + MongoParameters mongoParameters = method.getParameters(); + int rangeIndex = mongoParameters.getScoreRangeIndex(); + + if (rangeIndex != -1) { + return getValue(rangeIndex); + } + + int scoreIndex = mongoParameters.getScoreIndex(); + Bound maxDistance = scoreIndex == -1 ? Bound.unbounded() : Bound.inclusive((Score) getScore()); + + return Range.of(Bound.unbounded(), maxDistance); + } + + @SuppressWarnings("NullAway") + @Override + public Range getDistanceRange() { MongoParameters mongoParameters = method.getParameters(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index b8a8c34f48..1f742ec32f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.LogFactory; import org.bson.BsonRegularExpression; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; @@ -72,6 +73,7 @@ public class MongoQueryCreator extends AbstractQueryCreator { private final MongoParameterAccessor accessor; private final MappingContext context; private final boolean isGeoNearQuery; + private final boolean isSearchQuery; /** * Creates a new {@link MongoQueryCreator} from the given {@link PartTree}, {@link ConvertingParameterAccessor} and @@ -81,9 +83,9 @@ public class MongoQueryCreator extends AbstractQueryCreator { * @param accessor * @param context */ - public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, + public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor, MappingContext context) { - this(tree, accessor, context, false); + this(tree, accessor, context, false, false); } /** @@ -94,9 +96,10 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, * @param accessor * @param context * @param isGeoNearQuery + * @param isSearchQuery */ - public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, - MappingContext context, boolean isGeoNearQuery) { + public MongoQueryCreator(PartTree tree, MongoParameterAccessor accessor, + MappingContext context, boolean isGeoNearQuery, boolean isSearchQuery) { super(tree, accessor); @@ -104,6 +107,7 @@ public MongoQueryCreator(PartTree tree, ConvertingParameterAccessor accessor, this.accessor = accessor; this.isGeoNearQuery = isGeoNearQuery; + this.isSearchQuery = isSearchQuery; this.context = context; } @@ -114,6 +118,10 @@ protected Criteria create(Part part, Iterator iterator) { return new Criteria(); } + if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { + return null; + } + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); MongoPersistentProperty property = path.getLeafProperty(); @@ -127,6 +135,10 @@ protected Criteria and(Part part, Criteria base, Iterator iterator) { return create(part, iterator); } + if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { + return base; + } + PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); MongoPersistentProperty property = path.getLeafProperty(); @@ -164,6 +176,15 @@ protected Query complete(@Nullable Criteria criteria, Sort sort) { @SuppressWarnings("NullAway") private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator parameters) { + if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { + + int numberOfArguments = part.getType().getNumberOfArguments(); + for (int i = 0; i < numberOfArguments; i++) { + parameters.next(); + } + return null; + } + Type type = part.getType(); switch (type) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index 01d4e0c63d..d9a91434ce 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -15,14 +15,21 @@ */ package org.springframework.data.mongodb.repository.query; +import java.util.ArrayList; +import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.function.Supplier; +import org.bson.Document; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; import org.springframework.data.domain.Slice; import org.springframework.data.domain.SliceImpl; import org.springframework.data.geo.Distance; @@ -37,6 +44,10 @@ import org.springframework.data.mongodb.core.ExecutableRemoveOperation.ExecutableRemove; import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -210,6 +221,83 @@ private static boolean isListOfGeoResult(TypeInformation returnType) { } } + /** + * {@link MongoQueryExecution} to execute vector search. + * + * @author Mark Paluch + * @since 5.0 + */ + class VectorSearchExecution implements MongoQueryExecution { + + private final MongoOperations operations; + private final MongoQueryMethod method; + private final String collectionName; + private final VectorSearchDelegate.QueryMetadata queryMetadata; + private final List pipeline; + + public VectorSearchExecution(MongoOperations operations, MongoQueryMethod method, String collectionName, + VectorSearchDelegate.QueryMetadata queryMetadata, MongoParameterAccessor accessor) { + + this.operations = operations; + this.collectionName = collectionName; + this.queryMetadata = queryMetadata; + this.method = method; + this.pipeline = queryMetadata.getAggregationPipeline(method, accessor); + } + + @Override + public Object execute(Query query) { + + AggregationResults aggregated = operations.aggregate( + TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline), collectionName, + queryMetadata.outputType()); + + List mappedResults = aggregated.getMappedResults(); + + if (isSearchResult(method.getReturnType())) { + + List rawResults = aggregated.getRawResults().getList("results", org.bson.Document.class); + List> result = new ArrayList<>(mappedResults.size()); + + for (int i = 0; i < mappedResults.size(); i++) { + Document document = rawResults.get(i); + SearchResult searchResult = new SearchResult<>(mappedResults.get(i), + Similarity.raw(document.getDouble("__score__"), queryMetadata.scoringFunction())); + + result.add(searchResult); + } + + return isListOfSearchResult(method.getReturnType()) ? result : new SearchResults<>(result); + } + + return mappedResults; + } + + private static boolean isListOfSearchResult(TypeInformation returnType) { + + if (!Collection.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + + private static boolean isSearchResult(TypeInformation returnType) { + + if (SearchResults.class.isAssignableFrom(returnType.getType())) { + return true; + } + + if (!Iterable.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + } + /** * {@link MongoQueryExecution} to execute geo-near queries with paging. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java index 6c90746fe7..de628d59f4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryMethod.java @@ -35,6 +35,7 @@ import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.Tailable; import org.springframework.data.mongodb.repository.Update; +import org.springframework.data.mongodb.repository.VectorSearch; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.RepositoryMetadata; @@ -414,10 +415,28 @@ private Optional findAnnotatedAggregation() { .filter(it -> !ObjectUtils.isEmpty(it)); } + /** + * Returns whether the method has an annotated vector search. + * + * @return true if {@link VectorSearch} is present. + * @since 5.0 + */ + public boolean hasAnnotatedVectorSearch() { + return findAnnotatedVectorSearch().isPresent(); + } + + Optional findAnnotatedVectorSearch() { + return lookupVectorSearchAnnotation(); + } + Optional lookupAggregationAnnotation() { return doFindAnnotation(Aggregation.class); } + Optional lookupVectorSearchAnnotation() { + return doFindAnnotation(VectorSearch.class); + } + Optional lookupUpdateAnnotation() { return doFindAnnotation(Update.class); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java index 6116cc5534..9682e4971f 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/PartTreeMongoQuery.java @@ -81,7 +81,7 @@ public PartTree getTree() { @SuppressWarnings("NullAway") protected Query createQuery(ConvertingParameterAccessor accessor) { - MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery); + MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, isGeoNearQuery, false); Query query = creator.createQuery(); if (tree.isLimiting()) { @@ -126,7 +126,7 @@ protected Query createQuery(ConvertingParameterAccessor accessor) { @Override protected Query createCountQuery(ConvertingParameterAccessor accessor) { - return new MongoQueryCreator(tree, accessor, context, false).createQuery(); + return new MongoQueryCreator(tree, accessor, context, false, false).createQuery(); } @Override diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index 06f946d745..389f4e871d 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -18,18 +18,26 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; +import java.util.List; + +import org.bson.Document; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.DtoInstantiatingConverter; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Similarity; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.Point; import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; @@ -117,6 +125,57 @@ private boolean isStreamOfGeoResult() { } } + /** + * {@link ReactiveMongoQueryExecution} to execute vector search. + * + * @author Mark Paluch + * @since 5.0 + */ + class VectorSearchExecution implements ReactiveMongoQueryExecution { + + private final ReactiveMongoOperations operations; + private final VectorSearchDelegate.QueryMetadata queryMetadata; + private final List pipeline; + private final boolean returnSearchResult; + + public VectorSearchExecution(ReactiveMongoOperations operations, MongoQueryMethod method, + VectorSearchDelegate.QueryMetadata queryMetadata, MongoParameterAccessor accessor) { + + this.operations = operations; + this.queryMetadata = queryMetadata; + this.pipeline = queryMetadata.getAggregationPipeline(method, accessor); + this.returnSearchResult = isSearchResult(method.getReturnType()); + } + + @Override + public Publisher execute(Query query, Class type, String collection) { + + Flux aggregate = operations + .aggregate(TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline), collection, Document.class); + + return aggregate.map(document -> { + + Object mappedResult = operations.getConverter().read(queryMetadata.outputType(), document); + + return returnSearchResult + ? new SearchResult<>(mappedResult, + Similarity.raw(document.getDouble(queryMetadata.scoreField()), queryMetadata.scoringFunction())) + : mappedResult; + }); + } + + private static boolean isSearchResult(TypeInformation returnType) { + + if (!Publisher.class.isAssignableFrom(returnType.getType())) { + return false; + } + + TypeInformation componentType = returnType.getComponentType(); + return componentType != null && SearchResult.class.equals(componentType.getType()); + } + + } + /** * {@link ReactiveMongoQueryExecution} removing documents matching the query. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java index 4aa773091b..9a17b2b5fc 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactivePartTreeMongoQuery.java @@ -90,7 +90,7 @@ protected Mono createCountQuery(ConvertingParameterAccessor accessor) { @SuppressWarnings("NullAway") private Query createQueryInternal(ConvertingParameterAccessor accessor, boolean isCountQuery) { - MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery); + MongoQueryCreator creator = new MongoQueryCreator(tree, accessor, context, !isCountQuery && isGeoNearQuery, false); Query query = creator.createQuery(); if (isCountQuery) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java new file mode 100644 index 0000000000..1ecbb0235f --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java @@ -0,0 +1,123 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.query; + +import reactor.core.publisher.Mono; + +import org.bson.Document; +import org.reactivestreams.Publisher; + +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.ReactiveMongoOperations; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.spel.ExpressionDependencies; + +/** + * {@link AbstractReactiveMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either + * derived from the method name or provided through {@link VectorSearch#filter()}. + * + * @author Mark Paluch + * @since 5.0 + */ +public class ReactiveVectorSearchAggregation extends AbstractReactiveMongoQuery { + + private final ReactiveMongoOperations mongoOperations; + private final MongoPersistentEntity collectionEntity; + private final ValueExpressionDelegate valueExpressionDelegate; + private final VectorSearchDelegate delegate; + + /** + * Creates a new {@link ReactiveVectorSearchAggregation} from the given {@link MongoQueryMethod} and + * {@link MongoOperations}. + * + * @param method must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public ReactiveVectorSearchAggregation(ReactiveMongoQueryMethod method, ReactiveMongoOperations mongoOperations, + ValueExpressionDelegate delegate) { + + super(method, mongoOperations, delegate); + + this.valueExpressionDelegate = delegate; + if (!method.isSearchQuery() && !method.isCollectionQuery()) { + throw new InvalidMongoDbApiUsageException(String.format( + "Repository Vector Search method '%s' must return either return SearchResults or List but was %s", + method.getName(), method.getReturnType().getType().getSimpleName())); + } + + this.mongoOperations = mongoOperations; + this.collectionEntity = method.getEntityInformation().getCollectionEntity(); + this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate); + } + + @Override + protected Publisher doExecute(ReactiveMongoQueryMethod method, ResultProcessor processor, + ConvertingParameterAccessor accessor, @org.jspecify.annotations.Nullable Class typeToRead) { + + return getParameterBindingCodec().flatMapMany(codec -> { + + String json = delegate.getQueryString(); + ExpressionDependencies dependencies = codec.captureExpressionDependencies(json, accessor::getBindableValue, + valueExpressionDelegate); + + return getValueExpressionEvaluatorLater(dependencies, accessor).flatMapMany(expressionEvaluator -> { + + ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, + expressionEvaluator); + VectorSearchDelegate.QueryMetadata query = delegate.createQuery(expressionEvaluator, processor, accessor, + typeToRead, codec, bindingContext); + + ReactiveMongoQueryExecution.VectorSearchExecution execution = new ReactiveMongoQueryExecution.VectorSearchExecution( + mongoOperations, method, query, accessor); + + return execution.execute(query.query(), Document.class, collectionEntity.getCollection()); + }); + }); + } + + @Override + protected Mono createQuery(ConvertingParameterAccessor accessor) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isLimiting() { + return false; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java new file mode 100644 index 0000000000..9740c0696c --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java @@ -0,0 +1,112 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.query; + +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.lang.Nullable; + +/** + * {@link AbstractMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either derived + * from the method name or provided through {@link VectorSearch#filter()}. + * + * @author Mark Paluch + * @since 5.0 + */ +public class VectorSearchAggregation extends AbstractMongoQuery { + + private final MongoOperations mongoOperations; + private final MongoPersistentEntity collectionEntity; + private final VectorSearchDelegate delegate; + + /** + * Creates a new {@link VectorSearchAggregation} from the given {@link MongoQueryMethod} and {@link MongoOperations}. + * + * @param method must not be {@literal null}. + * @param mongoOperations must not be {@literal null}. + * @param delegate must not be {@literal null}. + */ + public VectorSearchAggregation(MongoQueryMethod method, MongoOperations mongoOperations, + ValueExpressionDelegate delegate) { + + super(method, mongoOperations, delegate); + + if (!method.isSearchQuery() && !method.isCollectionQuery()) { + throw new InvalidMongoDbApiUsageException(String.format( + "Repository Vector Search method '%s' must return either return SearchResults or List but was %s", + method.getName(), method.getReturnType().getType().getSimpleName())); + } + + this.mongoOperations = mongoOperations; + this.collectionEntity = method.getEntityInformation().getCollectionEntity(); + this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate); + } + + @SuppressWarnings("unchecked") + @Override + protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, + @Nullable Class typeToRead) { + + VectorSearchDelegate.QueryMetadata query = createVectorSearchQuery(processor, accessor, typeToRead); + + MongoQueryExecution.VectorSearchExecution execution = new MongoQueryExecution.VectorSearchExecution(mongoOperations, + method, collectionEntity.getCollection(), query, accessor); + + return execution.execute(query.query()); + } + + VectorSearchDelegate.QueryMetadata createVectorSearchQuery(ResultProcessor processor, MongoParameterAccessor accessor, + @Nullable Class typeToRead) { + + ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor); + ParameterBindingContext bindingContext = prepareBindingContext(delegate.getQueryString(), accessor); + + return delegate.createQuery(evaluator, processor, accessor, typeToRead, getParameterBindingCodec(), bindingContext); + } + + @Override + protected Query createQuery(ConvertingParameterAccessor accessor) { + throw new UnsupportedOperationException(); + } + + @Override + protected boolean isCountQuery() { + return false; + } + + @Override + protected boolean isExistsQuery() { + return false; + } + + @Override + protected boolean isDeleteQuery() { + return false; + } + + @Override + protected boolean isLimiting() { + return false; + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java new file mode 100644 index 0000000000..8932b85b1b --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java @@ -0,0 +1,396 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.query; + +import java.util.ArrayList; +import java.util.List; + +import org.bson.Document; +import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.ScoringFunction; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; +import org.springframework.data.expression.ValueExpression; +import org.springframework.data.mapping.PersistentPropertyPath; +import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; +import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty; +import org.springframework.data.mongodb.core.query.BasicQuery; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; +import org.springframework.data.repository.query.ResultProcessor; +import org.springframework.data.repository.query.ValueExpressionDelegate; +import org.springframework.data.repository.query.parser.Part; +import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.StringUtils; + +/** + * Delegate to assemble information about Vector Search queries necessary to run a MongoDB {@code $vectorSearch}. + * + * @author Mark Paluch + */ +class VectorSearchDelegate { + + private final VectorSearchQueryFactory queryFactory; + private final VectorSearchOperation.SearchType searchType; + private final @Nullable Integer numCandidates; + private final @Nullable String numCandidatesExpression; + private final Limit limit; + private final @Nullable String limitExpression; + private final MongoConverter converter; + + public VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, ValueExpressionDelegate delegate) { + + VectorSearch vectorSearch = method.findAnnotatedVectorSearch().orElseThrow(); + this.searchType = vectorSearch.searchType(); + + if (StringUtils.hasText(vectorSearch.numCandidates())) { + + ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.numCandidates()); + + if (expression.isLiteral()) { + numCandidates = Integer.parseInt(vectorSearch.numCandidates()); + numCandidatesExpression = null; + } else { + numCandidates = null; + numCandidatesExpression = vectorSearch.numCandidates(); + } + + } else { + numCandidates = null; + numCandidatesExpression = null; + } + + if (StringUtils.hasText(vectorSearch.limit())) { + + ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.limit()); + + if (expression.isLiteral()) { + limit = Limit.of(Integer.parseInt(vectorSearch.limit())); + limitExpression = null; + } else { + limit = Limit.unlimited(); + limitExpression = vectorSearch.limit(); + } + + } else { + limit = Limit.unlimited(); + limitExpression = null; + } + + this.converter = converter; + + if (StringUtils.hasText(vectorSearch.filter())) { + queryFactory = StringUtils.hasText(vectorSearch.path()) + ? new AnnotatedQueryFactory(vectorSearch.filter(), vectorSearch.path()) + : new AnnotatedQueryFactory(vectorSearch.filter(), method.getEntityInformation().getCollectionEntity()); + } else { + queryFactory = new PartTreeQueryFactory( + new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()), + converter.getMappingContext()); + } + } + + /** + * Create Query Metadata for {@code $vectorSearch}. + */ + public QueryMetadata createQuery(ValueExpressionEvaluator evaluator, ResultProcessor processor, + MongoParameterAccessor accessor, @Nullable Class typeToRead, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + Integer numCandidates = null; + Limit limit; + Class outputType = typeToRead != null ? typeToRead : processor.getReturnedType().getReturnedType(); + VectorSearchInput query = queryFactory.createQuery(accessor, codec, context); + + if (this.limitExpression != null) { + Object value = evaluator.evaluate(this.limitExpression); + limit = value instanceof Limit l ? l : Limit.of(((Number) value).intValue()); + } else if (this.limit.isLimited()) { + limit = this.limit; + } else { + limit = accessor.getLimit(); + } + + if (limit.isLimited()) { + query.query().limit(limit); + } + + if (this.numCandidatesExpression != null) { + numCandidates = ((Number) evaluator.evaluate(this.numCandidatesExpression)).intValue(); + } else if (this.numCandidates != null) { + numCandidates = this.numCandidates; + } else if (query.query().isLimited() && (searchType == VectorSearchOperation.SearchType.ANN + || searchType == VectorSearchOperation.SearchType.DEFAULT)) { + + /* + MongoDB: We recommend that you specify a number at least 20 times higher than the number of documents to return (limit) to increase accuracy. + */ + numCandidates = query.query().getLimit() * 20; + } + + return new QueryMetadata(query.path, "__score__", query.query, searchType, outputType, numCandidates, + getSimilarityFunction(accessor)); + } + + public String getQueryString() { + return queryFactory.getQueryString(); + } + + ScoringFunction getSimilarityFunction(MongoParameterAccessor accessor) { + + Score score = accessor.getScore(); + + if (score != null) { + return score.getFunction(); + } + + Range scoreRange = accessor.getScoreRange(); + + if (scoreRange != null) { + if (scoreRange.getUpperBound().isBounded()) { + return scoreRange.getUpperBound().getValue().get().getFunction(); + } + + if (scoreRange.getLowerBound().isBounded()) { + return scoreRange.getLowerBound().getValue().get().getFunction(); + } + } + + return ScoringFunction.unspecified(); + } + + /** + * Metadata for a Vector Search Aggregation. + * + * @param path + * @param query + * @param searchType + * @param outputType + * @param numCandidates + * @param scoringFunction + */ + public record QueryMetadata(String path, String scoreField, Query query, VectorSearchOperation.SearchType searchType, + Class outputType, @Nullable Integer numCandidates, ScoringFunction scoringFunction) { + + /** + * Create the Aggregation Pipeline. + * + * @param queryMethod + * @param accessor + * @return + */ + public List getAggregationPipeline(MongoQueryMethod queryMethod, + MongoParameterAccessor accessor) { + + Vector vector = accessor.getVector(); + Score score = accessor.getScore(); + Range distance = accessor.getScoreRange(); + Limit limit = Limit.unlimited(); + + if (query.isLimited()) { + limit = Limit.of(query.getLimit()); + } + + List stages = new ArrayList<>(); + VectorSearchOperation $vectorSearch = Aggregation.vectorSearch(queryMethod.getAnnotatedHint()).path(path()) + .vector(vector).limit(limit); + + if (numCandidates() != null) { + $vectorSearch = $vectorSearch.numCandidates(numCandidates()); + } + + $vectorSearch = $vectorSearch.filter(query.getQueryObject()); + $vectorSearch = $vectorSearch.searchType(searchType()); + $vectorSearch = $vectorSearch.withSearchScore(scoreField()); + + if (score != null) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + c.gt(score.getValue()); + }); + } else if (distance.getLowerBound().isBounded() || distance.getUpperBound().isBounded()) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + Range.Bound lower = distance.getLowerBound(); + if (lower.isBounded()) { + double value = lower.getValue().get().getValue(); + if (lower.isInclusive()) { + c.gte(value); + } else { + c.gt(value); + } + } + + Range.Bound upper = distance.getUpperBound(); + if (upper.isBounded()) { + + double value = upper.getValue().get().getValue(); + if (upper.isInclusive()) { + c.lte(value); + } else { + c.lt(value); + } + } + }); + } + + stages.add($vectorSearch); + + if (query.isSorted()) { + // TODO stages.add(Aggregation.sort(query.with())); + } else { + stages.add(Aggregation.sort(Sort.Direction.DESC, "__score__")); + } + + return stages; + } + + } + + /** + * Strategy interface to implement a query factory for the Vector Search pre-filter query. + */ + private interface VectorSearchQueryFactory { + + VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context); + + /** + * @return the underlying query string to determine {@link ParameterBindingContext}. + */ + String getQueryString(); + } + + private static class AnnotatedQueryFactory implements VectorSearchQueryFactory { + + private final String query; + private final String path; + + AnnotatedQueryFactory(String query, String path) { + + this.query = query; + this.path = path; + } + + AnnotatedQueryFactory(String query, MongoPersistentEntity entity) { + + this.query = query; + String path = null; + for (MongoPersistentProperty property : entity) { + if (Vector.class.isAssignableFrom(property.getType())) { + path = property.getFieldName(); + break; + } + } + + if (path == null) { + throw new InvalidMongoDbApiUsageException( + "Cannot find Vector Search property in entity [%s]".formatted(entity.getName())); + } + + this.path = path; + } + + public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + Document queryObject = codec.decode(this.query, context); + Query query = new BasicQuery(queryObject); + + Sort sort = parameterAccessor.getSort(); + if (sort.isSorted()) { + query = query.with(sort); + } + + return new VectorSearchInput(path, query); + } + + @Override + public String getQueryString() { + return this.query; + } + } + + private class PartTreeQueryFactory implements VectorSearchQueryFactory { + + private final String path; + private final PartTree tree; + + @SuppressWarnings("NullableProblems") + PartTreeQueryFactory(PartTree tree, MappingContext context) { + + String path = null; + for (PartTree.OrPart part : tree) { + for (Part p : part) { + if (p.getType() == Part.Type.SIMPLE_PROPERTY || p.getType() == Part.Type.NEAR + || p.getType() == Part.Type.WITHIN || p.getType() == Part.Type.BETWEEN) { + PersistentPropertyPath ppp = context.getPersistentPropertyPath(p.getProperty()); + MongoPersistentProperty property = ppp.getLeafProperty(); + + if (Vector.class.isAssignableFrom(property.getType())) { + path = p.getProperty().toDotPath(); + break; + } + } + } + } + + if (path == null) { + throw new InvalidMongoDbApiUsageException( + "No Simple Property/Near/Within/Between part found for a Vector property"); + } + + this.path = path; + this.tree = tree; + } + + public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, + ParameterBindingContext context) { + + MongoQueryCreator creator = new MongoQueryCreator(tree, parameterAccessor, converter.getMappingContext(), + false, true); + + Query query = creator.createQuery(parameterAccessor.getSort()); + + if (tree.isLimiting()) { + query.limit(tree.getMaxResults()); + } + + return new VectorSearchInput(path, query); + } + + @Override + public String getQueryString() { + return ""; + } + } + + private record VectorSearchInput(String path, Query query) { + + } + +} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 4ff89c9fdb..07268cce2c 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -32,6 +32,7 @@ import org.springframework.data.mongodb.repository.query.PartTreeMongoQuery; import org.springframework.data.mongodb.repository.query.StringBasedAggregation; import org.springframework.data.mongodb.repository.query.StringBasedMongoQuery; +import org.springframework.data.mongodb.repository.query.VectorSearchAggregation; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; @@ -176,6 +177,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); return new StringBasedMongoQuery(namedQuery, queryMethod, operations, expressionSupport); + } else if (queryMethod.hasAnnotatedVectorSearch()) { + return new VectorSearchAggregation(queryMethod, operations, expressionSupport); } else if (queryMethod.hasAnnotatedAggregation()) { return new StringBasedAggregation(queryMethod, operations, expressionSupport); } else if (queryMethod.hasAnnotatedQuery()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index c113c70a5b..1b5c218ce7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -32,6 +32,7 @@ import org.springframework.data.mongodb.repository.query.ReactivePartTreeMongoQuery; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedAggregation; import org.springframework.data.mongodb.repository.query.ReactiveStringBasedMongoQuery; +import org.springframework.data.mongodb.repository.query.ReactiveVectorSearchAggregation; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.repository.core.NamedQueries; import org.springframework.data.repository.core.RepositoryInformation; @@ -179,6 +180,8 @@ public RepositoryQuery resolveQuery(Method method, RepositoryMetadata metadata, if (namedQueries.hasQuery(namedQueryName)) { String namedQuery = namedQueries.getQuery(namedQueryName); return new ReactiveStringBasedMongoQuery(namedQuery, queryMethod, operations, delegate); + } else if (queryMethod.hasAnnotatedVectorSearch()) { + return new ReactiveVectorSearchAggregation(queryMethod, operations, delegate); } else if (queryMethod.hasAnnotatedAggregation()) { return new ReactiveStringBasedAggregation(queryMethod, operations, delegate); } else if (queryMethod.hasAnnotatedQuery()) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java index 1b9aba1ba0..5f66e61bdc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/aggregation/GeoNearOperationUnitTests.java @@ -70,7 +70,7 @@ public void rendersNearQueryWithKeyCorrectly() { @Test // DATAMONGO-2264 public void rendersMaxDistanceCorrectly() { - NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(new Distance(30.0)); + NearQuery query = NearQuery.near(10.0, 20.0).maxDistance(Distance.of(30.0)); assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT)) .containsExactly($geoNear().near(10.0, 20.0).maxDistance(30.0).doc()); @@ -79,7 +79,7 @@ public void rendersMaxDistanceCorrectly() { @Test // DATAMONGO-2264 public void rendersMinDistanceCorrectly() { - NearQuery query = NearQuery.near(10.0, 20.0).minDistance(new Distance(30.0)); + NearQuery query = NearQuery.near(10.0, 20.0).minDistance(Distance.of(30.0)); assertThat(new GeoNearOperation(query, "distance").toPipelineStages(Aggregation.DEFAULT_CONTEXT)) .containsExactly($geoNear().near(10.0, 20.0).minDistance(30.0).doc()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java index 7fb664b00c..84a494f9d8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/GeoConvertersUnitTests.java @@ -69,7 +69,7 @@ public void convertsCircleToDocumentAndBackCorrectlyNeutralDistance() { @Test // DATAMONGO-858 public void convertsCircleToDocumentAndBackCorrectlyMilesDistance() { - Distance radius = new Distance(3, Metrics.MILES); + Distance radius = Distance.of(3, Metrics.MILES); Circle circle = new Circle(new Point(1, 2), radius); Document document = CircleToDocumentConverter.INSTANCE.convert(circle); @@ -106,7 +106,7 @@ public void convertsSphereToDocumentAndBackCorrectlyWithNeutralDistance() { @Test // DATAMONGO-858 public void convertsSphereToDocumentAndBackCorrectlyWithKilometerDistance() { - Distance radius = new Distance(3, Metrics.KILOMETERS); + Distance radius = Distance.of(3, Metrics.KILOMETERS); Sphere sphere = new Sphere(new Point(1, 2), radius); Document document = SphereToDocumentConverter.INSTANCE.convert(sphere); @@ -160,7 +160,7 @@ public void convertsCircleCorrectlyWhenUsingNonDoubleForCoordinates() { circle.put("radius", 3L); assertThat(DocumentToCircleConverter.INSTANCE.convert(circle)) - .isEqualTo(new Circle(new Point(1, 2), new Distance(3))); + .isEqualTo(new Circle(new Point(1, 2), Distance.of(3))); } @Test // DATAMONGO-1607 @@ -171,7 +171,7 @@ public void convertsSphereCorrectlyWhenUsingNonDoubleForCoordinates() { sphere.put("radius", 3L); assertThat(DocumentToSphereConverter.INSTANCE.convert(sphere)) - .isEqualTo(new Sphere(new Point(1, 2), new Distance(3))); + .isEqualTo(new Sphere(new Point(1, 2), Distance.of(3))); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java index 5bd7e06b97..6f1c7439c0 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/convert/MappingMongoConverterUnitTests.java @@ -1626,7 +1626,7 @@ void shouldWriteEntityWithGeoSphereCorrectly() { void shouldWriteEntityWithGeoSphereWithMetricDistanceCorrectly() { ClassWithGeoSphere object = new ClassWithGeoSphere(); - Sphere sphere = new Sphere(new Point(1, 2), new Distance(3, Metrics.KILOMETERS)); + Sphere sphere = new Sphere(new Point(1, 2), Distance.of(3, Metrics.KILOMETERS)); Distance radius = sphere.getRadius(); object.sphere = sphere; @@ -4082,8 +4082,7 @@ static class WithExplicitTargetTypes { @Field(targetType = FieldType.DECIMAL128) // BigDecimal bigDecimal; - @Field(targetType = FieldType.DECIMAL128) - BigInteger bigInteger; + @Field(targetType = FieldType.DECIMAL128) BigInteger bigInteger; @Field(targetType = FieldType.INT64) // Date dateAsLong; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java index 3a9140d34c..1774c36493 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatial2DSphereTests.java @@ -23,9 +23,9 @@ import java.util.List; import org.junit.Test; + import org.springframework.data.domain.Sort.Direction; import org.springframework.data.geo.GeoResults; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.Venue; @@ -67,7 +67,7 @@ public void geoNearWithMinDistance() { GeoResults result = template.geoNear(geoNear, Venue.class); assertThat(result.getContent().size()).isNotEqualTo(0); - assertThat(result.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(result.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-1110 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java index bbdad047f2..fdfa840d58 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/MetricConversionUnitTests.java @@ -17,6 +17,7 @@ package org.springframework.data.mongodb.core.query; import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.data.Offset.*; import static org.assertj.core.data.Offset.offset; import org.junit.jupiter.api.Test; @@ -34,7 +35,7 @@ public class MetricConversionUnitTests { @Test // DATAMONGO-1348 public void shouldConvertMilesToMeters() { - Distance distance = new Distance(1, Metrics.MILES); + Distance distance = Distance.of(1, Metrics.MILES); double distanceInMeters = MetricConversion.getDistanceInMeters(distance); assertThat(distanceInMeters).isCloseTo(1609.3438343d, offset(0.000000001)); @@ -43,7 +44,7 @@ public void shouldConvertMilesToMeters() { @Test // DATAMONGO-1348 public void shouldConvertKilometersToMeters() { - Distance distance = new Distance(1, Metrics.KILOMETERS); + Distance distance = Distance.of(1, Metrics.KILOMETERS); double distanceInMeters = MetricConversion.getDistanceInMeters(distance); assertThat(distanceInMeters).isCloseTo(1000, offset(0.000000001)); @@ -72,11 +73,13 @@ public void shouldCalculateMetersToMilesMultiplier() { @Test // GH-4004 void shouldConvertKilometersToRadians/* on an earth like sphere with r=6378.137km */() { - assertThat(MetricConversion.toRadians(new Distance(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, offset(0.000000001)); + assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.KILOMETERS))).isCloseTo(0.000156785594d, + offset(0.000000001)); } @Test // GH-4004 void shouldConvertMilesToRadians/* on an earth like sphere with r=6378.137km */() { - assertThat(MetricConversion.toRadians(new Distance(1, Metrics.MILES))).isCloseTo(0.000252321328d, offset(0.000000001)); + assertThat(MetricConversion.toRadians(Distance.of(1, Metrics.MILES))).isCloseTo(0.000252321328d, + offset(0.000000001)); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java index f4e3d26eb1..2b600988db 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/query/NearQueryUnitTests.java @@ -21,10 +21,10 @@ import java.math.RoundingMode; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.DocumentTestUtils; @@ -44,7 +44,7 @@ */ public class NearQueryUnitTests { - private static final Distance ONE_FIFTY_KILOMETERS = new Distance(150, Metrics.KILOMETERS); + private static final Distance ONE_FIFTY_KILOMETERS = Distance.of(150, Metrics.KILOMETERS); @Test public void rejectsNullPoint() { @@ -57,7 +57,7 @@ public void settingUpNearWithMetricRecalculatesDistance() { NearQuery query = NearQuery.near(2.5, 2.5, Metrics.KILOMETERS).maxDistance(150); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(query.isSpherical()).isTrue(); } @@ -68,27 +68,27 @@ public void settingMetricRecalculatesMaxDistance() { query.inMiles(); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); } @Test public void configuresResultMetricCorrectly() { NearQuery query = NearQuery.near(2.5, 2.1); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.NEUTRAL); + assertThat(query.getMetric()).isEqualTo(Metrics.NEUTRAL); query = query.maxDistance(ONE_FIFTY_KILOMETERS); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(query.getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); assertThat(query.isSpherical()).isTrue(); query = query.in(Metrics.MILES); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); assertThat(query.getMaxDistance()).isEqualTo(ONE_FIFTY_KILOMETERS); assertThat(query.isSpherical()).isTrue(); - query = query.maxDistance(new Distance(200, Metrics.KILOMETERS)); - assertThat(query.getMetric()).isEqualTo((Metric) Metrics.MILES); + query = query.maxDistance(Distance.of(200, Metrics.KILOMETERS)); + assertThat(query.getMetric()).isEqualTo(Metrics.MILES); } @Test // DATAMONGO-445, DATAMONGO-2264 @@ -200,7 +200,7 @@ public void shouldUseMetersForGeoJsonData() { public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.KILOMETERS)); + query.maxDistance(Distance.of(1, Metrics.KILOMETERS)); assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.001D); } @@ -209,7 +209,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInKilometers() { public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.MILES)); + query.maxDistance(Distance.of(1, Metrics.MILES)); assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier", 0.00062137D); @@ -219,7 +219,7 @@ public void shouldUseMetersForGeoJsonDataWhenDistanceInMiles() { public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.MILES)).in(Metrics.KILOMETERS); + query.maxDistance(Distance.of(1, Metrics.MILES)).in(Metrics.KILOMETERS); assertThat(query.toDocument()).containsEntry("maxDistance", 1609.3438343D).containsEntry("distanceMultiplier", 0.001D); @@ -229,7 +229,7 @@ public void shouldUseKilometersForDistanceWhenMaxDistanceInMiles() { public void shouldUseMilesForDistanceWhenMaxDistanceInKilometers() { NearQuery query = NearQuery.near(new GeoJsonPoint(27.987901, 86.9165379)); - query.maxDistance(new Distance(1, Metrics.KILOMETERS)).in(Metrics.MILES); + query.maxDistance(Distance.of(1, Metrics.KILOMETERS)).in(Metrics.MILES); assertThat(query.toDocument()).containsEntry("maxDistance", 1000D).containsEntry("distanceMultiplier", 0.00062137D); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java index 3f2e60f4c4..c2cb6cacf8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/AbstractPersonRepositoryIntegrationTests.java @@ -38,6 +38,7 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DuplicateKeyException; import org.springframework.dao.IncorrectResultSizeDataAccessException; @@ -49,7 +50,6 @@ import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; import org.springframework.data.geo.GeoResults; -import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; import org.springframework.data.geo.Polygon; @@ -458,7 +458,7 @@ void executesGeoNearQueryForResultsCorrectly() { repository.save(dave); GeoResults results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS)); + Distance.of(2000, Metrics.KILOMETERS)); assertThat(results.getContent()).isNotEmpty(); } @@ -470,11 +470,11 @@ void executesGeoPageQueryForResultsCorrectly() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 20)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 20)); assertThat(results.getContent()).isNotEmpty(); // DATAMONGO-607 - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-323 @@ -634,13 +634,13 @@ void executesGeoPageQueryForWithPageRequestForPageInBetween() { repository.saveAll(Arrays.asList(dave, oliver, carter, boyd, leroi)); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(2); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isFalse(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); assertThat(results.getAverageDistance().getNormalizedValue()).isEqualTo(0.0); } @@ -656,12 +656,12 @@ void executesGeoPageQueryForWithPageRequestForPageAtTheEnd() { repository.saveAll(Arrays.asList(dave, oliver, carter)); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(1); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-445 @@ -672,13 +672,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElement() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(0, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(0, 2)); assertThat(results.getContent()).isNotEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(1); assertThat(results.isFirst()).isTrue(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-445 @@ -688,13 +688,13 @@ void executesGeoPageQueryForWithPageRequestForJustOneElementEmptyPage() { repository.save(dave); GeoPage results = repository.findByLocationNear(new Point(-73.99, 40.73), - new Distance(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); + Distance.of(2000, Metrics.KILOMETERS), PageRequest.of(1, 2)); assertThat(results.getContent()).isEmpty(); assertThat(results.getNumberOfElements()).isEqualTo(0); assertThat(results.isFirst()).isFalse(); assertThat(results.isLast()).isTrue(); - assertThat(results.getAverageDistance().getMetric()).isEqualTo((Metric) Metrics.KILOMETERS); + assertThat(results.getAverageDistance().getMetric()).isEqualTo(Metrics.KILOMETERS); } @Test // DATAMONGO-1608 @@ -1117,7 +1117,7 @@ void executesGeoNearQueryForResultsCorrectlyWhenGivenMinAndMaxDistance() { dave.setLocation(point); repository.save(dave); - Range range = Distance.between(new Distance(0.01, KILOMETERS), new Distance(2000, KILOMETERS)); + Range range = Distance.between(Distance.of(0.01, KILOMETERS), Distance.of(2000, KILOMETERS)); GeoResults results = repository.findPersonByLocationNear(new Point(-73.99, 40.73), range); assertThat(results.getContent()).isNotEmpty(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java index 93a293ecff..1f4f682ebc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/PersonRepository.java @@ -24,6 +24,7 @@ import java.util.stream.Stream; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java index 1a481b49ed..2a76c0ba6c 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveMongoRepositoryTests.java @@ -20,6 +20,7 @@ import static org.springframework.data.domain.Sort.Direction.*; import static org.springframework.data.mongodb.core.query.Criteria.*; import static org.springframework.data.mongodb.core.query.Query.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import reactor.core.Disposable; @@ -40,6 +41,7 @@ import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.api.extension.ExtendWith; import org.reactivestreams.Publisher; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; @@ -353,7 +355,7 @@ void findsPeopleGeoresultByLocationWithinBox() { repository.save(dave).as(StepVerifier::create).expectNextCount(1).verifyComplete(); repository.findByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> { + Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create).consumeNextWith(actual -> { assertThat(actual.getDistance().getValue()).isCloseTo(1, offset(1d)); assertThat(actual.getContent()).isEqualTo(dave); @@ -372,7 +374,7 @@ void findsPeoplePageableGeoresultByLocationWithinBox() throws InterruptedExcepti Thread.sleep(500); repository.findByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS), // + Distance.of(2000, Metrics.KILOMETERS), // PageRequest.of(0, 10)).as(StepVerifier::create) // .consumeNextWith(actual -> { @@ -393,7 +395,7 @@ void findsPeopleByLocationWithinBox() throws InterruptedException { Thread.sleep(500); repository.findPersonByLocationNear(new Point(-73.99, 40.73), // - new Distance(2000, Metrics.KILOMETERS)).as(StepVerifier::create) // + Distance.of(2000, Metrics.KILOMETERS)).as(StepVerifier::create) // .expectNext(dave) // .verifyComplete(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java new file mode 100644 index 0000000000..14a4749c8a --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/ReactiveVectorSearchTests.java @@ -0,0 +1,224 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import static org.assertj.core.api.Assertions.*; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.ReactiveMongoTemplate; +import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory; +import org.springframework.data.mongodb.core.TestMongoConfiguration; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.index.VectorIndex; +import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction; +import org.springframework.data.mongodb.repository.config.EnableReactiveMongoRepositories; +import org.springframework.data.mongodb.test.util.AtlasContainer; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Integration tests using reactive Vector Search and Vector Indexes through local MongoDB Atlas. + * + * @author Mark Paluch + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig(classes = { ReactiveVectorSearchTests.Config.class }) +public class ReactiveVectorSearchTests { + + Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true); + private static final String COLLECTION_NAME = "collection-1"; + + static MongoClient client; + static MongoTestTemplate template; + + @Autowired ReactiveVectorSearchRepository repository; + + @EnableReactiveMongoRepositories( + includeFilters = { + @ComponentScan.Filter(value = ReactiveVectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) }, + considerNestedRepositories = true) + static class Config extends TestMongoConfiguration { + + @Override + public String getDatabaseName() { + return "vector-search-tests"; + } + + @Override + public MongoClient mongoClient() { + atlasLocal.start(); + return MongoClients.create(atlasLocal.getConnectionString()); + } + + @Bean + public com.mongodb.reactivestreams.client.MongoClient reactiveMongoClient() { + atlasLocal.start(); + return com.mongodb.reactivestreams.client.MongoClients.create(atlasLocal.getConnectionString()); + } + + @Bean + ReactiveMongoTemplate reactiveMongoTemplate(MappingMongoConverter mongoConverter) { + return new ReactiveMongoTemplate(new SimpleReactiveMongoDatabaseFactory(reactiveMongoClient(), getDatabaseName()), + mongoConverter); + } + } + + @BeforeAll + static void beforeAll() throws InterruptedException { + atlasLocal.start(); + + System.out.println(atlasLocal.getConnectionString()); + client = MongoClients.create(atlasLocal.getConnectionString()); + template = new MongoTestTemplate(client, "vector-search-tests"); + + template.remove(WithVectorFields.class).all(); + initDocuments(); + initIndexes(); + + Thread.sleep(500); // just wait a little or the index will be broken + } + + @Test + void shouldSearchEnnWithAnnotatedFilter() { + + Flux> results = repository.searchAnnotated("de", VECTOR, Score.of(0.4), + Limit.of(10)); + + results.as(StepVerifier::create).consumeNextWith(actual -> { + assertThat(actual.getScore().getValue()).isGreaterThan(0.4); + assertThat(actual.getScore()).isInstanceOf(Similarity.class); + + }).expectNextCount(2).verifyComplete(); + } + + @Test + void shouldSearchEnnWithDerivedFilter() { + + Flux results = repository.searchByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10)); + + results.as(StepVerifier::create).consumeNextWith(actual -> assertThat(actual).isInstanceOf(WithVectorFields.class)) + .expectNextCount(2).verifyComplete(); + } + + static void initDocuments() { + + WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f)); + WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f)); + WithVectorFields w3 = new WithVectorFields("en", "three", + Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f)); + WithVectorFields w4 = new WithVectorFields("de", "four", + Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f)); + + template.insertAll(List.of(w1, w2, w3, w4)); + } + + static void initIndexes() { + + VectorIndex cosIndex = new VectorIndex("cos-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + + VectorIndex euclideanIndex = new VectorIndex("euc-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country"); + + VectorIndex inner = new VectorIndex("ip-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(inner); + template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, inner.getName()); + } + + interface ReactiveVectorSearchRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}", + searchType = VectorSearchOperation.SearchType.ANN) + Flux> searchAnnotated(String country, Vector vector, Score distance, Limit limit); + + @VectorSearch(indexName = "cos-index") + Flux searchByCountryAndEmbeddingNear(String country, Vector vector, Limit limit); + + } + + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + public WithVectorFields(String country, String description, Vector embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public String getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description + + '\'' + '}'; + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java new file mode 100644 index 0000000000..028a6926fb --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import static org.assertj.core.api.Assertions.*; + +import java.util.List; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.FilterType; +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResult; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Similarity; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.TestMongoConfiguration; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.index.VectorIndex; +import org.springframework.data.mongodb.core.index.VectorIndex.SimilarityFunction; +import org.springframework.data.mongodb.repository.config.EnableMongoRepositories; +import org.springframework.data.mongodb.test.util.AtlasContainer; +import org.springframework.data.mongodb.test.util.MongoTestTemplate; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.mongodb.MongoDBAtlasLocalContainer; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Integration tests using Vector Search and Vector Indexes through local MongoDB Atlas. + * + * @author Christoph Strobl + * @author Mark Paluch + */ +@Testcontainers(disabledWithoutDocker = true) +@SpringJUnitConfig(classes = { VectorSearchTests.Config.class }) +public class VectorSearchTests { + + Vector VECTOR = Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f); + + private static final MongoDBAtlasLocalContainer atlasLocal = AtlasContainer.bestMatch().withReuse(true); + private static final String COLLECTION_NAME = "collection-1"; + + static MongoClient client; + static MongoTestTemplate template; + + @Autowired VectorSearchRepository repository; + + @EnableMongoRepositories( + includeFilters = { + @ComponentScan.Filter(value = VectorSearchRepository.class, type = FilterType.ASSIGNABLE_TYPE) }, + considerNestedRepositories = true) + static class Config extends TestMongoConfiguration { + + @Override + public String getDatabaseName() { + return "vector-search-tests"; + } + + @Override + public MongoClient mongoClient() { + atlasLocal.start(); + return MongoClients.create(atlasLocal.getConnectionString()); + } + } + + @BeforeAll + static void beforeAll() throws InterruptedException { + atlasLocal.start(); + + System.out.println(atlasLocal.getConnectionString()); + client = MongoClients.create(atlasLocal.getConnectionString()); + template = new MongoTestTemplate(client, "vector-search-tests"); + + template.remove(WithVectorFields.class).all(); + initDocuments(); + initIndexes(); + + Thread.sleep(500); // just wait a little or the index will be broken + } + + @Test + void shouldSearchEnnWithAnnotatedFilter() { + + SearchResults results = repository.searchAnnotated("de", VECTOR, + Score.of(0.4), Limit.of(10)); + + assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class); + assertThat(results).hasSize(3); + } + + @Test + void shouldSearchEnnWithDerivedFilter() { + + SearchResults results = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, + Similarity.of(0.98), + Limit.of(10)); + + assertThat(results).extracting(SearchResult::getScore).hasOnlyElementsOfType(Similarity.class); + assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry) + .containsOnly("de", "de"); + + assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription) + .containsExactlyInAnyOrder("two", "one"); + } + + @Test + void shouldSearchEnnWithDerivedFilterWithoutScore() { + + SearchResults de = repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, + Similarity.of(0.4), Limit.of(10)); + + assertThat(de).hasSizeGreaterThanOrEqualTo(2); + + assertThat(repository.searchCosineByCountryAndEmbeddingNear("de", VECTOR, Similarity.of(0.999), Limit.of(10))) + .hasSize(1); + } + + @Test + void shouldSearchAsListEnnWithDerivedFilterWithoutScore() { + + List de = repository.searchAsListByCountryAndEmbeddingNear("de", VECTOR, Limit.of(10)); + + assertThat(de).hasOnlyElementsOfType(WithVectorFields.class); + } + + @Test + void shouldSearchEuclideanWithDerivedFilter() { + + SearchResults results = repository.searchEuclideanByCountryAndEmbeddingNear("de", VECTOR, + Limit.of(2)); + + assertThat(results).hasSize(2).extracting(SearchResult::getContent).extracting(WithVectorFields::getCountry) + .containsOnly("de", "de"); + + assertThat(results).extracting(SearchResult::getContent).extracting(WithVectorFields::getDescription) + .containsExactlyInAnyOrder("two", "one"); + } + + @Test + void shouldSearchEnnWithDerivedFilterWithin() { + + SearchResults results = repository.searchByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.between(0.93, 0.98)); + + assertThat(results).hasSize(1); + for (SearchResult result : results) { + assertThat(result.getScore().getValue()).isBetween(0.93, 0.98); + } + } + + @Test + void shouldSearchEnnWithDerivedAndLimitedFilterWithin() { + + SearchResults results = repository.searchTop1ByCountryAndEmbeddingWithin("de", VECTOR, + Similarity.between(0.8, 1)); + + assertThat(results).hasSize(1); + + for (SearchResult result : results) { + assertThat(result.getScore().getValue()).isBetween(0.8, 1.0); + } + } + + static void initDocuments() { + + WithVectorFields w1 = new WithVectorFields("de", "one", Vector.of(0.1001f, 0.22345f, 0.33456f, 0.44567f, 0.55678f)); + WithVectorFields w2 = new WithVectorFields("de", "two", Vector.of(0.2001f, 0.32345f, 0.43456f, 0.54567f, 0.65678f)); + WithVectorFields w3 = new WithVectorFields("en", "three", + Vector.of(0.9001f, 0.82345f, 0.73456f, 0.64567f, 0.55678f)); + WithVectorFields w4 = new WithVectorFields("de", "four", + Vector.of(0.9001f, 0.92345f, 0.93456f, 0.94567f, 0.95678f)); + + template.insertAll(List.of(w1, w2, w3, w4)); + } + + static void initIndexes() { + + VectorIndex cosIndex = new VectorIndex("cos-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.COSINE).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + + VectorIndex euclideanIndex = new VectorIndex("euc-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.EUCLIDEAN).dimensions(5)).addFilter("country"); + + VectorIndex inner = new VectorIndex("ip-index") + .addVector("embedding", it -> it.similarity(SimilarityFunction.DOT_PRODUCT).dimensions(5)).addFilter("country"); + + template.searchIndexOps(WithVectorFields.class).createIndex(cosIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(euclideanIndex); + template.searchIndexOps(WithVectorFields.class).createIndex(inner); + template.awaitIndexCreation(WithVectorFields.class, cosIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, euclideanIndex.getName()); + template.awaitIndexCreation(WithVectorFields.class, inner.getName()); + } + + interface VectorSearchRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", numCandidates = "#{10+10}", + searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchAnnotated(String country, Vector vector, + Score distance, Limit limit); + + @VectorSearch(indexName = "cos-index") + SearchResults searchCosineByCountryAndEmbeddingNear(String country, Vector vector, + Score similarity, Limit limit); + + @VectorSearch(indexName = "cos-index") + List searchAsListByCountryAndEmbeddingNear(String country, Vector vector, Limit limit); + + @VectorSearch(indexName = "euc-index") + SearchResults searchEuclideanByCountryAndEmbeddingNear(String country, Vector vector, + Limit limit); + + @VectorSearch(indexName = "cos-index", limit = "10") + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector vector, + Range distance); + + @VectorSearch(indexName = "cos-index") + SearchResults searchTop1ByCountryAndEmbeddingWithin(String country, Vector vector, + Range distance); + + } + + @org.springframework.data.mongodb.core.mapping.Document(COLLECTION_NAME) + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + public WithVectorFields(String country, String description, Vector embedding) { + this.country = country; + this.description = description; + this.embedding = embedding; + } + + public String getId() { + return id; + } + + public String getCountry() { + return country; + } + + public String getDescription() { + return description; + } + + public Vector getEmbedding() { + return embedding; + } + + @Override + public String toString() { + return "WithVectorFields{" + "id='" + id + '\'' + ", country='" + country + '\'' + ", description='" + description + + '\'' + '}'; + } + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java index 1c856394d8..f0ffebde20 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessorUnitTests.java @@ -22,8 +22,10 @@ import org.bson.Document; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; @@ -45,15 +47,15 @@ * @author Oliver Gierke * @author Christoph Strobl */ -public class MongoParametersParameterAccessorUnitTests { +class MongoParametersParameterAccessorUnitTests { - Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); - RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); - MongoMappingContext context = new MongoMappingContext(); - ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS); + private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); + private MongoMappingContext context = new MongoMappingContext(); + private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); @Test - public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException { + void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -64,7 +66,7 @@ public void returnsUnboundedForDistanceIfNoneAvailable() throws NoSuchMethodExce } @Test - public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException { + void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -75,7 +77,7 @@ public void returnsDistanceIfAvailable() throws NoSuchMethodException, SecurityE } @Test // DATAMONGO-973 - public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException { + void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Distance.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -86,7 +88,7 @@ public void shouldReturnAsFullTextStringWhenNoneDefinedForMethod() throws NoSuch } @Test // DATAMONGO-973 - public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException { + void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, TextCriteria.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -98,13 +100,13 @@ public void shouldProperlyConvertTextCriteria() throws NoSuchMethodException, Se } @Test // DATAMONGO-1110 - public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException { + void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class, Range.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); - Distance min = new Distance(10, Metrics.KILOMETERS); - Distance max = new Distance(20, Metrics.KILOMETERS); + Distance min = Distance.of(10, Metrics.KILOMETERS); + Distance max = Distance.of(20, Metrics.KILOMETERS); MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, new Object[] { new Point(10, 20), Distance.between(min, max) }); @@ -116,7 +118,7 @@ public void shouldDetectMinAndMaxDistance() throws NoSuchMethodException, Securi } @Test // DATAMONGO-1854 - public void shouldDetectCollation() throws NoSuchMethodException, SecurityException { + void shouldDetectCollation() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Collation.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -129,7 +131,7 @@ public void shouldDetectCollation() throws NoSuchMethodException, SecurityExcept } @Test // GH-2107 - public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException { + void shouldReturnUpdateIfPresent() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findAndModifyByFirstname", String.class, UpdateDefinition.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -142,7 +144,7 @@ public void shouldReturnUpdateIfPresent() throws NoSuchMethodException, Security } @Test // GH-2107 - public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException { + void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, SecurityException { Method method = PersonRepository.class.getMethod("findByLocationNear", Point.class); MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); @@ -153,6 +155,23 @@ public void shouldReturnNullIfNoUpdatePresent() throws NoSuchMethodException, Se assertThat(accessor.getUpdate()).isNull(); } + @Test // GH- + void shouldReturnRangeFromScore() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("findByFirstname", String.class, Score.class); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, metadata, factory, context); + + MongoParameterAccessor accessor = new MongoParametersParameterAccessor(queryMethod, + new Object[] { "foo", Score.of(1) }); + + Range scoreRange = accessor.getScoreRange(); + + assertThat(scoreRange).isNotNull(); + assertThat(scoreRange.getLowerBound().isBounded()).isFalse(); + assertThat(scoreRange.getUpperBound().isBounded()).isTrue(); + assertThat(scoreRange.getUpperBound().getValue()).contains(Score.of(1)); + } + interface PersonRepository extends Repository { List findByLocationNear(Point point); @@ -165,6 +184,8 @@ interface PersonRepository extends Repository { List findByFirstname(String firstname, Collation collation); + List findByFirstname(String firstname, Score score); + List findAndModifyByFirstname(String firstname, UpdateDefinition update); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java index 93674e23fc..fc1ffb971e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoParametersUnitTests.java @@ -27,6 +27,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; @@ -43,6 +45,7 @@ * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @ExtendWith(MockitoExtension.class) class MongoParametersUnitTests { @@ -184,6 +187,21 @@ void shouldReturnInvalidIndexIfUpdateDoesNotExist() throws NoSuchMethodException assertThat(parameters.getUpdateIndex()).isEqualTo(-1); } + @Test // GH-2107 + void shouldOmitVector() throws NoSuchMethodException, SecurityException { + + Method method = PersonRepository.class.getMethod("shouldOmitVector", Vector.class, Score.class, + Range.class, String.class); + MongoParameters parameters = new MongoParameters(ParametersSource.of(method), false); + + assertThat(parameters.getVectorIndex()).isEqualTo(0); + assertThat(parameters.getScoreIndex()).isEqualTo(1); + assertThat(parameters.getScoreRangeIndex()).isEqualTo(2); + + MongoParameters bindableParameters = parameters.getBindableParameters(); + assertThat(bindableParameters).hasSize(3); + } + interface PersonRepository { List findByLocationNear(Point point, Distance distance); @@ -205,5 +223,8 @@ interface PersonRepository { List findByText(String text, Collation collation); List findAndModifyByFirstname(String firstname, UpdateDefinition update, Pageable page); + + List shouldOmitVector(Vector vector, Score distance, Range range, + String country); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java index 609e0a0018..55e3df6b43 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryCreatorUnitTests.java @@ -29,6 +29,7 @@ import org.bson.types.ObjectId; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Distance; @@ -120,7 +121,7 @@ void createsIsNullQueryCorrectly() { void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception { Point point = new Point(10, 20); - Distance distance = new Distance(2.5, Metrics.KILOMETERS); + Distance distance = Distance.of(2.5, Metrics.KILOMETERS); Query query = query( where("location").nearSphere(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); @@ -131,7 +132,7 @@ void bindsMetricDistanceParameterToNearSphereCorrectly() throws Exception { void bindsDistanceParameterToNearCorrectly() throws Exception { Point point = new Point(10, 20); - Distance distance = new Distance(2.5); + Distance distance = Distance.of(2.5); Query query = query( where("location").near(point).maxDistance(distance.getNormalizedValue()).and("firstname").is("Dave")); @@ -405,7 +406,7 @@ void shouldCreateRegexWhenUsingNotContainsOnStringProperty() { void createsNonSphericalNearForDistanceWithDefaultMetric() { Point point = new Point(1.0, 1.0); - Distance distance = new Distance(1.0); + Distance distance = Distance.of(1.0); PartTree tree = new PartTree("findByLocationNear", Venue.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context); @@ -445,7 +446,7 @@ void shouldCreateNearSphereQueryForSphericalProperty() { void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMetric() { Point point = new Point(1.0, 1.0); - Distance distance = new Distance(1.0); + Distance distance = Distance.of(1.0); PartTree tree = new PartTree("findByAddress2dSphere_GeoNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, distance), context); @@ -458,7 +459,7 @@ void shouldCreateNearSphereQueryForSphericalPropertyHavingDistanceWithDefaultMet void shouldCreateNearQueryForMinMaxDistance() { Point point = new Point(10, 20); - Range range = Distance.between(new Distance(10), new Distance(20)); + Range range = Distance.between(Distance.of(10), Distance.of(20)); PartTree tree = new PartTree("findByAddress_GeoNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, getAccessor(converter, point, range), context); @@ -664,7 +665,7 @@ void nearShouldUseMetricDistanceForGeoJsonTypes() { GeoJsonPoint point = new GeoJsonPoint(27.987901, 86.9165379); PartTree tree = new PartTree("findByLocationNear", User.class); MongoQueryCreator creator = new MongoQueryCreator(tree, - getAccessor(converter, point, new Distance(1, Metrics.KILOMETERS)), context); + getAccessor(converter, point, Distance.of(1, Metrics.KILOMETERS)), context); assertThat(creator.createQuery()).isEqualTo(query(where("location").nearSphere(point).maxDistance(1000.0D))); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java index dbd17aa805..2c0c996bc3 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryExecutionUnitTests.java @@ -32,6 +32,7 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; + import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; @@ -86,7 +87,7 @@ class MongoQueryExecutionUnitTests { @Mock DbRefResolver dbRefResolver; private Point POINT = new Point(10, 20); - private Distance DISTANCE = new Distance(2.5, Metrics.KILOMETERS); + private Distance DISTANCE = Distance.of(2.5, Metrics.KILOMETERS); private RepositoryMetadata metadata = new DefaultRepositoryMetadata(PersonRepository.class); private MongoMappingContext context = new MongoMappingContext(); private ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java index 8f9824e14d..386d0fa4b5 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/MongoQueryMethodUnitTests.java @@ -23,6 +23,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.domain.Pageable; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoPage; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java index d7a3430048..1fbd60414a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecutionUnitTests.java @@ -71,7 +71,7 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception { Query query = new Query(); when(parameterAccessor.getGeoNearLocation()).thenReturn(new Point(1, 2)); when(parameterAccessor.getDistanceRange()) - .thenReturn(Range.from(Bound.inclusive(new Distance(10))).to(Bound.inclusive(new Distance(15)))); + .thenReturn(Range.from(Bound.inclusive(Distance.of(10))).to(Bound.inclusive(Distance.of(15)))); when(parameterAccessor.getPageable()).thenReturn(PageRequest.of(1, 10)); new GeoNearExecution(operations, parameterAccessor, TypeInformation.fromReturnTypeOf(geoNear)).execute(query, @@ -83,8 +83,8 @@ public void geoNearExecutionShouldApplyQuerySettings() throws Exception { NearQuery nearQuery = queryArgumentCaptor.getValue(); assertThat(nearQuery.toDocument().get("near")).isEqualTo(Arrays.asList(1d, 2d)); assertThat(nearQuery.getSkip()).isEqualTo(10L); - assertThat(nearQuery.getMinDistance()).isEqualTo(new Distance(10)); - assertThat(nearQuery.getMaxDistance()).isEqualTo(new Distance(15)); + assertThat(nearQuery.getMinDistance()).isEqualTo(Distance.of(10)); + assertThat(nearQuery.getMaxDistance()).isEqualTo(Distance.of(15)); } @Test // DATAMONGO-1444 diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java index 82cd0a157c..14cbbc0394 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryMethodUnitTests.java @@ -17,7 +17,6 @@ import static org.assertj.core.api.Assertions.*; -import org.springframework.data.mongodb.repository.query.MongoQueryMethodUnitTests.PersonRepository; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,6 +26,7 @@ import org.assertj.core.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java index 3ed7ace0f9..91f23bb049 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/StubParameterAccessor.java @@ -19,11 +19,14 @@ import java.util.Iterator; import org.jspecify.annotations.Nullable; + import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; +import org.springframework.data.domain.Score; import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Vector; import org.springframework.data.geo.Distance; import org.springframework.data.geo.Point; import org.springframework.data.mongodb.core.convert.MongoWriter; @@ -73,6 +76,21 @@ public StubParameterAccessor(Object... values) { } } + @Override + public Vector getVector() { + return null; + } + + @Override + public @org.jspecify.annotations.Nullable Score getScore() { + return null; + } + + @Override + public @org.jspecify.annotations.Nullable Range getScoreRange() { + return null; + } + @Override public ScrollPosition getScrollPosition() { return null; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java new file mode 100644 index 0000000000..c347936dfe --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java @@ -0,0 +1,108 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.bson.conversions.Bson; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.CrudRepository; +import org.springframework.data.repository.core.support.DefaultRepositoryMetadata; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Unit tests for {@link VectorSearchAggregation}. + * + * @author Mark Paluch + */ +class VectorSearchAggregationUnitTests { + + MongoOperations operationsMock; + MongoMappingContext context; + MappingMongoConverter converter; + + @BeforeEach + public void setUp() { + + context = new MongoMappingContext(); + converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, context); + operationsMock = Mockito.mock(MongoOperations.class); + + when(operationsMock.getConverter()).thenReturn(converter); + when(operationsMock.execute(any())).thenReturn(Bson.DEFAULT_CODEC_REGISTRY); + } + + @Test + void derivesPrefilter() throws Exception { + + VectorSearchAggregation aggregation = aggregation(SampleRepository.class, "searchByCountryAndEmbeddingNear", + String.class, Vector.class, Score.class, Limit.class); + + VectorSearchDelegate.QueryMetadata query = aggregation.createVectorSearchQuery( + aggregation.getQueryMethod().getResultProcessor(), + new MongoParametersParameterAccessor(aggregation.getQueryMethod(), + new Object[] { "de", Vector.of(1f), Score.of(1), Limit.unlimited() }), + Object.class); + + assertThat(query.query().getQueryObject()).containsEntry("country", "de"); + } + + private VectorSearchAggregation aggregation(Class repository, String name, Class... parameters) + throws Exception { + + Method method = repository.getMethod(name, parameters); + ProjectionFactory factory = new SpelAwareProxyProjectionFactory(); + MongoQueryMethod queryMethod = new MongoQueryMethod(method, new DefaultRepositoryMetadata(repository), factory, + context); + return new VectorSearchAggregation(queryMethod, operationsMock, ValueExpressionDelegate.create()); + } + + interface SampleRepository extends CrudRepository { + + @VectorSearch(indexName = "cos-index") + SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score similarity, + Limit limit); + + } + + static class WithVectorFields { + + String id; + String country; + String description; + + Vector embedding; + + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java new file mode 100644 index 0000000000..06a80e78fc --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java @@ -0,0 +1,130 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.query; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.Test; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Score; +import org.springframework.data.domain.SearchResults; +import org.springframework.data.domain.Vector; +import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; +import org.springframework.data.mongodb.core.convert.MappingMongoConverter; +import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.MongoMappingContext; +import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.util.json.ParameterBindingContext; +import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.AnnotationRepositoryMetadata; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +/** + * Unit tests for {@link VectorSearchDelegate}. + * + * @author Mark Paluch + */ +class VectorSearchDelegateUnitTests { + + MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); + + @Test + void shouldConsiderDerivedLimit() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); + + VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + + assertThat(query.query().getLimit()).isEqualTo(10); + assertThat(query.numCandidates()).isEqualTo(10 * 20); + } + + @Test + void shouldNotSetNumCandidates() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10EnnByEmbeddingNear", Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); + + VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + + assertThat(query.query().getLimit()).isEqualTo(10); + assertThat(query.numCandidates()).isNull(); + } + + @Test + void shouldConsiderProvidedLimit() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class, + Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11)); + + VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + + assertThat(query.query().getLimit()).isEqualTo(11); + assertThat(query.numCandidates()).isEqualTo(11 * 20); + } + + private VectorSearchDelegate.QueryMetadata createQueryMetadata(MongoQueryMethod queryMethod, + MongoParametersParameterAccessor accessor) { + + VectorSearchDelegate delegate = new VectorSearchDelegate(queryMethod, converter, ValueExpressionDelegate.create()); + + return delegate.createQuery(mock(ValueExpressionEvaluator.class), queryMethod.getResultProcessor(), accessor, + Object.class, new ParameterBindingDocumentCodec(), mock(ParameterBindingContext.class)); + } + + private MongoQueryMethod getMongoQueryMethod(Method method) { + RepositoryMetadata metadata = AnnotationRepositoryMetadata.getMetadata(method.getDeclaringClass()); + return new MongoQueryMethod(method, metadata, new SpelAwareProxyProjectionFactory(), converter.getMappingContext()); + } + + private static MongoParametersParameterAccessor getAccessor(MongoQueryMethod queryMethod, Object... values) { + return new MongoParametersParameterAccessor(queryMethod, values); + } + + interface VectorSearchRepository extends Repository { + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ENN) + SearchResults searchTop10EnnByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity, Limit limit); + + } + + static class WithVector { + + Vector embedding; + } +} diff --git a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt index cbb7ae46f3..99d57002e4 100644 --- a/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt +++ b/spring-data-mongodb/src/test/kotlin/org/springframework/data/mongodb/core/ReactiveFindOperationExtensionsTests.kt @@ -270,9 +270,9 @@ class ReactiveFindOperationExtensionsTests { fun terminatingFindNearAllAsFlow() { val spec = mockk>() - val foo = GeoResult("foo", Distance(0.0)) - val bar = GeoResult("bar", Distance(0.0)) - val baz = GeoResult("baz", Distance(0.0)) + val foo = GeoResult("foo", Distance.of(0.0)) + val bar = GeoResult("bar", Distance.of(0.0)) + val baz = GeoResult("baz", Distance.of(0.0)) every { spec.all() } returns Flux.just(foo, bar, baz) runBlocking { diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index a7401fb11f..6f2d1e2847 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -45,6 +45,7 @@ ** xref:repositories/create-instances.adoc[] ** xref:repositories/query-methods-details.adoc[] ** xref:mongodb/repositories/query-methods.adoc[] +** xref:mongodb/repositories/vector-search.adoc[] ** xref:mongodb/repositories/modifying-methods.adoc[] ** xref:repositories/projections.adoc[] ** xref:repositories/custom-implementations.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc new file mode 100644 index 0000000000..2e590107ec --- /dev/null +++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc @@ -0,0 +1,8 @@ +:vector-search-intro-include: data-mongodb::partial$vector-search-intro-include.adoc +:vector-search-model-include: data-mongodb::partial$vector-search-model-include.adoc +:vector-search-repository-include: data-mongodb::partial$vector-search-repository-include.adoc +:vector-search-scoring-include: data-mongodb::partial$vector-search-scoring-include.adoc +:vector-search-method-derived-include: data-mongodb::partial$vector-search-method-derived-include.adoc +:vector-search-method-annotated-include: data-mongodb::partial$vector-search-method-annotated-include.adoc + +include::{commons}@data-commons::page$repositories/vector-search.adoc[] diff --git a/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc new file mode 100644 index 0000000000..355bccf4e3 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-intro-include.adoc @@ -0,0 +1 @@ +To use Vector Search with MongoDB, you need a MongoDB Atlas instance that is either running in the cloud or by using https://www.mongodb.com/docs/atlas/cli/current/atlas-cli-deploy-docker/[Docker]. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc new file mode 100644 index 0000000000..752ffad622 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc @@ -0,0 +1,23 @@ +Annotated search methods use the `@VectorSearch` annotation to define parameters for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage. + +.Using `@VectorSearch` Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score distance); + + @VectorSearch(indexName = "my-index", filter = "{country: ?0}", numCandidates = "#{#limit * 20}", + searchType = VectorSearchOperation.SearchType.ANN) + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, int limit); +} +---- +==== + +Annotated Search Methods can define `filter` for pre-filter usage. + +`filter`, `limit`, and `numCandidates` support xref:page$mongodb/value-expressions.adoc[Value Expressions] allowing references to search method arguments. + diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc new file mode 100644 index 0000000000..dd06ee699a --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc @@ -0,0 +1,21 @@ +MongoDB Search methods must use the `@VectorSearch` annotation to define the index name for the https://www.mongodb.com/docs/upcoming/reference/operator/aggregation/vectorSearch/[`$vectorSearch`] aggregation stage. + +.Using `Near` and `Within` Keywords in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "my-index") + SearchResults searchByEmbeddingNear(Vector vector, Score score); + + @VectorSearch(indexName = "my-index") + SearchResults searchByEmbeddingWithin(Vector vector, Range range); + + @VectorSearch(indexName = "my-index") + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector vector, Range range); +} +---- +==== + +Derived Search Methods can define domain model attributes to create the pre-filter for indexed fields. diff --git a/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc new file mode 100644 index 0000000000..e657f3aa63 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-model-include.adoc @@ -0,0 +1,15 @@ +==== +[source,java] +---- +class Comment { + + @Id String id; + String country; + String comment; + + Vector embedding; + + // getters, setters, … +} +---- +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc new file mode 100644 index 0000000000..c7ad91c9db --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc @@ -0,0 +1,19 @@ +.Using `SearchResult` in a Repository Search Method +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(indexName = "my-index") + SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score score, + Limit limit); + + @VectorSearch(indexName = "my-index") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + Score score); + +} + +SearchResults results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10)); +---- +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc new file mode 100644 index 0000000000..b97475b467 --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc @@ -0,0 +1,32 @@ +MongoDB reports the score directly as similarity value. +The scoring function must be specified in the index and therefore, Vector search methods do not consider the `Score.scoringFunction`. +The scoring function defaults to `ScoringFunction.unspecified()` as there is no information inside of search results how the score has been computed. + +.Using `Score` and `Similarity` in a Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + @VectorSearch(…) + SearchResults searchByEmbeddingNear(Vector vector, Score similarity); + + @VectorSearch(…) + SearchResults searchByEmbeddingNear(Vector vector, Similarity similarity); + + @VectorSearch(…) + SearchResults searchByEmbeddingNear(Vector vector, Range range); +} + +repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9)); <1> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.of(0.9)); <2> + +repository.searchByEmbeddingNear(Vector.of(…), Similarity.between(0.5, 1)); <3> +---- + +<1> Run a search and return results with a similarity of `0.9` or greater. +<2> Return results with a similarity of `0.9` or greater. +<3> Return results with a similarity of between `0.5` and `1.0` or greater. +==== + From 21568c84ebeee70f0e56841fa8b5df5c4b80d163 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 6 May 2025 10:56:42 +0200 Subject: [PATCH 61/74] Polishing. Original Pull Request: #4960 --- .../data/mongodb/core/MongoTemplate.java | 27 +- .../mongodb/core/ReactiveMongoTemplate.java | 1 + .../core/aggregation/AggregationPipeline.java | 14 +- .../core/aggregation/ArrayOperators.java | 1 + .../query/ConvertingParameterAccessor.java | 6 +- .../MongoParametersParameterAccessor.java | 9 +- .../repository/query/MongoQueryCreator.java | 49 ++-- .../repository/query/MongoQueryExecution.java | 67 ++--- .../query/ReactiveMongoQueryExecution.java | 20 +- .../ReactiveVectorSearchAggregation.java | 8 +- .../query/VectorSearchAggregation.java | 10 +- .../query/VectorSearchDelegate.java | 252 ++++++++++-------- .../mongodb/repository/VectorSearchTests.java | 3 +- .../VectorSearchAggregationUnitTests.java | 3 +- .../query/VectorSearchDelegateUnitTests.java | 156 +++++++++-- .../pages/mongodb/mongo-search-indexes.adoc | 2 +- ...ector-search-method-annotated-include.adoc | 8 +- .../vector-search-method-derived-include.adoc | 12 +- .../vector-search-repository-include.adoc | 12 +- .../vector-search-scoring-include.adoc | 6 +- 20 files changed, 415 insertions(+), 251 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index db3dcc1263..ab03b41424 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -486,7 +486,7 @@ protected Stream doStream(Query query, Class entityType, String collec return doStream(query, entityType, collectionName, returnType, QueryResultConverter.entity()); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) Stream doStream(Query query, Class entityType, String collectionName, Class returnType, QueryResultConverter resultConverter) { @@ -1086,34 +1086,29 @@ GeoResults doGeoNear(NearQuery near, Class domainType, String colle return new GeoResults<>(result, avgDistance); } - @Nullable - @Override - public T findAndModify(Query query, UpdateDefinition update, Class entityClass) { + public @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, Class entityClass, + public @Nullable T findAndModify(Query query, UpdateDefinition update, Class entityClass, String collectionName) { return findAndModify(query, update, new FindAndModifyOptions(), entityClass, collectionName); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + public @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass) { return findAndModify(query, update, options, entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + public @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName) { return findAndModify(query, update, options, entityClass, collectionName, QueryResultConverter.entity()); } - T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, + @Nullable T findAndModify(Query query, UpdateDefinition update, FindAndModifyOptions options, Class entityClass, String collectionName, QueryResultConverter resultConverter) { Assert.notNull(query, "Query must not be null"); @@ -1185,15 +1180,13 @@ T findAndModify(Query query, UpdateDefinition update, FindAndModifyOption // Find methods that take a Query to express the query and that return a single object that is also removed from the // collection in the database. - @Nullable @Override - public T findAndRemove(Query query, Class entityClass) { + public @Nullable T findAndRemove(Query query, Class entityClass) { return findAndRemove(query, entityClass, getCollectionName(entityClass)); } - @Nullable @Override - public T findAndRemove(Query query, Class entityClass, String collectionName) { + public @Nullable T findAndRemove(Query query, Class entityClass, String collectionName) { Assert.notNull(query, "Query must not be null"); Assert.notNull(entityClass, "EntityClass must not be null"); @@ -2161,11 +2154,11 @@ protected UpdateResult replace(Query query, Class entityType, T replac * @param entityClass * @return */ - @SuppressWarnings("NullAway") protected List doFindAndDelete(String collectionName, Query query, Class entityClass) { return doFindAndDelete(collectionName, query, entityClass, QueryResultConverter.entity()); } + @SuppressWarnings("NullAway") List doFindAndDelete(String collectionName, Query query, Class entityClass, QueryResultConverter resultConverter) { @@ -2229,7 +2222,7 @@ protected AggregationResults doAggregate(Aggregation aggregation, String return doAggregate(aggregation, collectionName, outputType, QueryResultConverter.entity(), context); } - @SuppressWarnings("ConstantConditions") + @SuppressWarnings({"ConstantConditions", "NullAway"}) AggregationResults doAggregate(Aggregation aggregation, String collectionName, Class outputType, QueryResultConverter resultConverter, AggregationOperationContext context) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 935c55fc9e..0ad473b8b7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -2293,6 +2293,7 @@ protected Flux doFindAndDelete(String collectionName, Query query, Class< .flatMapSequential(deleteResult -> Flux.fromIterable(list))); } + @SuppressWarnings({"rawtypes", "unchecked", "NullAway"}) Flux doFindAndDelete(String collectionName, Query query, Class entityClass, QueryResultConverter resultConverter) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java index 40966bcf3d..f06803997b 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/AggregationPipeline.java @@ -22,8 +22,10 @@ import java.util.function.Predicate; import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.springframework.lang.Contract; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; /** * The {@link AggregationPipeline} holds the collection of {@link AggregationOperation aggregation stages}. @@ -82,6 +84,14 @@ public List getOperations() { return Collections.unmodifiableList(pipeline); } + public @Nullable AggregationOperation firstOperation() { + return CollectionUtils.firstElement(pipeline); + } + + public @Nullable AggregationOperation lastOperation() { + return CollectionUtils.lastElement(pipeline); + } + List toDocuments(AggregationOperationContext context) { verify(); @@ -97,8 +107,8 @@ public boolean isOutOrMerge() { return false; } - AggregationOperation operation = pipeline.get(pipeline.size() - 1); - return isOut(operation) || isMerge(operation); + AggregationOperation operation = lastOperation(); + return operation != null && (isOut(operation) || isMerge(operation)); } void verify() { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java index 02b805d5ed..85952d8f39 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/aggregation/ArrayOperators.java @@ -356,6 +356,7 @@ public SortArray sort(Sort sort) { * @return new instance of {@link SortArray}. * @since 4.5 */ + @SuppressWarnings("NullAway") public SortArray sort(Direction direction) { if (usesFieldRef()) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java index e51d4435a8..f203b67e67 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ConvertingParameterAccessor.java @@ -77,7 +77,7 @@ public PotentiallyConvertingIterator iterator() { } @Override - public Vector getVector() { + public @Nullable Vector getVector() { return delegate.getVector(); } @@ -104,12 +104,12 @@ public Sort getSort() { } @Override - public @org.jspecify.annotations.Nullable Score getScore() { + public @Nullable Score getScore() { return delegate.getScore(); } @Override - public @org.jspecify.annotations.Nullable Range getScoreRange() { + public @Nullable Range getScoreRange() { return delegate.getScoreRange(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java index 41cf084d45..0f56223492 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoParametersParameterAccessor.java @@ -61,14 +61,13 @@ public MongoParametersParameterAccessor(MongoQueryMethod method, Object[] values public Range getScoreRange() { MongoParameters mongoParameters = method.getParameters(); - int rangeIndex = mongoParameters.getScoreRangeIndex(); - if (rangeIndex != -1) { - return getValue(rangeIndex); + if (mongoParameters.hasScoreRangeParameter()) { + return getValue(mongoParameters.getScoreRangeIndex()); } - int scoreIndex = mongoParameters.getScoreIndex(); - Bound maxDistance = scoreIndex == -1 ? Bound.unbounded() : Bound.inclusive((Score) getScore()); + Score score = getScore(); + Bound maxDistance = score != null ? Bound.inclusive(score) : Bound.unbounded(); return Range.of(Bound.unbounded(), maxDistance); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java index 1f742ec32f..ba7394ec17 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryCreator.java @@ -15,7 +15,8 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.springframework.data.mongodb.core.query.Criteria.*; +import static org.springframework.data.mongodb.core.query.Criteria.Placeholder; +import static org.springframework.data.mongodb.core.query.Criteria.where; import java.util.Arrays; import java.util.Collection; @@ -27,7 +28,6 @@ import org.apache.commons.logging.LogFactory; import org.bson.BsonRegularExpression; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.Range; import org.springframework.data.domain.Range.Bound; import org.springframework.data.domain.Sort; @@ -118,8 +118,9 @@ protected Criteria create(Part part, Iterator iterator) { return new Criteria(); } - if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { - return null; + if (isPartOfSearchQuery(part)) { + skip(part, iterator); + return new Criteria(); } PersistentPropertyPath path = context.getPersistentPropertyPath(part.getProperty()); @@ -135,7 +136,8 @@ protected Criteria and(Part part, Criteria base, Iterator iterator) { return create(part, iterator); } - if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { + if (isPartOfSearchQuery(part)) { + skip(part, iterator); return base; } @@ -176,15 +178,6 @@ protected Query complete(@Nullable Criteria criteria, Sort sort) { @SuppressWarnings("NullAway") private Criteria from(Part part, MongoPersistentProperty property, Criteria criteria, Iterator parameters) { - if (isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN))) { - - int numberOfArguments = part.getType().getNumberOfArguments(); - for (int i = 0; i < numberOfArguments; i++) { - parameters.next(); - } - return null; - } - Type type = part.getType(); switch (type) { @@ -206,13 +199,13 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit return criteria.is(null); case NOT_IN: Object ninValue = parameters.next(); - if(ninValue instanceof Placeholder) { + if (ninValue instanceof Placeholder) { return criteria.raw("$nin", ninValue); } return criteria.nin(valueAsList(ninValue, part)); case IN: Object inValue = parameters.next(); - if(inValue instanceof Placeholder) { + if (inValue instanceof Placeholder) { return criteria.raw("$in", inValue); } return criteria.in(valueAsList(inValue, part)); @@ -231,7 +224,7 @@ private Criteria from(Part part, MongoPersistentProperty property, Criteria crit return param instanceof Pattern pattern ? criteria.regex(pattern) : criteria.regex(param.toString()); case EXISTS: Object next = parameters.next(); - if(next instanceof Placeholder placeholder) { + if (next instanceof Placeholder placeholder) { return criteria.raw("$exists", placeholder); } else { return criteria.exists((Boolean) next); @@ -355,7 +348,7 @@ private Criteria createContainingCriteria(Part part, MongoPersistentProperty pro if (property.isCollectionLike()) { Object next = parameters.next(); - if(next instanceof Placeholder) { + if (next instanceof Placeholder) { return criteria.raw("$in", next); } return criteria.in(valueAsList(next, part)); @@ -433,8 +426,7 @@ private java.util.List valueAsList(Object value, Part part) { streamable = streamable.map(it -> { if (it instanceof String sv) { - return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), - regexOptions); + return new BsonRegularExpression(MongoRegexCreator.INSTANCE.toRegularExpression(sv, matchMode), regexOptions); } return it; }); @@ -468,10 +460,23 @@ private boolean isSpherical(MongoPersistentProperty property) { return false; } + private boolean isPartOfSearchQuery(Part part) { + return isSearchQuery && (part.getType().equals(Type.NEAR) || part.getType().equals(Type.WITHIN)); + } + + private static void skip(Part part, Iterator parameters) { + + int total = part.getNumberOfArguments(); + int i = 0; + while (parameters.hasNext() && i < total) { + parameters.next(); + i++; + } + } + /** * Compute a {@link Type#BETWEEN} typed {@link Part} using {@link Criteria#gt(Object) $gt}, - * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}. - *
+ * {@link Criteria#gte(Object) $gte}, {@link Criteria#lt(Object) $lt} and {@link Criteria#lte(Object) $lte}.
* In case the first {@literal value} is actually a {@link Range} the lower and upper bounds of the {@link Range} are * used according to their {@link Bound#isInclusive() inclusion} definition. Otherwise the {@literal value} is used * for {@literal $gt} and {@link Iterator#next() parameters.next()} as {@literal $lt}. diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java index d9a91434ce..c0531e0e19 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/MongoQueryExecution.java @@ -15,18 +15,16 @@ */ package org.springframework.data.mongodb.repository.query; -import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; import java.util.List; import java.util.function.Supplier; -import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Range; +import org.springframework.data.domain.ScoringFunction; import org.springframework.data.domain.SearchResult; import org.springframework.data.domain.SearchResults; import org.springframework.data.domain.Similarity; @@ -37,6 +35,7 @@ import org.springframework.data.geo.GeoResult; import org.springframework.data.geo.GeoResults; import org.springframework.data.geo.Point; +import org.springframework.data.mongodb.core.ExecutableAggregationOperation.TerminatingAggregation; import org.springframework.data.mongodb.core.ExecutableFindOperation; import org.springframework.data.mongodb.core.ExecutableFindOperation.FindWithQuery; import org.springframework.data.mongodb.core.ExecutableFindOperation.TerminatingFind; @@ -45,12 +44,13 @@ import org.springframework.data.mongodb.core.ExecutableRemoveOperation.TerminatingRemove; import org.springframework.data.mongodb.core.ExecutableUpdateOperation.ExecutableUpdate; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.mongodb.repository.util.SliceUtils; import org.springframework.data.repository.query.QueryMethod; import org.springframework.data.support.PageableExecutionUtils; @@ -186,7 +186,7 @@ public Object execute(Query query) { return isListOfGeoResult(method.getReturnType()) ? results.getContent() : results; } - @SuppressWarnings({"unchecked","NullAway"}) + @SuppressWarnings({ "unchecked", "NullAway" }) GeoResults doExecuteQuery(Query query) { Point nearLocation = accessor.getGeoNearLocation(); @@ -225,52 +225,53 @@ private static boolean isListOfGeoResult(TypeInformation returnType) { * {@link MongoQueryExecution} to execute vector search. * * @author Mark Paluch + * @author Chistoph Strobl * @since 5.0 */ class VectorSearchExecution implements MongoQueryExecution { private final MongoOperations operations; - private final MongoQueryMethod method; + private final TypeInformation returnType; private final String collectionName; - private final VectorSearchDelegate.QueryMetadata queryMetadata; - private final List pipeline; + private final Class targetType; + private final ScoringFunction scoringFunction; + private final AggregationPipeline pipeline; + + VectorSearchExecution(MongoOperations operations, MongoQueryMethod method, String collectionName, + QueryContainer queryContainer) { + this(operations, queryContainer.outputType(), collectionName, method.getReturnType(), queryContainer.pipeline(), + queryContainer.scoringFunction()); + } - public VectorSearchExecution(MongoOperations operations, MongoQueryMethod method, String collectionName, - VectorSearchDelegate.QueryMetadata queryMetadata, MongoParameterAccessor accessor) { + public VectorSearchExecution(MongoOperations operations, Class targetType, String collectionName, + TypeInformation returnType, AggregationPipeline pipeline, ScoringFunction scoringFunction) { this.operations = operations; + this.returnType = returnType; this.collectionName = collectionName; - this.queryMetadata = queryMetadata; - this.method = method; - this.pipeline = queryMetadata.getAggregationPipeline(method, accessor); + this.targetType = targetType; + this.scoringFunction = scoringFunction; + this.pipeline = pipeline; } @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) public Object execute(Query query) { - AggregationResults aggregated = operations.aggregate( - TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline), collectionName, - queryMetadata.outputType()); - - List mappedResults = aggregated.getMappedResults(); + TerminatingAggregation executableAggregation = operations.aggregateAndReturn(targetType) + .inCollection(collectionName).by(TypedAggregation.newAggregation(targetType, pipeline.getOperations())); - if (isSearchResult(method.getReturnType())) { - - List rawResults = aggregated.getRawResults().getList("results", org.bson.Document.class); - List> result = new ArrayList<>(mappedResults.size()); - - for (int i = 0; i < mappedResults.size(); i++) { - Document document = rawResults.get(i); - SearchResult searchResult = new SearchResult<>(mappedResults.get(i), - Similarity.raw(document.getDouble("__score__"), queryMetadata.scoringFunction())); - - result.add(searchResult); - } - - return isListOfSearchResult(method.getReturnType()) ? result : new SearchResults<>(result); + if (!isSearchResult(returnType)) { + return executableAggregation.all().getMappedResults(); } - return mappedResults; + AggregationResults> result = executableAggregation + .map((raw, container) -> new SearchResult<>(container.get(), + Similarity.raw(raw.getDouble("__score__"), scoringFunction))) + .all(); + + return isListOfSearchResult(returnType) ? result.getMappedResults() + : new SearchResults(result.getMappedResults()); } private static boolean isListOfSearchResult(TypeInformation returnType) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java index 389f4e871d..29e2127e18 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveMongoQueryExecution.java @@ -18,12 +18,9 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import java.util.List; - import org.bson.Document; import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; - import org.springframework.core.convert.converter.Converter; import org.springframework.data.convert.DtoInstantiatingConverter; import org.springframework.data.domain.Pageable; @@ -36,11 +33,12 @@ import org.springframework.data.mapping.model.EntityInstantiators; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.ReactiveUpdateOperation.ReactiveUpdate; -import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; import org.springframework.data.mongodb.core.query.NearQuery; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ReturnedType; import org.springframework.data.util.ReactiveWrappers; @@ -134,24 +132,24 @@ private boolean isStreamOfGeoResult() { class VectorSearchExecution implements ReactiveMongoQueryExecution { private final ReactiveMongoOperations operations; - private final VectorSearchDelegate.QueryMetadata queryMetadata; - private final List pipeline; + private final QueryContainer queryMetadata; + private final AggregationPipeline pipeline; private final boolean returnSearchResult; - public VectorSearchExecution(ReactiveMongoOperations operations, MongoQueryMethod method, - VectorSearchDelegate.QueryMetadata queryMetadata, MongoParameterAccessor accessor) { + VectorSearchExecution(ReactiveMongoOperations operations, MongoQueryMethod method, QueryContainer queryMetadata) { this.operations = operations; this.queryMetadata = queryMetadata; - this.pipeline = queryMetadata.getAggregationPipeline(method, accessor); + this.pipeline = queryMetadata.pipeline(); this.returnSearchResult = isSearchResult(method.getReturnType()); } @Override public Publisher execute(Query query, Class type, String collection) { - Flux aggregate = operations - .aggregate(TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline), collection, Document.class); + Flux aggregate = operations.aggregate( + TypedAggregation.newAggregation(queryMetadata.outputType(), pipeline.getOperations()), collection, + Document.class); return aggregate.map(document -> { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java index 1ecbb0235f..cf75c7db94 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/ReactiveVectorSearchAggregation.java @@ -19,13 +19,13 @@ import org.bson.Document; import org.reactivestreams.Publisher; - import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.ReactiveMongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; @@ -84,11 +84,11 @@ protected Publisher doExecute(ReactiveMongoQueryMethod method, ResultPro ParameterBindingContext bindingContext = new ParameterBindingContext(accessor::getBindableValue, expressionEvaluator); - VectorSearchDelegate.QueryMetadata query = delegate.createQuery(expressionEvaluator, processor, accessor, - typeToRead, codec, bindingContext); + QueryContainer query = delegate.createQuery(expressionEvaluator, processor, accessor, typeToRead, codec, + bindingContext); ReactiveMongoQueryExecution.VectorSearchExecution execution = new ReactiveMongoQueryExecution.VectorSearchExecution( - mongoOperations, method, query, accessor); + mongoOperations, method, query); return execution.execute(query.query(), Document.class, collectionEntity.getCollection()); }); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java index 9740c0696c..eb8dc2e52e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregation.java @@ -15,16 +15,17 @@ */ package org.springframework.data.mongodb.repository.query; +import org.jspecify.annotations.Nullable; import org.springframework.data.mapping.model.ValueExpressionEvaluator; import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.repository.query.ResultProcessor; import org.springframework.data.repository.query.ValueExpressionDelegate; -import org.springframework.lang.Nullable; /** * {@link AbstractMongoQuery} implementation to run a {@link VectorSearchAggregation}. The pre-filter is either derived @@ -62,20 +63,19 @@ public VectorSearchAggregation(MongoQueryMethod method, MongoOperations mongoOpe this.delegate = new VectorSearchDelegate(method, mongoOperations.getConverter(), delegate); } - @SuppressWarnings("unchecked") @Override protected Object doExecute(MongoQueryMethod method, ResultProcessor processor, ConvertingParameterAccessor accessor, @Nullable Class typeToRead) { - VectorSearchDelegate.QueryMetadata query = createVectorSearchQuery(processor, accessor, typeToRead); + QueryContainer query = createVectorSearchQuery(processor, accessor, typeToRead); MongoQueryExecution.VectorSearchExecution execution = new MongoQueryExecution.VectorSearchExecution(mongoOperations, - method, collectionEntity.getCollection(), query, accessor); + method, collectionEntity.getCollection(), query); return execution.execute(query.query()); } - VectorSearchDelegate.QueryMetadata createVectorSearchQuery(ResultProcessor processor, MongoParameterAccessor accessor, + QueryContainer createVectorSearchQuery(ResultProcessor processor, MongoParameterAccessor accessor, @Nullable Class typeToRead) { ValueExpressionEvaluator evaluator = getExpressionEvaluatorFor(accessor); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java index 8932b85b1b..0dbff2e932 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegate.java @@ -20,7 +20,6 @@ import org.bson.Document; import org.jspecify.annotations.Nullable; - import org.springframework.data.domain.Limit; import org.springframework.data.domain.Range; import org.springframework.data.domain.Score; @@ -34,6 +33,7 @@ import org.springframework.data.mongodb.InvalidMongoDbApiUsageException; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.AggregationOperation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity; @@ -47,6 +47,7 @@ import org.springframework.data.repository.query.ValueExpressionDelegate; import org.springframework.data.repository.query.parser.Part; import org.springframework.data.repository.query.parser.PartTree; +import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; /** @@ -58,32 +59,35 @@ class VectorSearchDelegate { private final VectorSearchQueryFactory queryFactory; private final VectorSearchOperation.SearchType searchType; + private final String indexName; private final @Nullable Integer numCandidates; private final @Nullable String numCandidatesExpression; private final Limit limit; private final @Nullable String limitExpression; private final MongoConverter converter; - public VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, ValueExpressionDelegate delegate) { + VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, ValueExpressionDelegate delegate) { VectorSearch vectorSearch = method.findAnnotatedVectorSearch().orElseThrow(); + this.searchType = vectorSearch.searchType(); + this.indexName = method.getAnnotatedHint(); if (StringUtils.hasText(vectorSearch.numCandidates())) { ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.numCandidates()); if (expression.isLiteral()) { - numCandidates = Integer.parseInt(vectorSearch.numCandidates()); - numCandidatesExpression = null; + this.numCandidates = Integer.parseInt(vectorSearch.numCandidates()); + this.numCandidatesExpression = null; } else { - numCandidates = null; - numCandidatesExpression = vectorSearch.numCandidates(); + this.numCandidates = null; + this.numCandidatesExpression = vectorSearch.numCandidates(); } } else { - numCandidates = null; - numCandidatesExpression = null; + this.numCandidates = null; + this.numCandidatesExpression = null; } if (StringUtils.hasText(vectorSearch.limit())) { @@ -91,26 +95,26 @@ public VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, V ValueExpression expression = delegate.getValueExpressionParser().parse(vectorSearch.limit()); if (expression.isLiteral()) { - limit = Limit.of(Integer.parseInt(vectorSearch.limit())); - limitExpression = null; + this.limit = Limit.of(Integer.parseInt(vectorSearch.limit())); + this.limitExpression = null; } else { - limit = Limit.unlimited(); - limitExpression = vectorSearch.limit(); + this.limit = Limit.unlimited(); + this.limitExpression = vectorSearch.limit(); } } else { - limit = Limit.unlimited(); - limitExpression = null; + this.limit = Limit.unlimited(); + this.limitExpression = null; } this.converter = converter; if (StringUtils.hasText(vectorSearch.filter())) { - queryFactory = StringUtils.hasText(vectorSearch.path()) + this.queryFactory = StringUtils.hasText(vectorSearch.path()) ? new AnnotatedQueryFactory(vectorSearch.filter(), vectorSearch.path()) : new AnnotatedQueryFactory(vectorSearch.filter(), method.getEntityInformation().getCollectionEntity()); } else { - queryFactory = new PartTreeQueryFactory( + this.queryFactory = new PartTreeQueryFactory( new PartTree(method.getName(), method.getResultProcessor().getReturnedType().getDomainType()), converter.getMappingContext()); } @@ -119,43 +123,136 @@ public VectorSearchDelegate(MongoQueryMethod method, MongoConverter converter, V /** * Create Query Metadata for {@code $vectorSearch}. */ - public QueryMetadata createQuery(ValueExpressionEvaluator evaluator, ResultProcessor processor, + QueryContainer createQuery(ValueExpressionEvaluator evaluator, ResultProcessor processor, MongoParameterAccessor accessor, @Nullable Class typeToRead, ParameterBindingDocumentCodec codec, ParameterBindingContext context) { - Integer numCandidates = null; - Limit limit; + String scoreField = "__score__"; Class outputType = typeToRead != null ? typeToRead : processor.getReturnedType().getReturnedType(); - VectorSearchInput query = queryFactory.createQuery(accessor, codec, context); + VectorSearchInput vectorSearchInput = createSearchInput(evaluator, accessor, codec, context); + AggregationPipeline pipeline = createVectorSearchPipeline(vectorSearchInput, scoreField, outputType, accessor, + evaluator); - if (this.limitExpression != null) { - Object value = evaluator.evaluate(this.limitExpression); - limit = value instanceof Limit l ? l : Limit.of(((Number) value).intValue()); - } else if (this.limit.isLimited()) { - limit = this.limit; - } else { - limit = accessor.getLimit(); - } + return new QueryContainer(vectorSearchInput.path, scoreField, vectorSearchInput.query, pipeline, searchType, + outputType, getSimilarityFunction(accessor), indexName); + } - if (limit.isLimited()) { - query.query().limit(limit); - } + @SuppressWarnings("NullAway") + AggregationPipeline createVectorSearchPipeline(VectorSearchInput input, String scoreField, Class outputType, + MongoParameterAccessor accessor, ValueExpressionEvaluator evaluator) { + + Vector vector = accessor.getVector(); + Score score = accessor.getScore(); + Range distance = accessor.getScoreRange(); + Limit limit = Limit.of(input.query().getLimit()); + + List stages = new ArrayList<>(); + VectorSearchOperation $vectorSearch = Aggregation.vectorSearch(indexName).path(input.path()).vector(vector) + .limit(limit); + Integer candidates = null; if (this.numCandidatesExpression != null) { - numCandidates = ((Number) evaluator.evaluate(this.numCandidatesExpression)).intValue(); + candidates = ((Number) evaluator.evaluate(this.numCandidatesExpression)).intValue(); } else if (this.numCandidates != null) { - numCandidates = this.numCandidates; - } else if (query.query().isLimited() && (searchType == VectorSearchOperation.SearchType.ANN + candidates = this.numCandidates; + } else if (input.query().isLimited() && (searchType == VectorSearchOperation.SearchType.ANN || searchType == VectorSearchOperation.SearchType.DEFAULT)) { /* MongoDB: We recommend that you specify a number at least 20 times higher than the number of documents to return (limit) to increase accuracy. */ - numCandidates = query.query().getLimit() * 20; + candidates = input.query().getLimit() * 20; } - return new QueryMetadata(query.path, "__score__", query.query, searchType, outputType, numCandidates, - getSimilarityFunction(accessor)); + if (candidates != null) { + $vectorSearch = $vectorSearch.numCandidates(candidates); + } + // + $vectorSearch = $vectorSearch.filter(input.query.getQueryObject()); + $vectorSearch = $vectorSearch.searchType(this.searchType); + $vectorSearch = $vectorSearch.withSearchScore(scoreField); + + if (score != null) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + c.gt(score.getValue()); + }); + } else if (distance.getLowerBound().isBounded() || distance.getUpperBound().isBounded()) { + $vectorSearch = $vectorSearch.withFilterBySore(c -> { + Range.Bound lower = distance.getLowerBound(); + if (lower.isBounded()) { + double value = lower.getValue().get().getValue(); + if (lower.isInclusive()) { + c.gte(value); + } else { + c.gt(value); + } + } + + Range.Bound upper = distance.getUpperBound(); + if (upper.isBounded()) { + + double value = upper.getValue().get().getValue(); + if (upper.isInclusive()) { + c.lte(value); + } else { + c.lt(value); + } + } + }); + } + + stages.add($vectorSearch); + + if (input.query().isSorted()) { + + stages.add(ctx -> { + + Document mappedSort = ctx.getMappedObject(input.query().getSortObject(), outputType); + mappedSort.append(scoreField, -1); + return ctx.getMappedObject(new Document("$sort", mappedSort)); + }); + } else { + stages.add(Aggregation.sort(Sort.Direction.DESC, scoreField)); + } + + return new AggregationPipeline(stages); + } + + private VectorSearchInput createSearchInput(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor, + ParameterBindingDocumentCodec codec, ParameterBindingContext context) { + + VectorSearchInput input = queryFactory.createQuery(accessor, codec, context); + Limit limit = getLimit(evaluator, accessor); + if(!input.query.isLimited() || (input.query.isLimited() && !limit.isUnlimited())) { + input.query().limit(limit); + } + return input; + } + + private Limit getLimit(ValueExpressionEvaluator evaluator, MongoParameterAccessor accessor) { + + if (this.limitExpression != null) { + + Object value = evaluator.evaluate(this.limitExpression); + if (value != null) { + if (value instanceof Limit l) { + return l; + } + if (value instanceof Number n) { + return Limit.of(n.intValue()); + } + if (value instanceof String s) { + return Limit.of(NumberUtils.parseNumber(s, Integer.class)); + } + throw new IllegalArgumentException("Invalid type for Limit. Found [%s], expected Limit or Number"); + } + } + + if (this.limit.isLimited()) { + return this.limit; + } + + return accessor.getLimit(); } public String getQueryString() { @@ -192,82 +289,10 @@ ScoringFunction getSimilarityFunction(MongoParameterAccessor accessor) { * @param query * @param searchType * @param outputType - * @param numCandidates * @param scoringFunction */ - public record QueryMetadata(String path, String scoreField, Query query, VectorSearchOperation.SearchType searchType, - Class outputType, @Nullable Integer numCandidates, ScoringFunction scoringFunction) { - - /** - * Create the Aggregation Pipeline. - * - * @param queryMethod - * @param accessor - * @return - */ - public List getAggregationPipeline(MongoQueryMethod queryMethod, - MongoParameterAccessor accessor) { - - Vector vector = accessor.getVector(); - Score score = accessor.getScore(); - Range distance = accessor.getScoreRange(); - Limit limit = Limit.unlimited(); - - if (query.isLimited()) { - limit = Limit.of(query.getLimit()); - } - - List stages = new ArrayList<>(); - VectorSearchOperation $vectorSearch = Aggregation.vectorSearch(queryMethod.getAnnotatedHint()).path(path()) - .vector(vector).limit(limit); - - if (numCandidates() != null) { - $vectorSearch = $vectorSearch.numCandidates(numCandidates()); - } - - $vectorSearch = $vectorSearch.filter(query.getQueryObject()); - $vectorSearch = $vectorSearch.searchType(searchType()); - $vectorSearch = $vectorSearch.withSearchScore(scoreField()); - - if (score != null) { - $vectorSearch = $vectorSearch.withFilterBySore(c -> { - c.gt(score.getValue()); - }); - } else if (distance.getLowerBound().isBounded() || distance.getUpperBound().isBounded()) { - $vectorSearch = $vectorSearch.withFilterBySore(c -> { - Range.Bound lower = distance.getLowerBound(); - if (lower.isBounded()) { - double value = lower.getValue().get().getValue(); - if (lower.isInclusive()) { - c.gte(value); - } else { - c.gt(value); - } - } - - Range.Bound upper = distance.getUpperBound(); - if (upper.isBounded()) { - - double value = upper.getValue().get().getValue(); - if (upper.isInclusive()) { - c.lte(value); - } else { - c.lt(value); - } - } - }); - } - - stages.add($vectorSearch); - - if (query.isSorted()) { - // TODO stages.add(Aggregation.sort(query.with())); - } else { - stages.add(Aggregation.sort(Sort.Direction.DESC, "__score__")); - } - - return stages; - } + record QueryContainer(String path, String scoreField, Query query, AggregationPipeline pipeline, + VectorSearchOperation.SearchType searchType, Class outputType, ScoringFunction scoringFunction, String index) { } @@ -368,11 +393,12 @@ private class PartTreeQueryFactory implements VectorSearchQueryFactory { this.tree = tree; } + @SuppressWarnings("NullAway") public VectorSearchInput createQuery(MongoParameterAccessor parameterAccessor, ParameterBindingDocumentCodec codec, ParameterBindingContext context) { - MongoQueryCreator creator = new MongoQueryCreator(tree, parameterAccessor, converter.getMappingContext(), - false, true); + MongoQueryCreator creator = new MongoQueryCreator(tree, parameterAccessor, converter.getMappingContext(), false, + true); Query query = creator.createQuery(parameterAccessor.getSort()); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java index 028a6926fb..a224481da1 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/VectorSearchTests.java @@ -81,16 +81,15 @@ public String getDatabaseName() { @Override public MongoClient mongoClient() { - atlasLocal.start(); return MongoClients.create(atlasLocal.getConnectionString()); } } @BeforeAll static void beforeAll() throws InterruptedException { + atlasLocal.start(); - System.out.println(atlasLocal.getConnectionString()); client = MongoClients.create(atlasLocal.getConnectionString()); template = new MongoTestTemplate(client, "vector-search-tests"); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java index c347936dfe..819bba5a48 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchAggregationUnitTests.java @@ -34,6 +34,7 @@ import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; import org.springframework.data.projection.ProjectionFactory; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; import org.springframework.data.repository.CrudRepository; @@ -68,7 +69,7 @@ void derivesPrefilter() throws Exception { VectorSearchAggregation aggregation = aggregation(SampleRepository.class, "searchByCountryAndEmbeddingNear", String.class, Vector.class, Score.class, Limit.class); - VectorSearchDelegate.QueryMetadata query = aggregation.createVectorSearchQuery( + QueryContainer query = aggregation.createVectorSearchQuery( aggregation.getQueryMethod().getResultProcessor(), new MongoParametersParameterAccessor(aggregation.getQueryMethod(), new Object[] { "de", Vector.of(1f), Score.of(1), Limit.unlimited() }), diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java index 06a80e78fc..078c01eece 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/query/VectorSearchDelegateUnitTests.java @@ -15,23 +15,30 @@ */ package org.springframework.data.mongodb.repository.query; -import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.*; +import static org.mockito.Mockito.mock; +import static org.springframework.data.mongodb.test.util.Assertions.assertThat; import java.lang.reflect.Method; +import java.util.List; +import org.bson.Document; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; - import org.springframework.data.domain.Limit; import org.springframework.data.domain.Score; import org.springframework.data.domain.SearchResults; import org.springframework.data.domain.Vector; import org.springframework.data.mapping.model.ValueExpressionEvaluator; +import org.springframework.data.mongodb.core.aggregation.Aggregation; +import org.springframework.data.mongodb.core.aggregation.AggregationPipeline; import org.springframework.data.mongodb.core.aggregation.VectorSearchOperation; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; +import org.springframework.data.mongodb.core.mapping.Field; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; import org.springframework.data.mongodb.repository.VectorSearch; +import org.springframework.data.mongodb.repository.query.VectorSearchDelegate.QueryContainer; +import org.springframework.data.mongodb.util.aggregation.TestAggregationContext; import org.springframework.data.mongodb.util.json.ParameterBindingContext; import org.springframework.data.mongodb.util.json.ParameterBindingDocumentCodec; import org.springframework.data.projection.SpelAwareProxyProjectionFactory; @@ -44,6 +51,7 @@ * Unit tests for {@link VectorSearchDelegate}. * * @author Mark Paluch + * @author Christoph Strobl */ class VectorSearchDelegateUnitTests { @@ -57,10 +65,10 @@ void shouldConsiderDerivedLimit() throws ReflectiveOperationException { MongoQueryMethod queryMethod = getMongoQueryMethod(method); MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); - VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + QueryContainer container = createQueryContainer(queryMethod, accessor); - assertThat(query.query().getLimit()).isEqualTo(10); - assertThat(query.numCandidates()).isEqualTo(10 * 20); + assertThat(container.query().getLimit()).isEqualTo(10); + assertThat(numCandidates(container.pipeline())).isEqualTo(10 * 20); } @Test @@ -71,10 +79,10 @@ void shouldNotSetNumCandidates() throws ReflectiveOperationException { MongoQueryMethod queryMethod = getMongoQueryMethod(method); MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1)); - VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + QueryContainer container = createQueryContainer(queryMethod, accessor); - assertThat(query.query().getLimit()).isEqualTo(10); - assertThat(query.numCandidates()).isNull(); + assertThat(container.query().getLimit()).isEqualTo(10); + assertThat(numCandidates(container.pipeline())).isNull(); } @Test @@ -86,19 +94,87 @@ void shouldConsiderProvidedLimit() throws ReflectiveOperationException { MongoQueryMethod queryMethod = getMongoQueryMethod(method); MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11)); - VectorSearchDelegate.QueryMetadata query = createQueryMetadata(queryMethod, accessor); + QueryContainer container = createQueryContainer(queryMethod, accessor); - assertThat(query.query().getLimit()).isEqualTo(11); - assertThat(query.numCandidates()).isEqualTo(11 * 20); + assertThat(container.query().getLimit()).isEqualTo(11); + assertThat(numCandidates(container.pipeline())).isEqualTo(11 * 20); } - private VectorSearchDelegate.QueryMetadata createQueryMetadata(MongoQueryMethod queryMethod, - MongoParametersParameterAccessor accessor) { + @Test + void considersDerivedQueryPart() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByFirstNameAndEmbeddingNear", String.class, + Vector.class, Score.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, "spring", Vector.of(1, 2), Score.of(1)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter", + new Document("first_name", "spring")); + } + + @Test + void considersDerivedQueryPartInDifferentOrder() throws ReflectiveOperationException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNearAndFirstName", Vector.class, + Score.class, String.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), "spring"); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + assertThat(vectorSearchStageOf(container.pipeline())).containsEntry("$vectorSearch.filter", + new Document("first_name", "spring")); + } + + @Test + void defaultSortsByScore() throws NoSuchMethodException { + + Method method = VectorSearchRepository.class.getMethod("searchTop10ByEmbeddingNear", Vector.class, Score.class, + Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(10)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + + List stages = container.pipeline().lastOperation() + .toPipelineStages(TestAggregationContext.contextFor(WithVector.class)); + + assertThat(stages).containsExactly(new Document("$sort", new Document("__score__", -1))); + } + + @Test + void usesDerivedSort() throws NoSuchMethodException { + + Method method = VectorSearchRepository.class.getMethod("searchByEmbeddingNearOrderByFirstName", Vector.class, + Score.class, Limit.class); + + MongoQueryMethod queryMethod = getMongoQueryMethod(method); + MongoParametersParameterAccessor accessor = getAccessor(queryMethod, Vector.of(1, 2), Score.of(1), Limit.of(11)); + + QueryContainer container = createQueryContainer(queryMethod, accessor); + AggregationPipeline aggregationPipeline = container.pipeline(); + + List stages = aggregationPipeline.lastOperation() + .toPipelineStages(TestAggregationContext.contextFor(WithVector.class)); + + assertThat(stages).containsExactly(new Document("$sort", new Document("first_name", 1).append("__score__", -1))); + } + + Document vectorSearchStageOf(AggregationPipeline pipeline) { + return pipeline.firstOperation().toPipelineStages(TestAggregationContext.contextFor(WithVector.class)).get(0); + } + + private QueryContainer createQueryContainer(MongoQueryMethod queryMethod, MongoParametersParameterAccessor accessor) { VectorSearchDelegate delegate = new VectorSearchDelegate(queryMethod, converter, ValueExpressionDelegate.create()); - return delegate.createQuery(mock(ValueExpressionEvaluator.class), queryMethod.getResultProcessor(), accessor, - Object.class, new ParameterBindingDocumentCodec(), mock(ParameterBindingContext.class)); + return delegate.createQuery(mock(ValueExpressionEvaluator.class), queryMethod.getResultProcessor(), accessor, null, + new ParameterBindingDocumentCodec(), mock(ParameterBindingContext.class)); } private MongoQueryMethod getMongoQueryMethod(Method method) { @@ -110,21 +186,69 @@ private static MongoParametersParameterAccessor getAccessor(MongoQueryMethod que return new MongoParametersParameterAccessor(queryMethod, values); } + @Nullable + private static Integer numCandidates(AggregationPipeline pipeline) { + + Document $vectorSearch = pipeline.firstOperation().toPipelineStages(Aggregation.DEFAULT_CONTEXT).get(0); + if ($vectorSearch.containsKey("$vectorSearch")) { + Object value = $vectorSearch.get("$vectorSearch", Document.class).get("numCandidates"); + return value instanceof Number i ? i.intValue() : null; + } + return null; + } + interface VectorSearchRepository extends Repository { @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity); + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByFirstNameAndEmbeddingNear(String firstName, Vector vector, Score similarity); + + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchTop10ByEmbeddingNearAndFirstName(Vector vector, Score similarity, String firstname); + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ENN) SearchResults searchTop10EnnByEmbeddingNear(Vector vector, Score similarity); @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity, Limit limit); + @VectorSearch(indexName = "cos-index", searchType = VectorSearchOperation.SearchType.ANN) + SearchResults searchByEmbeddingNearOrderByFirstName(Vector vector, Score similarity, Limit limit); + } static class WithVector { Vector embedding; + + String lastName; + + @Field("first_name") String firstName; + + public Vector getEmbedding() { + return embedding; + } + + public void setEmbedding(Vector embedding) { + this.embedding = embedding; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } } } diff --git a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc index 345b5dbb6c..7fc51de007 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/mongo-search-indexes.adoc @@ -25,7 +25,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes",role="primary"] ---- VectorIndex index = new VectorIndex("vector_index") - .addVector("plotEmbedding"), vector -> vector.dimensions(1536).similarity(COSINE)) <1> + .addVector("plotEmbedding", vector -> vector.dimensions(1536).similarity(COSINE)) <1> .addFilter("year"); <2> mongoTemplate.searchIndexOps(Movie.class) <3> diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc index 752ffad622..252437f0b7 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-annotated-include.adoc @@ -6,13 +6,13 @@ Annotated search methods use the `@VectorSearch` annotation to define parameters ---- interface CommentRepository extends Repository { - @VectorSearch(indexName = "cos-index", filter = "{country: ?0}") - SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + @VectorSearch(indexName = "cos-index", filter = "{country: ?0}", limit="100", numCandidates="2000") + SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance); - @VectorSearch(indexName = "my-index", filter = "{country: ?0}", numCandidates = "#{#limit * 20}", + @VectorSearch(indexName = "my-index", filter = "{country: ?0}", limit="?3", numCandidates = "#{#limit * 20}", searchType = VectorSearchOperation.SearchType.ANN) - List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, int limit); + List findAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, Score distance, int limit); } ---- ==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc index dd06ee699a..f2b006b8e4 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-method-derived-include.adoc @@ -6,14 +6,14 @@ MongoDB Search methods must use the `@VectorSearch` annotation to define the ind ---- interface CommentRepository extends Repository { - @VectorSearch(indexName = "my-index") - SearchResults searchByEmbeddingNear(Vector vector, Score score); + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score score); - @VectorSearch(indexName = "my-index") - SearchResults searchByEmbeddingWithin(Vector vector, Range range); + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByEmbeddingWithin(Vector vector, Range range); - @VectorSearch(indexName = "my-index") - SearchResults searchByCountryAndEmbeddingWithin(String country, Vector vector, Range range); + @VectorSearch(indexName = "my-index", numCandidates="200") + SearchResults searchTop10ByCountryAndEmbeddingWithin(String country, Vector vector, Range range); } ---- ==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc index c7ad91c9db..0e987fc1c5 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-repository-include.adoc @@ -4,12 +4,12 @@ ---- interface CommentRepository extends Repository { - @VectorSearch(indexName = "my-index") + @VectorSearch(indexName = "my-index", numCandidates="#{#limit.max() * 20}") SearchResults searchByCountryAndEmbeddingNear(String country, Vector vector, Score score, Limit limit); - @VectorSearch(indexName = "my-index") - SearchResults searchAnnotatedByCountryAndEmbeddingWithin(String country, Vector embedding, + @VectorSearch(indexName = "my-index", limit="10", numCandidates="200") + SearchResults searchByCountryAndEmbeddingWithin(String country, Vector embedding, Score score); } @@ -17,3 +17,9 @@ interface CommentRepository extends Repository { SearchResults results = repository.searchByCountryAndEmbeddingNear("en", Vector.of(…), Score.of(0.9), Limit.of(10)); ---- ==== + +[TIP] +==== +The MongoDB https://www.mongodb.com/docs/atlas/atlas-vector-search/vector-search-stage/[vector search aggregation] stage defines a set of required arguments and restrictions. +Please make sure to follow the guidelines and make sure to provide required arguments like `limit`. +==== diff --git a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc index b97475b467..313d8bf394 100644 --- a/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc +++ b/src/main/antora/modules/ROOT/partials/vector-search-scoring-include.adoc @@ -9,13 +9,13 @@ The scoring function defaults to `ScoringFunction.unspecified()` as there is no interface CommentRepository extends Repository { @VectorSearch(…) - SearchResults searchByEmbeddingNear(Vector vector, Score similarity); + SearchResults searchTop10ByEmbeddingNear(Vector vector, Score similarity); @VectorSearch(…) - SearchResults searchByEmbeddingNear(Vector vector, Similarity similarity); + SearchResults searchTop10ByEmbeddingNear(Vector vector, Similarity similarity); @VectorSearch(…) - SearchResults searchByEmbeddingNear(Vector vector, Range range); + SearchResults searchTop10ByEmbeddingNear(Vector vector, Range range); } repository.searchByEmbeddingNear(Vector.of(…), Score.of(0.9)); <1> From 405c2ebe1468e944281e7a7da937f33d6fda4402 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 08:54:44 +0200 Subject: [PATCH 62/74] Update CI Properties. See #4956 --- ci/pipeline.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/pipeline.properties b/ci/pipeline.properties index 8dd2295acc..a79feac95d 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -1,5 +1,5 @@ # Java versions -java.main.tag=17.0.15_6-jdk-focal +java.main.tag=24.0.1_9-jdk-noble java.next.tag=24.0.1_9-jdk-noble # Docker container images - standard From 15c24d13c3d42e3df9ce81aa7048e86443383808 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 11:03:44 +0200 Subject: [PATCH 63/74] Remove MongoDB 6.0 docker build. See #4956 --- Jenkinsfile | 52 +++---------------- ci/openjdk17-mongodb-6.0/Dockerfile | 25 --------- ci/openjdk17-mongodb-7.0/Dockerfile | 25 --------- .../Dockerfile | 0 ci/pipeline.properties | 2 - 5 files changed, 6 insertions(+), 98 deletions(-) delete mode 100644 ci/openjdk17-mongodb-6.0/Dockerfile delete mode 100644 ci/openjdk17-mongodb-7.0/Dockerfile rename ci/{openjdk23-mongodb-8.0 => openjdk24-mongodb-8.0}/Dockerfile (100%) diff --git a/Jenkinsfile b/Jenkinsfile index 0e83b47e2f..ce2f272334 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -20,29 +20,10 @@ pipeline { stages { stage("Docker images") { parallel { - stage('Publish JDK (Java 17) + MongoDB 6.0') { - when { - anyOf { - changeset "ci/openjdk17-mongodb-6.0/**" - changeset "ci/pipeline.properties" - } - } - agent { label 'data' } - options { timeout(time: 30, unit: 'MINUTES') } - - steps { - script { - def image = docker.build("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.6.0.version']} ci/openjdk17-mongodb-6.0/") - docker.withRegistry(p['docker.registry'], p['docker.credentials']) { - image.push() - } - } - } - } - stage('Publish JDK (Java 17) + MongoDB 7.0') { + stage('Publish JDK (Java 24) + MongoDB 8.0') { when { anyOf { - changeset "ci/openjdk17-mongodb-7.0/**" + changeset "ci/openjdk24-mongodb-8.0/**" changeset "ci/pipeline.properties" } } @@ -51,7 +32,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk17-mongodb-7.0/") + def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}", "--build-arg BASE=${p['docker.java.main.image']} --build-arg MONGODB=${p['docker.mongodb.7.0.version']} ci/openjdk24-mongodb-8.0/") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -61,7 +42,7 @@ pipeline { stage('Publish JDK (Java.next) + MongoDB 8.0') { when { anyOf { - changeset "ci/openjdk17-mongodb-8.0/**" + changeset "ci/openjdk24-mongodb-8.0/**" changeset "ci/pipeline.properties" } } @@ -70,7 +51,7 @@ pipeline { steps { script { - def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk23-mongodb-8.0/") + def image = docker.build("springci/spring-data-with-mongodb-8.0:${p['java.next.tag']}", "--build-arg BASE=${p['docker.java.next.image']} --build-arg MONGODB=${p['docker.mongodb.8.0.version']} ci/openjdk24-mongodb-8.0/") docker.withRegistry(p['docker.registry'], p['docker.credentials']) { image.push() } @@ -99,7 +80,7 @@ pipeline { steps { script { docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { - docker.image("springci/spring-data-with-mongodb-6.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { + docker.image("springci/spring-data-with-mongodb-8.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { sh 'ci/start-replica.sh' sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + "./mvnw -s settings.xml -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-mongodb clean dependency:list test -Dsort -U -B" @@ -118,27 +99,6 @@ pipeline { } } parallel { - stage("test: MongoDB 7.0 (main)") { - agent { - label 'data' - } - options { timeout(time: 30, unit: 'MINUTES') } - environment { - ARTIFACTORY = credentials("${p['artifactory.credentials']}") - DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") - } - steps { - script { - docker.withRegistry(p['docker.proxy.registry'], p['docker.proxy.credentials']) { - docker.image("springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}").inside(p['docker.java.inside.docker']) { - sh 'ci/start-replica.sh' - sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + - "./mvnw -s settings.xml -Ddevelocity.storage.directory=/tmp/jenkins-home/.develocity-root -Dmaven.repo.local=/tmp/jenkins-home/.m2/spring-data-mongodb clean dependency:list test -Dsort -U -B" - } - } - } - } - } stage("test: MongoDB 8.0") { agent { diff --git a/ci/openjdk17-mongodb-6.0/Dockerfile b/ci/openjdk17-mongodb-6.0/Dockerfile deleted file mode 100644 index fd2580e23a..0000000000 --- a/ci/openjdk17-mongodb-6.0/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG BASE -FROM ${BASE} -# Any ARG statements before FROM are cleared. -ARG MONGODB - -ENV TZ=Etc/UTC -ENV DEBIAN_FRONTEND=noninteractive -ENV MONGO_VERSION=${MONGODB} - -RUN set -eux; \ - sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ - sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ - sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ - sed -i -e 's/http/https/g' /etc/apt/sources.list && \ - apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \ - # MongoDB 6.0 release signing key - wget -qO - https://www.mongodb.org/static/pgp/server-6.0.asc | apt-key add - && \ - # Needed when MongoDB creates a 6.0 folder. - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/6.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-6.0.list && \ - echo ${TZ} > /etc/timezone - -RUN apt-get update && \ - apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* diff --git a/ci/openjdk17-mongodb-7.0/Dockerfile b/ci/openjdk17-mongodb-7.0/Dockerfile deleted file mode 100644 index 5701ab9fbc..0000000000 --- a/ci/openjdk17-mongodb-7.0/Dockerfile +++ /dev/null @@ -1,25 +0,0 @@ -ARG BASE -FROM ${BASE} -# Any ARG statements before FROM are cleared. -ARG MONGODB - -ENV TZ=Etc/UTC -ENV DEBIAN_FRONTEND=noninteractive -ENV MONGO_VERSION=${MONGODB} - -RUN set -eux; \ - sed -i -e 's/archive.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ - sed -i -e 's/security.ubuntu.com/mirror.one.com/g' /etc/apt/sources.list && \ - sed -i -e 's/ports.ubuntu.com/mirrors.ocf.berkeley.edu/g' /etc/apt/sources.list && \ - sed -i -e 's/http/https/g' /etc/apt/sources.list && \ - apt-get update && apt-get install -y apt-transport-https apt-utils gnupg2 wget && \ - # MongoDB 6.0 release signing key - wget -qO - https://www.mongodb.org/static/pgp/server-7.0.asc | apt-key add - && \ - # Needed when MongoDB creates a 7.0 folder. - echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu focal/mongodb-org/7.0 multiverse" | tee /etc/apt/sources.list.d/mongodb-org-7.0.list && \ - echo ${TZ} > /etc/timezone - -RUN apt-get update && \ - apt-get install -y mongodb-org=${MONGODB} mongodb-org-server=${MONGODB} mongodb-org-shell=${MONGODB} mongodb-org-mongos=${MONGODB} mongodb-org-tools=${MONGODB} && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* diff --git a/ci/openjdk23-mongodb-8.0/Dockerfile b/ci/openjdk24-mongodb-8.0/Dockerfile similarity index 100% rename from ci/openjdk23-mongodb-8.0/Dockerfile rename to ci/openjdk24-mongodb-8.0/Dockerfile diff --git a/ci/pipeline.properties b/ci/pipeline.properties index a79feac95d..4beebb0dfe 100644 --- a/ci/pipeline.properties +++ b/ci/pipeline.properties @@ -7,8 +7,6 @@ docker.java.main.image=library/eclipse-temurin:${java.main.tag} docker.java.next.image=library/eclipse-temurin:${java.next.tag} # Supported versions of MongoDB -docker.mongodb.6.0.version=6.0.23 -docker.mongodb.7.0.version=7.0.20 docker.mongodb.8.0.version=8.0.9 # Supported versions of Redis From aaf864f6b1b13a986247e7916d78740bf7871c8c Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Mon, 12 May 2025 11:36:38 +0200 Subject: [PATCH 64/74] Polishing. See #4960 --- src/main/antora/antora-playbook.yml | 2 +- .../mongodb/repositories/vector-search.adoc | 14 +- .../modules/ROOT/partials/vector-search.adoc | 167 ++++++++++++++++++ 3 files changed, 175 insertions(+), 8 deletions(-) create mode 100644 src/main/antora/modules/ROOT/partials/vector-search.adoc diff --git a/src/main/antora/antora-playbook.yml b/src/main/antora/antora-playbook.yml index 9f842fe401..497f39ee04 100644 --- a/src/main/antora/antora-playbook.yml +++ b/src/main/antora/antora-playbook.yml @@ -17,7 +17,7 @@ content: - url: https://github.com/spring-projects/spring-data-commons # Refname matching: # https://docs.antora.org/antora/latest/playbook/content-refname-matching/ - branches: [ main, 3.3.x, 3.2.x] + branches: [ main, 4.0.x ] start_path: src/main/antora asciidoc: attributes: diff --git a/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc index 2e590107ec..9129c80a21 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/repositories/vector-search.adoc @@ -1,8 +1,8 @@ -:vector-search-intro-include: data-mongodb::partial$vector-search-intro-include.adoc -:vector-search-model-include: data-mongodb::partial$vector-search-model-include.adoc -:vector-search-repository-include: data-mongodb::partial$vector-search-repository-include.adoc -:vector-search-scoring-include: data-mongodb::partial$vector-search-scoring-include.adoc -:vector-search-method-derived-include: data-mongodb::partial$vector-search-method-derived-include.adoc -:vector-search-method-annotated-include: data-mongodb::partial$vector-search-method-annotated-include.adoc +:vector-search-intro-include: partial$vector-search-intro-include.adoc +:vector-search-model-include: partial$vector-search-model-include.adoc +:vector-search-repository-include: partial$vector-search-repository-include.adoc +:vector-search-scoring-include: partial$vector-search-scoring-include.adoc +:vector-search-method-derived-include: partial$vector-search-method-derived-include.adoc +:vector-search-method-annotated-include: partial$vector-search-method-annotated-include.adoc -include::{commons}@data-commons::page$repositories/vector-search.adoc[] +include::partial$/vector-search.adoc[] diff --git a/src/main/antora/modules/ROOT/partials/vector-search.adoc b/src/main/antora/modules/ROOT/partials/vector-search.adoc new file mode 100644 index 0000000000..15e32dccee --- /dev/null +++ b/src/main/antora/modules/ROOT/partials/vector-search.adoc @@ -0,0 +1,167 @@ +[[vector-search]] += Vector Search + +With the rise of Generative AI, Vector databases have gained strong traction in the world of databases. +These databases enable efficient storage and querying of high-dimensional vectors, making them well-suited for tasks such as semantic search, recommendation systems, and natural language understanding. + +Vector search is a technique that retrieves semantically similar data by comparing vector representations (also known as embeddings) rather than relying on traditional exact-match queries. +This approach enables intelligent, context-aware applications that go beyond keyword-based retrieval. + +In the context of Spring Data, vector search opens new possibilities for building intelligent, context-aware applications, particularly in domains like natural language processing, recommendation systems, and generative AI. +By modelling vector-based querying using familiar repository abstractions, Spring Data allows developers to seamlessly integrate similarity-based vector-capable databases with the simplicity and consistency of the Spring Data programming model. + +ifdef::vector-search-intro-include[] +include::{vector-search-intro-include}[] +endif::[] + +[[vector-search.model]] +== Vector Model + +To support vector search in a type-safe and idiomatic way, Spring Data introduces the following core abstractions: + +* <> +* <` and `SearchResult`>> +* <> + +[[vector-search.model.vector]] +=== `Vector` + +The `Vector` type represents an n-dimensional numerical embedding, typically produced by embedding models. +In Spring Data, it is defined as a lightweight wrapper around an array of floating-point numbers, ensuring immutability and consistency. +This type can be used as an input for search queries or as a property on a domain entity to store the associated vector representation. + +==== +[source,java] +---- +Vector vector = Vector.of(0.23f, 0.11f, 0.77f); +---- +==== + +Using `Vector` in your domain model removes the need to work with raw arrays or lists of numbers, providing a more type-safe and expressive way to handle vector data. +This abstraction also allows for easy integration with various vector databases and libraries. +It also allows for implementing vendor-specific optimizations such as binary or quantized vectors that do not map to a standard floating point (`float` and `double` as of https://en.wikipedia.org/wiki/IEEE_754[IEEE 754]) representation. +A domain object can have a vector property, which can be used for similarity searches. +Consider the following example: + +ifdef::vector-search-model-include[] +include::{vector-search-model-include}[] +endif::[] + +NOTE: Associating a vector with a domain object results in the vector being loaded and stored as part of the entity lifecycle, which may introduce additional overhead on retrieval and persistence operations. + +[[vector-search.model.search-result]] +=== Search Results + +The `SearchResult` type encapsulates the results of a vector similarity query. +It includes both the matched domain object and a relevance score that indicates how closely it matches the query vector. +This abstraction provides a structured way to handle result ranking and enables developers to easily work with both the data and its contextual relevance. + +ifdef::vector-search-repository-include[] +include::{vector-search-repository-include}[] +endif::[] + +In this example, the `searchByCountryAndEmbeddingNear` method returns a `SearchResults` object, which contains a list of `SearchResult` instances. +Each result includes the matched `Comment` entity and its relevance score. + +Relevance score is a numerical value that indicates how closely the matched vector aligns with the query vector. +Depending on whether a score represents distance or similarity a higher score can mean a closer match or a more distant one. + +The scoring function used to calculate this score can vary based on the underlying database, index or input parameters. + +[[vector-search.model.scoring]] +=== Score, Similarity, and Scoring Functions + +The `Score` type holds a numerical value indicating the relevance of a search result. +It can be used to rank results based on their similarity to the query vector. +The `Score` type is typically a floating-point number, and its interpretation (higher is better or lower is better) depends on the specific similarity function used. +Scores are a by-product of vector search and are not required for a successful search operation. +Score values are not part of a domain model and therefore represented best as out-of-band data. + +Generally, a Score is computed by a `ScoringFunction`. +The actual scoring function used to calculate this score can depends on the underlying database and can be obtained from a search index or input parameters. + +Spring Data support declares constants for commonly used functions such as: + +Euclidean Distance:: Calculates the straight-line distance in n-dimensional space involving the square root of the sum of squared differences. +Cosine Similarity:: Measures the angle between two vectors by calculating the Dot product first and then normalizing its result by dividing by the product of their lengths. +Dot Product:: Computes the sum of element-wise multiplications. + +The choice of similarity function can impact both the performance and semantics of the search and is often determined by the underlying database or index being used. +Spring Data adopts to the database's native scoring function capabilities and whether the score can be used to limit results. + +ifdef::vector-search-scoring-include[] +include::{vector-search-scoring-include}[] +endif::[] + +[[vector-search.methods]] +== Vector Search Methods + +Vector search methods are defined in repositories using the same conventions as standard Spring Data query methods. +These methods return `SearchResults` and require a `Vector` parameter to define the query vector. +The actual implementation depends on the actual internals of the underlying data store and its capabilities around vector search. + +NOTE: If you are new to Spring Data repositories, make sure to familiarize yourself with the xref:repositories/core-concepts.adoc[basics of repository definitions and query methods]. + +Generally, you have the choice of declaring a search method using two approaches: + +* Query Derivation +* Declaring a String-based Query + +Vector Search methods must declare a `Vector` parameter to define the query vector. + +[[vector-search.method.derivation]] +=== Derived Search Methods + +A derived search method uses the name of the method to derive the query. +Vector Search supports the following keywords to run a Vector search when declaring a search method: + +.Query predicate keywords +[options="header",cols="1,3"] +|=============== +|Logical keyword|Keyword expressions +|`NEAR`|`Near`, `IsNear` +|`WITHIN`|`Within`, `IsWithin` +|=============== + +ifdef::vector-search-method-derived-include[] +include::{vector-search-method-derived-include}[] +endif::[] + +Derived search methods are typically easier to read and maintain, as they rely on the method name to express the query intent. +However, a derived search method requires either to declare a `Score`, `Range` or `ScoreFunction` as second argument to the `Near`/`Within` keyword to limit search results by their score. + +[[vector-search.method.string]] +=== Annotated Search Methods + +Annotated methods provide full control over the query semantics and parameters. +Unlike derived methods, they do not rely on method name conventions. + +ifdef::vector-search-method-annotated-include[] +include::{vector-search-method-annotated-include}[] +endif::[] + +With more control over the actual query, Spring Data can make fewer assumptions about the query and its parameters. +For example, `Similarity` normalization uses the native score function within the query to normalize the given similarity into a score predicate value and vice versa. +If an annotated query does not define e.g. the score, then the score value in the returned `SearchResult` will be zero. + +[[vector-search.method.sorting]] +=== Sorting + +By default, search results are ordered according to their score. +You can override sorting by using the `Sort` parameter: + +.Using `Sort` in Repository Search Methods +==== +[source,java] +---- +interface CommentRepository extends Repository { + + SearchResults searchByEmbeddingNearOrderByCountry(Vector vector, Score score); + + SearchResults searchByEmbeddingWithin(Vector vector, Score score, Sort sort); +} +---- +==== + +Please note that custom sorting does not allow expressing the score as a sorting criteria. +You can only refer to domain properties. From b0d8a55cd02c74549d1b6fa1265d1576a21a906e Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 May 2025 12:31:44 +0200 Subject: [PATCH 65/74] Implement `RepositoryFactorySupport.getEntityInformation(RepositoryMetadata)` instead of private overload. Overriding the proper variant of EntityInformation is now possible because we no longer utilize a private method in addition to the public one leading to partial customization of EntityInformation. Closes #4967 --- .../support/MongoRepositoryFactory.java | 20 +++++++++---------- .../ReactiveMongoRepositoryFactory.java | 20 +++++++++---------- .../event/ApplicationContextEventTests.java | 3 ++- .../MongoRepositoryFactoryUnitTests.java | 7 ++----- ...ongoPredicateExecutorIntegrationTests.java | 6 ++++-- ...veQuerydslMongoPredicateExecutorTests.java | 6 ++++-- 6 files changed, 31 insertions(+), 31 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java index 07268cce2c..a309cea0a3 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactory.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.repository.support; -import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; @@ -120,14 +119,13 @@ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata * @since 3.2.1 */ protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata, MongoOperations operations) { - return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType()), operations); + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), operations); } @Override protected Object getTargetRepository(RepositoryInformation information) { - MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), - information); + MongoEntityInformation entityInformation = getEntityInformation(information); Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations); if (targetRepository instanceof SimpleMongoRepository repository) { @@ -143,16 +141,18 @@ protected Optional getQueryLookupStrategy(@Nullable Key key return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } + @Deprecated + @Override public MongoEntityInformation getEntityInformation(Class domainClass) { - return getEntityInformation(domainClass, null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + return MongoEntityInformationSupport.entityInformationFor(entity, null); } - private MongoEntityInformation getEntityInformation(Class domainClass, - @Nullable RepositoryMetadata metadata) { + @Override + public MongoEntityInformation getEntityInformation(RepositoryMetadata metadata) { - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); - return MongoEntityInformationSupport. entityInformationFor(entity, - metadata != null ? metadata.getIdType() : null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType()); } /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java index 1b5c218ce7..ce9820a4d9 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/support/ReactiveMongoRepositoryFactory.java @@ -15,7 +15,6 @@ */ package org.springframework.data.mongodb.repository.support; -import java.io.Serializable; import java.lang.reflect.Method; import java.util.Optional; @@ -118,14 +117,14 @@ protected Class getRepositoryBaseClass(RepositoryMetadata metadata) { */ @Override protected RepositoryFragments getRepositoryFragments(RepositoryMetadata metadata) { - return fragmentsContributor.contribute(metadata, getEntityInformation(metadata.getDomainType(), metadata), + return fragmentsContributor.contribute(metadata, getEntityInformation(metadata), operations); } @Override protected Object getTargetRepository(RepositoryInformation information) { - MongoEntityInformation entityInformation = getEntityInformation(information.getDomainType(), + MongoEntityInformation entityInformation = getEntityInformation( information); Object targetRepository = getTargetRepositoryViaReflection(information, entityInformation, operations); @@ -143,19 +142,18 @@ protected Optional getQueryLookupStrategy(Key key, return Optional.of(new MongoQueryLookupStrategy(operations, mappingContext, valueExpressionDelegate)); } + @Deprecated @Override public MongoEntityInformation getEntityInformation(Class domainClass) { - return getEntityInformation(domainClass, null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + return MongoEntityInformationSupport.entityInformationFor(entity, null); } - @SuppressWarnings("unchecked") - private MongoEntityInformation getEntityInformation(Class domainClass, - @Nullable RepositoryMetadata metadata) { - - MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(domainClass); + @Override + public MongoEntityInformation getEntityInformation(RepositoryMetadata metadata) { - return new MappingMongoEntityInformation<>((MongoPersistentEntity) entity, - metadata != null ? (Class) metadata.getIdType() : null); + MongoPersistentEntity entity = mappingContext.getRequiredPersistentEntity(metadata.getDomainType()); + return MongoEntityInformationSupport.entityInformationFor(entity, metadata.getIdType()); } /** diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java index 9bc1dc78aa..1d44bff5ad 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/mapping/event/ApplicationContextEventTests.java @@ -408,7 +408,8 @@ public void publishesEventsForQuerydslFindQueries() { template.save(new Person("Boba", "Fett", 40)); MongoRepositoryFactory factory = new MongoRepositoryFactory(template); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); QuerydslMongoPredicateExecutor executor = new QuerydslMongoPredicateExecutor<>(entityInformation, template); executor.findOne(QPerson.person.lastname.startsWith("Fe")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java index c40f24dacb..e614ef4e04 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java @@ -31,9 +31,7 @@ import org.mockito.junit.jupiter.MockitoSettings; import org.mockito.quality.Strictness; -import org.springframework.data.mapping.context.MappingContext; import org.springframework.data.mongodb.core.MongoOperations; -import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.convert.MongoConverter; import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver; @@ -41,9 +39,8 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.ReadPreference; -import org.springframework.data.mongodb.repository.query.MongoEntityInformation; import org.springframework.data.repository.ListCrudRepository; -import org.springframework.data.repository.Repository; +import org.springframework.data.repository.core.EntityInformation; /** * Unit test for {@link MongoRepositoryFactory}. @@ -69,7 +66,7 @@ public void setUp() { public void usesMappingMongoEntityInformationIfMappingContextSet() { MongoRepositoryFactory factory = new MongoRepositoryFactory(template); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + EntityInformation entityInformation = factory.getEntityInformation(Person.class); assertThat(entityInformation instanceof MappingMongoEntityInformation).isTrue(); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java index 7d9024e2fb..5f0800aba6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/QuerydslMongoPredicateExecutorIntegrationTests.java @@ -75,7 +75,8 @@ public class QuerydslMongoPredicateExecutorIntegrationTests { public void setup() { MongoRepositoryFactory factory = new MongoRepositoryFactory(operations); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new QuerydslMongoPredicateExecutor<>(entityInformation, operations); operations.dropCollection(Person.class); @@ -246,7 +247,8 @@ protected MongoDatabase doGetDatabase() { }; MongoRepositoryFactory factory = new MongoRepositoryFactory(ops); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new QuerydslMongoPredicateExecutor<>(entityInformation, ops); repository.findOne(person.firstname.contains("batman")); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java index 807b7aec22..c807a1bcbd 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/ReactiveQuerydslMongoPredicateExecutorTests.java @@ -111,7 +111,8 @@ public static void cleanDb() { public void setup() { ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(operations); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, operations); dave = new Person("Dave", "Matthews", 42); @@ -326,7 +327,8 @@ protected Mono doGetDatabase() { }; ReactiveMongoRepositoryFactory factory = new ReactiveMongoRepositoryFactory(ops); - MongoEntityInformation entityInformation = factory.getEntityInformation(Person.class); + MongoEntityInformation entityInformation = factory + .getEntityInformation(Person.class); repository = new ReactiveQuerydslMongoPredicateExecutor<>(entityInformation, ops); repository.findOne(person.firstname.contains("batman")) // From 95e0ec3b086faec8a6fb2899eb6d9c705c5e26ba Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Tue, 13 May 2025 12:39:14 +0200 Subject: [PATCH 66/74] Polishing. Use weaker test visibility. See #4967 --- .../support/MongoRepositoryFactoryUnitTests.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java index e614ef4e04..3d0e468155 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/support/MongoRepositoryFactoryUnitTests.java @@ -51,19 +51,19 @@ */ @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) -public class MongoRepositoryFactoryUnitTests { +class MongoRepositoryFactoryUnitTests { @Mock MongoOperations template; - MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); + private MongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, new MongoMappingContext()); @BeforeEach - public void setUp() { + void setUp() { when(template.getConverter()).thenReturn(converter); } @Test - public void usesMappingMongoEntityInformationIfMappingContextSet() { + void usesMappingMongoEntityInformationIfMappingContextSet() { MongoRepositoryFactory factory = new MongoRepositoryFactory(template); EntityInformation entityInformation = factory.getEntityInformation(Person.class); @@ -71,7 +71,7 @@ public void usesMappingMongoEntityInformationIfMappingContextSet() { } @Test // DATAMONGO-385 - public void createsRepositoryWithIdTypeLong() { + void createsRepositoryWithIdTypeLong() { MongoRepositoryFactory factory = new MongoRepositoryFactory(template); MyPersonRepository repository = factory.getRepository(MyPersonRepository.class); From 0e606d26bf9cc4b3780753a482cbfefe5713c98b Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 14 May 2025 11:03:43 +0200 Subject: [PATCH 67/74] Add AOT support for dynamic projections, streaming/scroll queries and Meta annotation. Closes: #4970 --- .../repository/aot/MongoCodeBlocks.java | 62 +++++++++---- .../aot/MongoRepositoryContributor.java | 7 +- .../test/java/example/aot/UserRepository.java | 13 ++- .../AotFragmentTestConfigurationSupport.java | 7 +- .../aot/MongoRepositoryContributorTests.java | 43 +++++++++ .../MongoRepositoryContributorUnitTests.java | 90 +++++++++++++++++++ .../aot/StubRepositoryInformation.java | 5 ++ .../aot/TestMongoAotRepositoryContext.java | 6 ++ .../modules/ROOT/pages/mongodb/aot.adoc | 7 +- 9 files changed, 208 insertions(+), 32 deletions(-) create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index 7afa2a5f53..8ac187cb7a 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -23,6 +23,7 @@ import org.bson.Document; import org.jspecify.annotations.NullUnmarked; import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotation; import org.springframework.data.domain.SliceImpl; import org.springframework.data.domain.Sort.Order; @@ -40,6 +41,7 @@ import org.springframework.data.mongodb.core.query.BasicUpdate; import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.repository.Hint; +import org.springframework.data.mongodb.repository.Meta; import org.springframework.data.mongodb.repository.ReadPreference; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.DeleteExecution; import org.springframework.data.mongodb.repository.query.MongoQueryExecution.PagedExecution; @@ -256,15 +258,13 @@ CodeBlock build() { updateReference); } else if (ClassUtils.isAssignable(Long.class, returnType)) { builder.addStatement("return $L.matching($L).apply($L).all().getModifiedCount()", - context.localVariable("updater"), queryVariableName, - updateReference); + context.localVariable("updater"), queryVariableName, updateReference); } else { builder.addStatement("$T $L = $L.matching($L).apply($L).all().getModifiedCount()", Long.class, - context.localVariable("modifiedCount"), context.localVariable("updater"), - queryVariableName, updateReference); + context.localVariable("modifiedCount"), context.localVariable("updater"), queryVariableName, + updateReference); builder.addStatement("return $T.convertNumberToTargetClass($L, $T.class)", NumberUtils.class, - context.localVariable("modifiedCount"), - returnType); + context.localVariable("modifiedCount"), returnType); } return builder.build(); @@ -319,11 +319,9 @@ CodeBlock build() { Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, - context.localVariable("results"), mongoOpsRef, - aggregationVariableName, outputType); + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); if (!queryMethod.isCollectionQuery()) { - builder.addStatement( - "return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", + builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", CollectionUtils.class, returnType, returnType, context.localVariable("results")); } else { builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType, @@ -332,8 +330,7 @@ CodeBlock build() { } else { if (queryMethod.isSliceQuery()) { builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, - context.localVariable("results"), mongoOpsRef, - aggregationVariableName, outputType); + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); builder.addStatement("boolean $L = $L.getMappedResults().size() > $L.getPageSize()", context.localVariable("hasNext"), context.localVariable("results"), context.getPageableParameterName()); builder.addStatement( @@ -378,12 +375,16 @@ CodeBlock build() { boolean isProjecting = context.getReturnedType().isProjecting(); Class domainType = context.getRepositoryInformation().getDomainType(); - Object actualReturnType = isProjecting ? context.getActualReturnType().getType() + Object actualReturnType = queryMethod.getParameters().hasDynamicProjection() || isProjecting + ? TypeName.get(context.getActualReturnType().getType()) : domainType; builder.add("\n"); - if (isProjecting) { + if (queryMethod.getParameters().hasDynamicProjection()) { + builder.addStatement("$T<$T> $L = $L.query($T.class).as($L)", FindWithQuery.class, actualReturnType, + context.localVariable("finder"), mongoOpsRef, domainType, context.getDynamicProjectionParameterName()); + } else if (isProjecting) { builder.addStatement("$T<$T> $L = $L.query($T.class).as($T.class)", FindWithQuery.class, actualReturnType, context.localVariable("finder"), mongoOpsRef, domainType, actualReturnType); } else { @@ -400,6 +401,8 @@ CodeBlock build() { terminatingMethod = "count()"; } else if (query.isExists()) { terminatingMethod = "exists()"; + } else if (queryMethod.isStreamQuery()) { + terminatingMethod = "stream()"; } else { terminatingMethod = Optional.class.isAssignableFrom(context.getReturnType().toClass()) ? "one()" : "oneValue()"; } @@ -410,6 +413,12 @@ CodeBlock build() { } else if (queryMethod.isSliceQuery()) { builder.addStatement("return new $T($L, $L).execute($L)", SlicedExecution.class, context.localVariable("finder"), context.getPageableParameterName(), query.name()); + } else if (queryMethod.isScrollQuery()) { + + String scrollPositionParameterName = context.getScrollPositionParameterName(); + + builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), + scrollPositionParameterName); } else { builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), terminatingMethod); @@ -544,8 +553,7 @@ private CodeBlock aggregationOptions(String aggregationVariableName) { Builder optionsBuilder = CodeBlock.builder(); optionsBuilder.add("$T $L = $T.builder()\n", AggregationOptions.class, - context.localVariable("aggregationOptions"), - AggregationOptions.class); + context.localVariable("aggregationOptions"), AggregationOptions.class); optionsBuilder.indent(); for (CodeBlock optionBlock : options) { optionsBuilder.add(optionBlock); @@ -709,7 +717,27 @@ CodeBlock build() { com.mongodb.ReadPreference.class, readPreference); } - // TODO: Meta annotation + MergedAnnotation metaAnnotation = context.getAnnotation(Meta.class); + + if (metaAnnotation.isPresent()) { + + long maxExecutionTimeMs = metaAnnotation.getLong("maxExecutionTimeMs"); + if (maxExecutionTimeMs != -1) { + builder.addStatement("$L.maxTimeMsec($L)", queryVariableName, maxExecutionTimeMs); + } + + int cursorBatchSize = metaAnnotation.getInt("cursorBatchSize"); + if (cursorBatchSize != 0) { + builder.addStatement("$L.cursorBatchSize($L)", queryVariableName, cursorBatchSize); + } + + String comment = metaAnnotation.getString("comment"); + if (StringUtils.hasText("comment")) { + builder.addStatement("$L.comment($S)", queryVariableName, comment); + } + } + + // TODO: Meta annotation: Disk usage return builder.build(); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index 6d0596815a..354f4a6296 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -48,6 +48,7 @@ * MongoDB specific {@link RepositoryContributor}. * * @author Christoph Strobl + * @author Mark Paluch * @since 5.0 */ public class MongoRepositoryContributor extends RepositoryContributor { @@ -159,8 +160,7 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor private static boolean backoff(MongoQueryMethod method) { - boolean skip = method.isGeoNearQuery() || method.isScrollQuery() || method.isStreamQuery() - || method.isSearchQuery(); + boolean skip = method.isGeoNearQuery() || method.isSearchQuery(); if (skip && logger.isDebugEnabled()) { logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming, search or scrolling query" @@ -225,8 +225,7 @@ private static MethodContributor aggregationUpdateMethodContri .usingAggregationVariableName(updateVariableName).pipelineOnly(true).build()); builder.addStatement("$T $L = $T.from($L.getOperations())", AggregationUpdate.class, - context.localVariable("aggregationUpdate"), - AggregationUpdate.class, updateVariableName); + context.localVariable("aggregationUpdate"), AggregationUpdate.class, updateVariableName); builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) .referencingUpdate(context.localVariable("aggregationUpdate")).build()); diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index cdebb4fc50..0b62f5979b 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -22,13 +22,16 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import org.springframework.data.annotation.Id; import org.springframework.data.domain.Limit; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.repository.Aggregation; import org.springframework.data.mongodb.repository.Hint; @@ -94,8 +97,10 @@ public interface UserRepository extends CrudRepository { Slice findSliceOfUserByLastnameStartingWith(String lastname, Pageable page); - // TODO: Streaming - // TODO: Scrolling + Stream streamByLastnameStartingWith(String lastname, Sort sort, Limit limit); + + Window findTop2WindowByLastnameStartingWithOrderByUsername(String lastname, ScrollPosition scrollPosition); + // TODO: GeoQueries // TODO: TextSearch @@ -176,14 +181,14 @@ public interface UserRepository extends CrudRepository { @ReadPreference("no-such-read-preference") User findWithReadPreferenceByUsername(String username); - // TODO: hints - /* Projecting Queries */ List findUserProjectionByLastnameStartingWith(String lastname); Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page); + Page findUserProjectionByLastnameStartingWith(String lastname, Pageable page, Class projectionType); + /* Aggregations */ @Aggregation(pipeline = { // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java index 2427aec84b..eba08ecc2e 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/AotFragmentTestConfigurationSupport.java @@ -40,7 +40,7 @@ * This configuration generates the AOT repository, compiles sources and configures a BeanFactory to contain the AOT * fragment. Additionally, the fragment is exposed through a {@code repositoryInterface} JDK proxy forwarding method * invocations to the backing AOT fragment. Note that {@code repositoryInterface} is not a repository proxy. - * + * * @author Christoph Strobl */ public class AotFragmentTestConfigurationSupport implements BeanFactoryPostProcessor { @@ -62,7 +62,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) new MongoRepositoryContributor(repositoryContext).contribute(generationContext); AbstractBeanDefinition aotGeneratedRepository = BeanDefinitionBuilder - .genericBeanDefinition(repositoryInterface.getName() + "Impl__Aot") // + .genericBeanDefinition( + repositoryInterface.getPackageName() + "." + repositoryInterface.getSimpleName() + "Impl__Aot") // .addConstructorArgReference("mongoOperations") // .addConstructorArgValue(getCreationContext(repositoryContext)).getBeanDefinition(); @@ -80,6 +81,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) }).getBeanDefinition(); ((BeanDefinitionRegistry) beanFactory).registerBeanDefinition("fragmentFacade", fragmentFacade); + + beanFactory.registerSingleton("generationContext", generationContext); } private Object getFragmentFacadeProxy(Object fragment) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index d5c388751d..5a86c9658a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -34,11 +34,15 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.KeysetScrollPosition; import org.springframework.data.domain.Limit; +import org.springframework.data.domain.OffsetScrollPosition; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.ScrollPosition; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; import org.springframework.data.mongodb.core.MongoOperations; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.AggregationResults; @@ -271,6 +275,37 @@ void testDerivedFinderReturningSlice() { assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); } + @Test + void testDerivedQueryReturningStream() { + + List results = fragment.streamByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)).toList(); + + assertThat(results).hasSize(2); + assertThat(results).extracting(User::getUsername).containsExactly("han", "kylo"); + } + + @Test + void testDerivedQueryReturningWindowByOffset() { + + Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.offset()); + assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo"); + assertThat(window1.positionAt(1)).isInstanceOf(OffsetScrollPosition.class); + + Window window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1)); + assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader"); + } + + @Test + void testDerivedQueryReturningWindowByKeyset() { + + Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.keyset()); + assertThat(window1).extracting(User::getUsername).containsExactly("han", "kylo"); + assertThat(window1.positionAt(1)).isInstanceOf(KeysetScrollPosition.class); + + Window window2 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", window1.positionAt(1)); + assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader"); + } + @Test void testAnnotatedFinderReturningSingleValueWithQuery() { @@ -439,6 +474,14 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); } + @Test + void testDerivedFinderReturningPageOfDynamicProjections() { + + Page users = fragment.findUserProjectionByLastnameStartingWith("S", + PageRequest.of(0, 2, Sort.by("username")), UserProjection.class); + assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); + } + @Test void testUpdateWithDerivedQuery() { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java new file mode 100644 index 0000000000..e53b3ae679 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java @@ -0,0 +1,90 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository.aot; + +import static org.mockito.Mockito.*; +import static org.springframework.data.mongodb.test.util.Assertions.*; + +import example.aot.User; +import example.aot.UserRepository; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.aot.generate.GeneratedFiles; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.InputStreamResource; +import org.springframework.core.io.InputStreamSource; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.repository.Meta; +import org.springframework.data.mongodb.test.util.MongoClientExtension; +import org.springframework.data.repository.CrudRepository; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +/** + * Unit tests for the {@link UserRepository} fragment sources via {@link MongoRepositoryContributor}. + * + * @author Mark Paluch + */ +@ExtendWith(MongoClientExtension.class) +@SpringJUnitConfig(classes = MongoRepositoryContributorUnitTests.MongoRepositoryContributorConfiguration.class) +class MongoRepositoryContributorUnitTests { + + @Configuration + static class MongoRepositoryContributorConfiguration extends AotFragmentTestConfigurationSupport { + + public MongoRepositoryContributorConfiguration() { + super(MetaUserRepository.class); + } + + @Bean + MongoOperations mongoOperations() { + return mock(MongoOperations.class); + } + + } + + @Autowired TestGenerationContext generationContext; + + @Test + void shouldConsiderMetaAnnotation() throws IOException { + + InputStreamSource aotFragment = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.SOURCE, + MetaUserRepository.class.getPackageName().replace('.', '/') + "/MetaUserRepositoryImpl__Aot.java"); + + String content = new InputStreamResource(aotFragment).getContentAsString(StandardCharsets.UTF_8); + + assertThat(content).contains("filterQuery.maxTimeMsec(555)"); + assertThat(content).contains("filterQuery.cursorBatchSize(1234)"); + assertThat(content).contains("filterQuery.comment(\"foo\")"); + } + + interface MetaUserRepository extends CrudRepository { + + @Meta + User findAllByLastname(String lastname); + + @Meta(cursorBatchSize = 1234, comment = "foo", maxExecutionTimeMs = 555) + User findWithMetaAllByLastname(String lastname); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java index 36b01fa997..7092848c33 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/StubRepositoryInformation.java @@ -69,6 +69,11 @@ public Class getReturnedDomainClass(Method method) { return metadata.getReturnedDomainClass(method); } + @Override + public TypeInformation getReturnedDomainTypeInformation(Method method) { + return metadata.getReturnedDomainTypeInformation(method); + } + @Override public CrudMethods getCrudMethods() { return metadata.getCrudMethods(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 8100a67a64..39ce43e994 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -30,6 +30,7 @@ import org.springframework.core.test.tools.ClassFile; import org.springframework.data.mongodb.core.mapping.Document; import org.springframework.data.repository.config.AotRepositoryContext; +import org.springframework.data.repository.config.RepositoryConfigurationSource; import org.springframework.data.repository.core.RepositoryInformation; import org.springframework.data.repository.core.support.RepositoryComposition; @@ -70,6 +71,11 @@ public String getModuleName() { return "MongoDB"; } + @Override + public RepositoryConfigurationSource getConfigurationSource() { + return null; + } + @Override public Set getBasePackages() { return Set.of("org.springframework.data.dummy.repository.aot"); diff --git a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc index 345b24cb76..16dd2f9ca0 100644 --- a/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc +++ b/src/main/antora/modules/ROOT/pages/mongodb/aot.adoc @@ -66,16 +66,15 @@ These are typically all query methods that are not backed by an xref:repositorie * Query methods annotated with `@Query` (excluding those containing SpEL) * Methods annotated with `@Aggregation` * Methods using `@Update` -* `@Hint` & `@ReadPreference` support +* `@Hint`, `@Meta`, and `@ReadPreference` support * `Page`, `Slice`, and `Optional` return types * DTO Projections **Limitations** -* `@Meta` annotations are not evaluated. +* `@Meta.allowDiskUse` and `flags` are not evaluated. * Queries / Aggregations / Updates containing `SpEL` cannot be generated. * Limited `Collation` detection. -* Reserved parameter names (must not be used in method signature) `finder`, `filterQuery`, `countQuery`, `deleteQuery`, `remover` `updateDefinition`, `aggregation`, `aggregationPipeline`, `aggregationUpdate`, `aggregationOptions`, `updater`, `results`, `fields`. **Excluded methods** @@ -83,6 +82,4 @@ These are typically all query methods that are not backed by an xref:repositorie * Querydsl and Query by Example methods * Methods whose implementation would be overly complex * Query Methods obtaining MQL from a file -** Methods accepting `ScrollPosition` (e.g. `Keyset` pagination) -** Dynamic projections ** Geospatial Queries From 2e52276f39707c80f926c9a7a9a08812f9ae7cba Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 14 May 2025 15:18:11 +0200 Subject: [PATCH 68/74] Fix aggregation streams, count result conversion. See: #4939 Original Pull Request: #4970 --- .../MongoAotRepositoryFragmentSupport.java | 4 + .../repository/aot/MongoCodeBlocks.java | 77 +++++++++++++------ .../aot/MongoRepositoryContributor.java | 42 +++++++--- .../repository/aot/UpdateInteraction.java | 34 ++++++-- .../test/java/example/aot/UserRepository.java | 7 ++ .../aot/MongoRepositoryContributorTests.java | 14 +++- .../aot/TestMongoAotRepositoryContext.java | 4 +- 7 files changed, 139 insertions(+), 43 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index df635fcd28..8e9439e7fa 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -103,6 +103,10 @@ protected List convertSimpleRawResults(Class targetType, List targetType, Document rawResult) { + return extractSimpleTypeResult(rawResult, targetType, mongoConverter); + } + private static @Nullable T extractSimpleTypeResult(@Nullable Document source, Class targetType, MongoConverter converter) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java index 8ac187cb7a..999391f5ec 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoCodeBlocks.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.Optional; import java.util.regex.Pattern; +import java.util.stream.Stream; import org.bson.Document; import org.jspecify.annotations.NullUnmarked; @@ -49,7 +50,6 @@ import org.springframework.data.mongodb.repository.query.MongoQueryMethod; import org.springframework.data.repository.aot.generate.AotQueryMethodGenerationContext; import org.springframework.data.util.ReflectionUtils; -import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.TypeName; @@ -182,17 +182,15 @@ CodeBlock build() { String mongoOpsRef = context.fieldNameOf(MongoOperations.class); Builder builder = CodeBlock.builder(); + Class domainType = context.getRepositoryInformation().getDomainType(); boolean isProjecting = context.getActualReturnType() != null - && !ObjectUtils.nullSafeEquals(TypeName.get(context.getRepositoryInformation().getDomainType()), - context.getActualReturnType()); + && !ObjectUtils.nullSafeEquals(TypeName.get(domainType), context.getActualReturnType()); - Object actualReturnType = isProjecting ? context.getActualReturnType().getType() - : context.getRepositoryInformation().getDomainType(); + Object actualReturnType = isProjecting ? context.getActualReturnType().getType() : domainType; builder.add("\n"); - builder.addStatement("$T<$T> remover = $L.remove($T.class)", ExecutableRemove.class, - context.getRepositoryInformation().getDomainType(), mongoOpsRef, - context.getRepositoryInformation().getDomainType()); + builder.addStatement("$T<$T> $L = $L.remove($T.class)", ExecutableRemove.class, domainType, + context.localVariable("remover"), mongoOpsRef, domainType); DeleteExecution.Type type = DeleteExecution.Type.FIND_AND_REMOVE_ALL; if (!queryMethod.isCollectionQuery()) { @@ -204,11 +202,20 @@ CodeBlock build() { } actualReturnType = ClassUtils.isPrimitiveOrWrapper(context.getMethod().getReturnType()) - ? ClassName.get(context.getMethod().getReturnType()) + ? TypeName.get(context.getMethod().getReturnType()) : queryMethod.isCollectionQuery() ? context.getReturnTypeName() : actualReturnType; - builder.addStatement("return ($T) new $T(remover, $T.$L).execute($L)", actualReturnType, DeleteExecution.class, - DeleteExecution.Type.class, type.name(), queryVariableName); + if (ClassUtils.isVoidType(context.getMethod().getReturnType())) { + builder.addStatement("new $T($L, $T.$L).execute($L)", DeleteExecution.class, context.localVariable("remover"), + DeleteExecution.Type.class, type.name(), queryVariableName); + } else if (context.getMethod().getReturnType() == Optional.class) { + builder.addStatement("return $T.ofNullable(($T) new $T($L, $T.$L).execute($L))", Optional.class, + actualReturnType, DeleteExecution.class, context.localVariable("remover"), DeleteExecution.Type.class, + type.name(), queryVariableName); + } else { + builder.addStatement("return ($T) new $T($L, $T.$L).execute($L)", actualReturnType, DeleteExecution.class, + context.localVariable("remover"), DeleteExecution.Type.class, type.name(), queryVariableName); + } return builder.build(); } @@ -318,14 +325,25 @@ CodeBlock build() { Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); - builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, - context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); - if (!queryMethod.isCollectionQuery()) { - builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", - CollectionUtils.class, returnType, returnType, context.localVariable("results")); + if (queryMethod.isStreamQuery()) { + + builder.addStatement("$T<$T> $L = $L.aggregateStream($L, $T.class)", Stream.class, Document.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); + + builder.addStatement("return $L.map(it -> ($T) convertSimpleRawResult($T.class, it))", + context.localVariable("results"), returnType, returnType); } else { - builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType, - context.localVariable("results")); + + builder.addStatement("$T $L = $L.aggregate($L, $T.class)", AggregationResults.class, + context.localVariable("results"), mongoOpsRef, aggregationVariableName, outputType); + + if (!queryMethod.isCollectionQuery()) { + builder.addStatement("return $T.<$T>firstElement(convertSimpleRawResults($T.class, $L.getMappedResults()))", + CollectionUtils.class, returnType, returnType, context.localVariable("results")); + } else { + builder.addStatement("return convertSimpleRawResults($T.class, $L.getMappedResults())", returnType, + context.localVariable("results")); + } } } else { if (queryMethod.isSliceQuery()) { @@ -339,8 +357,15 @@ CodeBlock build() { context.getPageableParameterName(), context.localVariable("results"), context.getPageableParameterName(), context.localVariable("hasNext")); } else { - builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, - aggregationVariableName, outputType); + + if (queryMethod.isStreamQuery()) { + builder.addStatement("return $L.aggregateStream($L, $T.class)", mongoOpsRef, aggregationVariableName, + outputType); + } else { + + builder.addStatement("return $L.aggregate($L, $T.class).getMappedResults()", mongoOpsRef, + aggregationVariableName, outputType); + } } } @@ -420,8 +445,16 @@ CodeBlock build() { builder.addStatement("return $L.matching($L).scroll($L)", context.localVariable("finder"), query.name(), scrollPositionParameterName); } else { - builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), - terminatingMethod); + if (query.isCount() && !ClassUtils.isAssignable(Long.class, context.getActualReturnType().getRawClass())) { + + Class returnType = ClassUtils.resolvePrimitiveIfNecessary(queryMethod.getReturnedObjectType()); + builder.addStatement("return $T.convertNumberToTargetClass($L.matching($L).$L, $T.class)", NumberUtils.class, + context.localVariable("finder"), query.name(), terminatingMethod, returnType); + + } else { + builder.addStatement("return $L.matching($L).$L", context.localVariable("finder"), query.name(), + terminatingMethod); + } } return builder.build(); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java index 354f4a6296..424d067d74 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributor.java @@ -18,6 +18,7 @@ import static org.springframework.data.mongodb.repository.aot.MongoCodeBlocks.*; import java.lang.reflect.Method; +import java.util.Locale; import java.util.regex.Pattern; import org.apache.commons.logging.Log; @@ -119,14 +120,23 @@ protected void customizeConstructor(AotRepositoryConstructorBuilder constructorB if (queryMethod.isModifyingQuery()) { - Update updateSource = queryMethod.getUpdateSource(); - if (StringUtils.hasText(updateSource.value())) { - UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value())); + int updateIndex = queryMethod.getParameters().getUpdateIndex(); + if (updateIndex != -1) { + + UpdateInteraction update = new UpdateInteraction(query, null, updateIndex); return updateMethodContributor(queryMethod, update); - } - if (!ObjectUtils.isEmpty(updateSource.pipeline())) { - AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); - return aggregationUpdateMethodContributor(queryMethod, update); + + } else { + Update updateSource = queryMethod.getUpdateSource(); + if (StringUtils.hasText(updateSource.value())) { + UpdateInteraction update = new UpdateInteraction(query, new StringUpdate(updateSource.value()), null); + return updateMethodContributor(queryMethod, update); + } + + if (!ObjectUtils.isEmpty(updateSource.pipeline())) { + AggregationUpdateInteraction update = new AggregationUpdateInteraction(query, updateSource.pipeline()); + return aggregationUpdateMethodContributor(queryMethod, update); + } } } @@ -160,10 +170,12 @@ private QueryInteraction createStringQuery(RepositoryInformation repositoryInfor private static boolean backoff(MongoQueryMethod method) { - boolean skip = method.isGeoNearQuery() || method.isSearchQuery(); + // TODO: namedQuery, Regex queries, queries accepting Shapes (e.g. within) or returning arrays. + boolean skip = method.isGeoNearQuery() || method.isSearchQuery() + || method.getName().toLowerCase(Locale.ROOT).contains("regex") || method.getReturnType().getType().isArray(); if (skip && logger.isDebugEnabled()) { - logger.debug("Skipping AOT generation for [%s]. Method is either geo-near, streaming, search or scrolling query" + logger.debug("Skipping AOT generation for [%s]. Method is either returning an array or a geo-near, regex query" .formatted(method.getName())); } return skip; @@ -197,9 +209,15 @@ private static MethodContributor updateMethodContributor(Mongo .usingQueryVariableName(filterVariableName).build()); // update definition - String updateVariableName = context.localVariable("updateDefinition"); - builder.add( - updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName).build()); + String updateVariableName; + + if (update.hasUpdateDefinitionParameter()) { + updateVariableName = context.getParameterName(update.getRequiredUpdateDefinitionParameter()); + } else { + updateVariableName = context.localVariable("updateDefinition"); + builder.add(updateBlockBuilder(context, queryMethod).update(update).usingUpdateVariableName(updateVariableName) + .build()); + } builder.add(updateExecutionBlockBuilder(context, queryMethod).withFilter(filterVariableName) .referencingUpdate(updateVariableName).build()); diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java index bbc76bec59..525a4782a5 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/UpdateInteraction.java @@ -17,37 +17,60 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.data.repository.aot.generate.QueryMetadata; +import org.springframework.util.Assert; /** * An {@link MongoInteraction} to execute an update. * * @author Christoph Strobl + * @author Mark Paluch * @since 5.0 */ class UpdateInteraction extends MongoInteraction implements QueryMetadata { private final QueryInteraction filter; - private final StringUpdate update; + private final @Nullable StringUpdate update; + private final @Nullable Integer updateDefinitionParameter; - UpdateInteraction(QueryInteraction filter, StringUpdate update) { + UpdateInteraction(QueryInteraction filter, @Nullable StringUpdate update, + @Nullable Integer updateDefinitionParameter) { this.filter = filter; this.update = update; + this.updateDefinitionParameter = updateDefinitionParameter; } - QueryInteraction getFilter() { + public QueryInteraction getFilter() { return filter; } - StringUpdate getUpdate() { + public @Nullable StringUpdate getUpdate() { return update; } + public int getRequiredUpdateDefinitionParameter() { + + Assert.notNull(updateDefinitionParameter, "UpdateDefinitionParameter must not be null!"); + + return updateDefinitionParameter; + } + + public boolean hasUpdateDefinitionParameter() { + return updateDefinitionParameter != null; + } + @Override public Map serialize() { Map serialized = filter.serialize(); - serialized.put("update", update.getUpdateString()); + + if (update != null) { + serialized.put("filter", filter.getQuery().getQueryString()); + serialized.put("update", update.getUpdateString()); + } + return serialized; } @@ -55,4 +78,5 @@ public Map serialize() { InteractionType getExecutionType() { return InteractionType.UPDATE; } + } diff --git a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java index 0b62f5979b..5eb9fed686 100644 --- a/spring-data-mongodb/src/test/java/example/aot/UserRepository.java +++ b/spring-data-mongodb/src/test/java/example/aot/UserRepository.java @@ -55,6 +55,8 @@ public interface UserRepository extends CrudRepository { Long countUsersByLastname(String lastname); + int countUsersAsIntByLastname(String lastname); + Boolean existsUserByLastname(String lastname); List findByLastnameStartingWith(String lastname); @@ -216,6 +218,11 @@ public interface UserRepository extends CrudRepository { "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) AggregationResults groupByLastnameAndAsAggregationResults(String property); + @Aggregation(pipeline = { // + "{ '$match' : { 'last_name' : { '$ne' : null } } }", // + "{ '$group': { '_id' : '$last_name', names : { $addToSet : '$?0' } } }" }) + Stream streamGroupByLastnameAndAsAggregationResults(String property); + @Aggregation(pipeline = { // "{ '$match' : { 'posts' : { '$ne' : null } } }", // "{ '$project': { 'nrPosts' : {'$size': '$posts' } } }", // diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index 5a86c9658a..1c9796ead6 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -107,8 +107,8 @@ void testFindDerivedFinderOptionalEntity() { @Test void testDerivedCount() { - Long value = fragment.countUsersByLastname("Skywalker"); - assertThat(value).isEqualTo(2L); + assertThat(fragment.countUsersByLastname("Skywalker")).isEqualTo(2L); + assertThat(fragment.countUsersAsIntByLastname("Skywalker")).isEqualTo(2); } @Test @@ -559,6 +559,16 @@ void testAggregationWithProjectedResultsWrappedInAggregationResults() { new UserAggregate("Solo", List.of("Han", "Ben"))); } + @Test + void testAggregationStreamWithProjectedResultsWrappedInAggregationResults() { + + List allLastnames = fragment.streamGroupByLastnameAndAsAggregationResults("first_name").toList(); + assertThat(allLastnames).containsExactlyInAnyOrder(// + new UserAggregate("Skywalker", List.of("Anakin", "Luke")), // + new UserAggregate("Organa", List.of("Leia")), // + new UserAggregate("Solo", List.of("Han", "Ben"))); + } + @Test void testAggregationWithSingleResultExtraction() { assertThat(fragment.sumPosts()).isEqualTo(5); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java index 39ce43e994..5f470dd550 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/TestMongoAotRepositoryContext.java @@ -37,12 +37,12 @@ /** * @author Christoph Strobl */ -class TestMongoAotRepositoryContext implements AotRepositoryContext { +public class TestMongoAotRepositoryContext implements AotRepositoryContext { private final StubRepositoryInformation repositoryInformation; private final Environment environment = new StandardEnvironment(); - TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { + public TestMongoAotRepositoryContext(Class repositoryInterface, @Nullable RepositoryComposition composition) { this.repositoryInformation = new StubRepositoryInformation(repositoryInterface, composition); } From ec218807b31a674f7766c2d78a89997566697e50 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 14 May 2025 15:18:21 +0200 Subject: [PATCH 69/74] Add repository benchmarks. See: #4939 Original Pull Request: #4970 --- .../repository/AotRepositoryBenchmark.java | 153 ++++++ .../repository/SmallerPersonRepository.java | 477 ++++++++++++++++++ .../SmallerRepositoryBenchmark.java | 83 +++ 3 files changed, 713 insertions(+) create mode 100644 spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java create mode 100644 spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java create mode 100644 spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java new file mode 100644 index 0000000000..ba9da66da4 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/AotRepositoryBenchmark.java @@ -0,0 +1,153 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; +import org.springframework.data.mongodb.repository.aot.MongoRepositoryContributor; +import org.springframework.data.mongodb.repository.aot.TestMongoAotRepositoryContext; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; +import org.springframework.data.mongodb.repository.support.QuerydslMongoPredicateExecutor; +import org.springframework.data.mongodb.repository.support.SimpleMongoRepository; +import org.springframework.data.projection.ProjectionFactory; +import org.springframework.data.projection.SpelAwareProxyProjectionFactory; +import org.springframework.data.repository.core.RepositoryMetadata; +import org.springframework.data.repository.core.support.RepositoryComposition; +import org.springframework.data.repository.core.support.RepositoryFactoryBeanSupport; +import org.springframework.data.repository.core.support.RepositoryFragment; +import org.springframework.data.repository.query.ValueExpressionDelegate; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Benchmark for AOT repositories. + * + * @author Mark Paluch + */ +@Testable +public class AotRepositoryBenchmark extends AbstractMicrobenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + public static Class aot; + public static TestMongoAotRepositoryContext repositoryContext = new TestMongoAotRepositoryContext( + SmallerPersonRepository.class, + RepositoryComposition.of(RepositoryFragment.structural(SimpleMongoRepository.class), + RepositoryFragment.structural(QuerydslMongoPredicateExecutor.class))); + + MongoClient mongoClient; + MongoTemplate mongoTemplate; + RepositoryComposition.RepositoryFragments fragments; + SmallerPersonRepository repositoryProxy; + + @Setup(Level.Trial) + public void doSetup() { + + mongoClient = MongoClients.create(); + mongoTemplate = new MongoTemplate(mongoClient, "jmh"); + + if (this.aot == null) { + + TestGenerationContext generationContext = new TestGenerationContext(PersonRepository.class); + + new MongoRepositoryContributor(repositoryContext).contribute(generationContext); + + TestCompiler.forSystem().withCompilerOptions("-parameters").with(generationContext).compile(compiled -> { + + try { + this.aot = compiled.getClassLoader().loadClass(SmallerPersonRepository.class.getName() + "Impl__Aot"); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + try { + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = getCreationContext(repositoryContext); + fragments = RepositoryComposition.RepositoryFragments + .just(aot.getConstructor(MongoOperations.class, RepositoryFactoryBeanSupport.FragmentCreationContext.class) + .newInstance(mongoTemplate, creationContext)); + + this.repositoryProxy = createRepository(fragments); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private RepositoryFactoryBeanSupport.FragmentCreationContext getCreationContext( + TestMongoAotRepositoryContext repositoryContext) { + + RepositoryFactoryBeanSupport.FragmentCreationContext creationContext = new RepositoryFactoryBeanSupport.FragmentCreationContext() { + @Override + public RepositoryMetadata getRepositoryMetadata() { + return repositoryContext.getRepositoryInformation(); + } + + @Override + public ValueExpressionDelegate getValueExpressionDelegate() { + return ValueExpressionDelegate.create(); + } + + @Override + public ProjectionFactory getProjectionFactory() { + return new SpelAwareProxyProjectionFactory(); + } + }; + + return creationContext; + } + + @TearDown(Level.Trial) + public void doTearDown() { + mongoClient.close(); + } + + public SmallerPersonRepository createRepository(RepositoryComposition.RepositoryFragments fragments) { + MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate); + return repositoryFactory.getRepository(SmallerPersonRepository.class, fragments); + } + + } + + @Benchmark + public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(parameters.fragments); + } + + @Benchmark + public Object findDerived(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByFirstname("foo"); + } + + @Benchmark + public Object findAnnotated(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByThePersonsFirstname("foo"); + } + +} diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java new file mode 100644 index 0000000000..bc3868e052 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerPersonRepository.java @@ -0,0 +1,477 @@ +/* + * Copyright 2010-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import java.util.Collection; +import java.util.Date; +import java.util.List; +import java.util.Optional; +import java.util.UUID; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.data.domain.Limit; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.ScrollPosition; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Window; +import org.springframework.data.mongodb.core.aggregation.AggregationResults; +import org.springframework.data.mongodb.core.query.UpdateDefinition; +import org.springframework.data.mongodb.repository.Person.Sex; +import org.springframework.data.querydsl.QuerydslPredicateExecutor; +import org.springframework.data.repository.query.Param; + +/** + * Sample repository managing {@link Person} entities. + * + * @author Oliver Gierke + * @author Thomas Darimont + * @author Christoph Strobl + * @author Fırat KÜÇÜK + * @author Mark Paluch + */ +public interface SmallerPersonRepository extends MongoRepository, QuerydslPredicateExecutor { + + /** + * Returns all {@link Person}s with the given lastname. + * + * @param lastname + * @return + */ + List findByLastname(String lastname); + + List findByLastnameStartsWith(String prefix); + + List findByLastnameEndsWith(String postfix); + + /** + * Returns all {@link Person}s with the given lastname ordered by their firstname. + * + * @param lastname + * @return + */ + List findByLastnameOrderByFirstnameAsc(String lastname); + + /** + * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be + * executed. + * + * @param firstname + * @return + */ + @Query(value = "{ 'lastname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}") + List findByThePersonsLastname(String lastname); + + /** + * Returns the {@link Person}s with the given firstname. Uses {@link Query} annotation to define the query to be + * executed. + * + * @param firstname + * @return + */ + @Query(value = "{ 'firstname' : ?0 }", fields = "{ 'firstname': 1, 'lastname': 1}") + List findByThePersonsFirstname(String firstname); + + // DATAMONGO-871 + @Query(value = "{ 'firstname' : ?0 }") + Person[] findByThePersonsFirstnameAsArray(String firstname); + + /** + * Returns all {@link Person}s with a firstname matching the given one (*-wildcard supported). + * + * @param firstname + * @return + */ + List findByFirstnameLike(@Nullable String firstname); + + List findByFirstnameNotContains(String firstname); + + /** + * Returns all {@link Person}s with a firstname not matching the given one (*-wildcard supported). + * + * @param firstname + * @return + */ + List findByFirstnameNotLike(String firstname); + + List findByFirstnameLikeOrderByLastnameAsc(String firstname, Sort sort); + + List findBySkillsContains(List skills); + + List findBySkillsNotContains(List skills); + + @Query("{'age' : { '$lt' : ?0 } }") + List findByAgeLessThan(int age, Sort sort); + + /** + * Returns a scroll of {@link Person}s with a lastname matching the given one (*-wildcards supported). + * + * @param lastname + * @param scrollPosition + * @return + */ + Window findTop2ByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition); + + Window findByLastnameLikeOrderByLastnameAscFirstnameAsc(String lastname, ScrollPosition scrollPosition, + Limit limit); + + /** + * Returns a scroll of {@link Person}s applying projections with a lastname matching the given one (*-wildcards + * supported). + * + * @param lastname + * @param pageable + * @return + */ + Window findCursorProjectionByLastnameLike(String lastname, Pageable pageable); + + /** + * Returns a page of {@link Person}s with a lastname matching the given one (*-wildcards supported). + * + * @param lastname + * @param pageable + * @return + */ + Page findByLastnameLike(String lastname, Pageable pageable); + + List findByLastnameLike(String lastname, Sort sort, Limit limit); + + @Query("{ 'lastname' : { '$regex' : '?0', '$options' : 'i'}}") + Page findByLastnameLikeWithPageable(String lastname, Pageable pageable); + + List findByFirstname(String firstname); + + List findByLastnameIgnoreCaseIn(String... lastname); + + /** + * Returns all {@link Person}s with a firstname contained in the given varargs. + * + * @param firstnames + * @return + */ + List findByFirstnameIn(String... firstnames); + + /** + * Returns all {@link Person}s with a firstname not contained in the given collection. + * + * @param firstnames + * @return + */ + List findByFirstnameNotIn(Collection firstnames); + + List findByFirstnameAndLastname(String firstname, String lastname); + + /** + * Returns all {@link Person}s with an age between the two given values. + * + * @param from + * @param to + * @return + */ + List findByAgeBetween(int from, int to); + + /** + * Returns the {@link Person} with the given {@link Address} as shipping address. + * + * @param address + * @return + */ + Person findByShippingAddresses(Address address); + + /** + * Returns all {@link Person}s with the given {@link Address}. + * + * @param address + * @return + */ + List findByAddress(Address address); + + List findByAddressZipCode(String zipCode); + + List findByLastnameLikeAndAgeBetween(String lastname, int from, int to); + + List findByAgeOrLastnameLikeAndFirstnameLike(int age, String lastname, String firstname); + + // TODO: List findByLocationNear(Point point); + + // TODO: List findByLocationWithin(Circle circle); + + // TODO: List findByLocationWithin(Box box); + + // TODO: List findByLocationWithin(Polygon polygon); + + List findBySex(Sex sex); + + List findBySex(Sex sex, Pageable pageable); + + // TODO: List findByNamedQuery(String firstname); + + List findByCreator(User user); + + // DATAMONGO-425 + List findByCreatedAtLessThan(Date date); + + // DATAMONGO-425 + List findByCreatedAtGreaterThan(Date date); + + // DATAMONGO-425 + @Query("{ 'createdAt' : { '$lt' : ?0 }}") + List findByCreatedAtLessThanManually(Date date); + + // DATAMONGO-427 + List findByCreatedAtBefore(Date date); + + // DATAMONGO-427 + List findByCreatedAtAfter(Date date); + + // DATAMONGO-472 + List findByLastnameNot(String lastname); + + // DATAMONGO-600 + List findByCredentials(Credentials credentials); + + // DATAMONGO-636 + long countByLastname(String lastname); + + // DATAMONGO-636 + int countByFirstname(String firstname); + + // DATAMONGO-636 + @Query(value = "{ 'lastname' : ?0 }", count = true) + long someCountQuery(String lastname); + + // DATAMONGO-1454 + boolean existsByFirstname(String firstname); + + // DATAMONGO-1454 + @ExistsQuery(value = "{ 'lastname' : ?0 }") + boolean someExistQuery(String lastname); + + // DATAMONGO-770 + List findByFirstnameIgnoreCase(@Nullable String firstName); + + // DATAMONGO-770 + List findByFirstnameNotIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameStartingWithIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameEndingWithIgnoreCase(String firstName); + + // DATAMONGO-770 + List findByFirstnameContainingIgnoreCase(String firstName); + + // DATAMONGO-870 + Slice findByAgeGreaterThan(int age, Pageable pageable); + + // DATAMONGO-821 + @Query("{ creator : { $exists : true } }") + Page findByHavingCreator(Pageable page); + + // DATAMONGO-566 + List deleteByLastname(String lastname); + + // DATAMONGO-566 + Long deletePersonByLastname(String lastname); + + // DATAMONGO-1997 + Optional deleteOptionalByLastname(String lastname); + + // DATAMONGO-566 + @Query(value = "{ 'lastname' : ?0 }", delete = true) + List removeByLastnameUsingAnnotatedQuery(String lastname); + + // DATAMONGO-566 + @Query(value = "{ 'lastname' : ?0 }", delete = true) + Long removePersonByLastnameUsingAnnotatedQuery(String lastname); + + // DATAMONGO-893 + Page findByAddressIn(List
address, Pageable page); + + // DATAMONGO-745 + @Query("{firstname:{$in:?0}, lastname:?1}") + Page findByCustomQueryFirstnamesAndLastname(List firstnames, String lastname, Pageable page); + + // DATAMONGO-745 + @Query("{lastname:?0, 'address.street':{$in:?1}}") + Page findByCustomQueryLastnameAndAddressStreetInList(String lastname, List streetNames, + Pageable page); + + // DATAMONGO-950 + List findTop3ByLastnameStartingWith(String lastname); + + // DATAMONGO-950 + Page findTop3ByLastnameStartingWith(String lastname, Pageable pageRequest); + + // DATAMONGO-1865 + Person findFirstBy(); // limits to 1 result if more, just return the first one + + // DATAMONGO-1865 + Person findPersonByLastnameLike(String firstname); // single person, error if more than one + + // DATAMONGO-1865 + Optional findOptionalPersonByLastnameLike(String firstname); // optional still, error when more than one + + // DATAMONGO-1030 + PersonSummaryDto findSummaryByLastname(String lastname); + + PersonSummaryWithOptional findSummaryWithOptionalByLastname(String lastname); + + @Query("{ ?0 : ?1 }") + List findByKeyValue(String key, String value); + + // DATAMONGO-1165 + @Query("{ firstname : { $in : ?0 }}") + Stream findByCustomQueryWithStreamingCursorByFirstnames(List firstnames); + + // DATAMONGO-990 + @Query("{ firstname : ?#{[0]}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterIndexOnly(String firstname); + + // DATAMONGO-990 + @Query("{ firstname : ?#{[0]}, email: ?#{principal.email} }") + List findWithSpelByFirstnameAndCurrentUserWithCustomQuery(String firstname); + + // DATAMONGO-990 + @Query("{ firstname : :#{#firstname}}") + List findWithSpelByFirstnameForSpELExpressionWithParameterVariableOnly(@Param("firstname") String firstname); + + // DATAMONGO-1911 + @Query("{ uniqueId: ?0}") + Person findByUniqueId(UUID uniqueId); + + /** + * Returns the count of {@link Person} with the given firstname. Uses {@link CountQuery} annotation to define the + * query to be executed. + * + * @param firstname + * @return + */ + @CountQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539 + long countByThePersonsFirstname(String firstname); + + /** + * Deletes {@link Person} entities with the given firstname. Uses {@link DeleteQuery} annotation to define the query + * to be executed. + * + * @param firstname + */ + @DeleteQuery("{ 'firstname' : ?0 }") // DATAMONGO-1539 + void deleteByThePersonsFirstname(String firstname); + + // DATAMONGO-1752 + Iterable findOpenProjectionBy(); + + // DATAMONGO-1752 + Iterable findClosedProjectionBy(); + + @Query(sort = "{ age : -1 }") + List findByAgeGreaterThan(int age); + + @Query(sort = "{ age : -1 }") + List findByAgeGreaterThan(int age, Sort sort); + + // TODO: List findByFirstnameRegex(Pattern pattern); + + @Query(value = "{ 'id' : ?0 }", fields = "{ 'fans': { '$slice': [ ?1, ?2 ] } }") + Person findWithSliceInProjection(String id, int skip, int limit); + + @Query(value = "{ 'id' : ?0 }", fields = "{ 'firstname': { '$toUpper': '$firstname' } }") + Person findWithAggregationInProjection(String id); + + @Query(value = "{ 'shippingAddresses' : { '$elemMatch' : { 'city' : { '$eq' : 'lnz' } } } }", + fields = "{ 'shippingAddresses.$': ?0 }") + Person findWithArrayPositionInProjection(int position); + + @Query(value = "{ 'fans' : { '$elemMatch' : { '$ref' : 'user' } } }", fields = "{ 'fans.$': ?0 }") + Person findWithArrayPositionInProjectionWithDbRef(int position); + + @Aggregation("{ '$project': { '_id' : '$lastname' } }") + List findAllLastnames(); + + @Aggregation("{ '$project': { '_id' : '$lastname' } }") + Stream findAllLastnamesAsStream(); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + Stream groupStreamByLastnameAnd(String property); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + Slice groupByLastnameAndAsSlice(String property, Pageable pageable); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property, Sort sort); + + @Aggregation("{ '$group': { '_id' : '$lastname', names : { $addToSet : '$?0' } } }") + List groupByLastnameAnd(String property, Pageable page); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + int sumAge(); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + AggregationResults sumAgeAndReturnAggregationResultWrapper(); + + @Aggregation(pipeline = "{ '$group' : { '_id' : null, 'total' : { $sum: '$age' } } }") + AggregationResults sumAgeAndReturnAggregationResultWrapperWithConcreteType(); + + @Aggregation({ "{ '$match' : { 'lastname' : 'Matthews'} }", + "{ '$project': { _id : 0, firstname : 1, lastname : 1 } }" }) + Iterable findAggregatedClosedInterfaceProjectionBy(); + + @Query(value = "{_id:?0}") + Optional findDocumentById(String id); + + @Query(value = "{ 'firstname' : ?0, 'lastname' : ?1, 'email' : ?2 , 'age' : ?3, 'sex' : ?4, " + + "'createdAt' : ?5, 'skills' : ?6, 'address.street' : ?7, 'address.zipCode' : ?8, " // + + "'address.city' : ?9, 'uniqueId' : ?10, 'credentials.username' : ?11, 'credentials.password' : ?12 }") + Person findPersonByManyArguments(String firstname, String lastname, String email, Integer age, Sex sex, + Date createdAt, List skills, String street, String zipCode, // + String city, UUID uniqueId, String username, String password); + + List findByUnwrappedUserUsername(String username); + + List findByUnwrappedUser(User user); + + int findAndUpdateViaMethodArgAllByLastname(String lastname, UpdateDefinition update); + + @Update("{ '$inc' : { 'visits' : ?1 } }") + int findAndIncrementVisitsByLastname(String lastname, int increment); + + @Query("{ 'lastname' : ?0 }") + @Update("{ '$inc' : { 'visits' : ?1 } }") + int updateAllByLastname(String lastname, int increment); + + @Update(pipeline = { "{ '$set' : { 'visits' : { '$add' : [ '$visits', ?1 ] } } }" }) + void findAndIncrementVisitsViaPipelineByLastname(String lastname, int increment); + + @Update("{ '$inc' : { 'visits' : ?#{[1]} } }") + int findAndIncrementVisitsUsingSpELByLastname(String lastname, int increment); + + @Update("{ '$push' : { 'shippingAddresses' : ?1 } }") + int findAndPushShippingAddressByEmail(String email, Address address); + + @Query("{ 'age' : null }") + Person findByQueryWithNullEqualityCheck(); + + List findBySpiritAnimal(User user); + +} diff --git a/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java new file mode 100644 index 0000000000..f461a22d31 --- /dev/null +++ b/spring-data-mongodb/src/jmh/java/org/springframework/data/mongodb/repository/SmallerRepositoryBenchmark.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.mongodb.repository; + +import org.junit.platform.commons.annotation.Testable; +import org.openjdk.jmh.annotations.Benchmark; +import org.openjdk.jmh.annotations.Level; +import org.openjdk.jmh.annotations.Scope; +import org.openjdk.jmh.annotations.Setup; +import org.openjdk.jmh.annotations.State; +import org.openjdk.jmh.annotations.TearDown; + +import org.springframework.data.mongodb.core.MongoTemplate; +import org.springframework.data.mongodb.microbenchmark.AbstractMicrobenchmark; +import org.springframework.data.mongodb.repository.support.MongoRepositoryFactory; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoClients; + +/** + * Benchmark for AOT repositories. + * + * @author Mark Paluch + */ +@Testable +public class SmallerRepositoryBenchmark extends AbstractMicrobenchmark { + + @State(Scope.Benchmark) + public static class BenchmarkParameters { + + MongoClient mongoClient; + MongoTemplate mongoTemplate; + SmallerPersonRepository repositoryProxy; + + @Setup(Level.Trial) + public void doSetup() { + + mongoClient = MongoClients.create(); + mongoTemplate = new MongoTemplate(mongoClient, "jmh"); + repositoryProxy = createRepository(); + } + + @TearDown(Level.Trial) + public void doTearDown() { + mongoClient.close(); + } + + public SmallerPersonRepository createRepository() { + MongoRepositoryFactory repositoryFactory = new MongoRepositoryFactory(mongoTemplate); + return repositoryFactory.getRepository(SmallerPersonRepository.class); + } + + } + + @Benchmark + public SmallerPersonRepository repositoryBootstrap(BenchmarkParameters parameters) { + return parameters.createRepository(); + } + + @Benchmark + public Object findDerived(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByFirstname("foo"); + } + + @Benchmark + public Object findAnnotated(BenchmarkParameters parameters) { + return parameters.repositoryProxy.findByThePersonsFirstname("foo"); + } + +} From c78a0e86cab1acf965f6dbaed66f4b79661979a1 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 15 May 2025 11:41:41 +0200 Subject: [PATCH 70/74] Update nullable contract and add issue references. Original Pull Request: #4970 --- .../aot/MongoAotRepositoryFragmentSupport.java | 2 +- .../aot/MongoRepositoryContributorTests.java | 10 +++++----- .../aot/MongoRepositoryContributorUnitTests.java | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java index 8e9439e7fa..178ce4bda6 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/repository/aot/MongoAotRepositoryFragmentSupport.java @@ -103,7 +103,7 @@ protected List convertSimpleRawResults(Class targetType, List targetType, Document rawResult) { + protected @Nullable Object convertSimpleRawResult(Class targetType, Document rawResult) { return extractSimpleTypeResult(rawResult, targetType, mongoConverter); } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java index 1c9796ead6..a2840ec268 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorTests.java @@ -275,7 +275,7 @@ void testDerivedFinderReturningSlice() { assertThat(slice.getContent()).extracting(User::getUsername).containsExactly("han", "kylo"); } - @Test + @Test // GH-4970 void testDerivedQueryReturningStream() { List results = fragment.streamByLastnameStartingWith("S", Sort.by("username"), Limit.of(2)).toList(); @@ -284,7 +284,7 @@ void testDerivedQueryReturningStream() { assertThat(results).extracting(User::getUsername).containsExactly("han", "kylo"); } - @Test + @Test // GH-4970 void testDerivedQueryReturningWindowByOffset() { Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.offset()); @@ -295,7 +295,7 @@ void testDerivedQueryReturningWindowByOffset() { assertThat(window2).extracting(User::getUsername).containsExactly("luke", "vader"); } - @Test + @Test // GH-4970 void testDerivedQueryReturningWindowByKeyset() { Window window1 = fragment.findTop2WindowByLastnameStartingWithOrderByUsername("S", ScrollPosition.keyset()); @@ -474,7 +474,7 @@ void testDerivedFinderReturningPageOfProjections() { assertThat(users).extracting(UserProjection::getUsername).containsExactly("han", "kylo"); } - @Test + @Test // GH-4970 void testDerivedFinderReturningPageOfDynamicProjections() { Page users = fragment.findUserProjectionByLastnameStartingWith("S", @@ -559,7 +559,7 @@ void testAggregationWithProjectedResultsWrappedInAggregationResults() { new UserAggregate("Solo", List.of("Han", "Ben"))); } - @Test + @Test // GH-4970 void testAggregationStreamWithProjectedResultsWrappedInAggregationResults() { List allLastnames = fragment.streamGroupByLastnameAndAsAggregationResults("first_name").toList(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java index e53b3ae679..bc70b4ded7 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/repository/aot/MongoRepositoryContributorUnitTests.java @@ -65,7 +65,7 @@ MongoOperations mongoOperations() { @Autowired TestGenerationContext generationContext; - @Test + @Test // GH-4970 void shouldConsiderMetaAnnotation() throws IOException { InputStreamSource aotFragment = generationContext.getGeneratedFiles().getGeneratedFile(GeneratedFiles.Kind.SOURCE, From 4ff7a30de158d3acbd5438920fd0790c1a6d4870 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:15:55 +0200 Subject: [PATCH 71/74] Prepare 5.0 M3 (2025.1.0). See #4956 --- pom.xml | 20 ++++---------------- src/main/resources/notice.txt | 3 ++- 2 files changed, 6 insertions(+), 17 deletions(-) diff --git a/pom.xml b/pom.xml index 95fc8379d9..623c47c40a 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-SNAPSHOT + 4.0.0-M3 @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-SNAPSHOT + 4.0.0-M3 5.5.0 1.19 @@ -157,20 +157,8 @@ - - spring-snapshot - https://repo.spring.io/snapshot - - true - - - false - - - - spring-milestone - https://repo.spring.io/milestone - + + diff --git a/src/main/resources/notice.txt b/src/main/resources/notice.txt index 392eac521c..8742b05aaa 100644 --- a/src/main/resources/notice.txt +++ b/src/main/resources/notice.txt @@ -1,4 +1,4 @@ -Spring Data MongoDB 5.0 M2 (2025.1.0) +Spring Data MongoDB 5.0 M3 (2025.1.0) Copyright (c) [2010-2019] Pivotal Software, Inc. This product is licensed to you under the Apache License, Version 2.0 (the "License"). @@ -8,3 +8,4 @@ This product may include a number of subcomponents with separate copyright notices and license terms. Your use of the source code for the these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. + From 614c1b2eefb27a9f055eb299884dcd50f9f77bc8 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:16:17 +0200 Subject: [PATCH 72/74] Release version 5.0 M3 (2025.1.0). See #4956 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 623c47c40a..3f838da877 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M3 pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index fc88571622..39fdef8911 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M3 ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 6f34da5660..5119698acc 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-SNAPSHOT + 5.0.0-M3 ../pom.xml From a11719405bdb02b56b9c3d2fcec1cfbe6150aa63 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:18:46 +0200 Subject: [PATCH 73/74] Prepare next development iteration. See #4956 --- pom.xml | 2 +- spring-data-mongodb-distribution/pom.xml | 2 +- spring-data-mongodb/pom.xml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pom.xml b/pom.xml index 3f838da877..623c47c40a 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M3 + 5.0.0-SNAPSHOT pom Spring Data MongoDB diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 39fdef8911..fc88571622 100644 --- a/spring-data-mongodb-distribution/pom.xml +++ b/spring-data-mongodb-distribution/pom.xml @@ -15,7 +15,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M3 + 5.0.0-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 5119698acc..6f34da5660 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/pom.xml @@ -13,7 +13,7 @@ org.springframework.data spring-data-mongodb-parent - 5.0.0-M3 + 5.0.0-SNAPSHOT ../pom.xml From e368a42484f278f5d923a8d0a5bd2bbb27f52051 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 16 May 2025 14:18:48 +0200 Subject: [PATCH 74/74] After release cleanups. See #4956 --- pom.xml | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/pom.xml b/pom.xml index 623c47c40a..95fc8379d9 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ org.springframework.data.build spring-data-parent - 4.0.0-M3 + 4.0.0-SNAPSHOT @@ -26,7 +26,7 @@ multi spring-data-mongodb - 4.0.0-M3 + 4.0.0-SNAPSHOT 5.5.0 1.19 @@ -157,8 +157,20 @@ - - + + spring-snapshot + https://repo.spring.io/snapshot + + true + + + false + + + + spring-milestone + https://repo.spring.io/milestone +