diff --git a/Jenkinsfile b/Jenkinsfile index f122dfb0c5..da92e1b68b 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -265,6 +265,34 @@ pipeline { } } + stage("test: MongoDB 7.0 (driver-next)") { + agent { + label 'data' + } + options { timeout(time: 30, unit: 'MINUTES') } + environment { + ARTIFACTORY = credentials("${p['artifactory.credentials']}") + DEVELOCITY_CACHE = credentials("${p['develocity.cache.credentials']}") + DEVELOCITY_ACCESS_KEY = credentials("${p['develocity.access-key']}") + } + steps { + script { + docker.image("harbor-repo.vmware.com/dockerhub-proxy-cache/springci/spring-data-with-mongodb-7.0:${p['java.main.tag']}").inside(p['docker.java.inside.basic']) { + sh 'mkdir -p /tmp/mongodb/db /tmp/mongodb/log' + sh 'mongod --setParameter transactionLifetimeLimitSeconds=90 --setParameter maxTransactionLockRequestTimeoutMillis=10000 --dbpath /tmp/mongodb/db --replSet rs0 --fork --logpath /tmp/mongodb/log/mongod.log &' + sh 'sleep 10' + sh 'mongosh --eval "rs.initiate({_id: \'rs0\', members:[{_id: 0, host: \'127.0.0.1:27017\'}]});"' + sh 'sleep 15' + sh 'MAVEN_OPTS="-Duser.name=' + "${p['jenkins.user.name']}" + ' -Duser.home=/tmp/jenkins-home" ' + + "DEVELOCITY_CACHE_USERNAME=${DEVELOCITY_CACHE_USR} " + + "DEVELOCITY_CACHE_PASSWORD=${DEVELOCITY_CACHE_PSW} " + + "GRADLE_ENTERPRISE_ACCESS_KEY=${DEVELOCITY_ACCESS_KEY} " + + "./mvnw -s settings.xml -Pmongo-5.0 clean dependency:list test -Dsort -U -B -Dgradle.cache.local.enabled=false -Dgradle.cache.remote.enabled=false" + } + } + } + } + stage("test: MongoDB 7.0 (next)") { agent { label 'data' diff --git a/pom.xml b/pom.xml index aff1afc489..8bae08443b 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT pom Spring Data MongoDB @@ -132,6 +132,13 @@ spring-data-mongodb-benchmarks + + mongo-5.0 + + 5.0.0-beta0 + + + diff --git a/spring-data-mongodb-benchmarks/pom.xml b/spring-data-mongodb-benchmarks/pom.xml index 34d95eb205..bd69685f22 100644 --- a/spring-data-mongodb-benchmarks/pom.xml +++ b/spring-data-mongodb-benchmarks/pom.xml @@ -7,7 +7,7 @@ org.springframework.data spring-data-mongodb-parent - 4.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb-distribution/pom.xml b/spring-data-mongodb-distribution/pom.xml index 124a6bf5ad..9638c69e8d 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.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 39fc1a1de9..0d1a169fbe 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.3.0-SNAPSHOT + 4.3.x-4578-SNAPSHOT ../pom.xml @@ -260,6 +260,12 @@ test + + org.junit.platform + junit-platform-launcher + test + + jakarta.transaction jakarta.transaction-api 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 3fcded33ed..40e4a7466a 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 @@ -25,6 +25,9 @@ import org.springframework.util.ClassUtils; /** + * Collection of {@link Predicate predicates} to determine dynamic library aspects during AOT computation. + * Intended for internal usage only. + * * @author Christoph Strobl * @since 4.0 */ @@ -33,13 +36,27 @@ public class MongoAotPredicates { public static final Predicate> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type) || TypeUtils.type(type).isPartOf("org.bson"); public static final Predicate IS_REACTIVE_LIBARARY_AVAILABLE = ReactiveWrappers::isAvailable; public static final Predicate IS_SYNC_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.client.MongoClient", classLoader); + public static final Predicate IS_REACTIVE_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.reactivestreams.client.MongoClient", classLoader); public static boolean isReactorPresent() { return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR); } + /** + * @param classLoader can be {@literal null}. + * @return {@literal true} if the {@link com.mongodb.client.MongoClient} is present. + * @since 4.0 + */ public static boolean isSyncClientPresent(@Nullable ClassLoader classLoader) { return IS_SYNC_CLIENT_PRESENT.test(classLoader); } + /** + * @param classLoader can be {@literal null}. + * @return {@literal true} if the {@link com.mongodb.reactivestreams.client.MongoClient} is present. + * @since 4.3 + */ + public static boolean isReactiveClientPresent(@Nullable ClassLoader classLoader) { + return IS_REACTIVE_CLIENT_PRESENT.test(classLoader); + } } 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 a49d0bece1..226a102290 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 @@ -19,6 +19,13 @@ import java.util.Arrays; +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.UnixServerAddress; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; @@ -31,6 +38,7 @@ 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.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; @@ -53,6 +61,7 @@ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) MemberCategory.INVOKE_PUBLIC_METHODS)); registerTransactionProxyHints(hints, classLoader); + registerMongoCompatibilityAdapterHints(hints, classLoader); if (isReactorPresent()) { @@ -80,4 +89,33 @@ private static void registerTransactionProxyHints(RuntimeHints hints, @Nullable } } + private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { + + hints.reflection() // + .registerType(MongoClientSettings.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(MongoClientSettings.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(IndexOptions.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(ServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(UnixServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory"), MemberCategory.INTROSPECT_PUBLIC_METHODS); + + if(MongoAotPredicates.isSyncClientPresent(classLoader)) { + + hints.reflection() // + .registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); + } + + if(MongoAotPredicates.isReactiveClientPresent(classLoader)) { + + hints.reflection() // + .registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS) + .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS); + } + } + } 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 583453c7a1..4772e6ab09 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 @@ -114,6 +114,7 @@ public DefaultIndexOperations(MongoOperations mongoOperations, String collection this.type = type; } + @Override public String ensureIndex(final IndexDefinition indexDefinition) { return execute(collection -> { 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 5776619399..548d3e0862 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 @@ -88,7 +88,8 @@ private DefaultReactiveIndexOperations(ReactiveMongoOperations mongoOperations, this.type = type; } - public Mono ensureIndex(final IndexDefinition indexDefinition) { + @Override + public Mono ensureIndex(IndexDefinition indexDefinition) { return mongoOperations.execute(collectionName, collection -> { 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 248c7c514c..a366960e0b 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,9 +18,11 @@ 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; @@ -89,7 +91,7 @@ private static Converter getIndexDefinitionIndexO ops = ops.bits((Integer) indexOptions.get("bits")); } if (indexOptions.containsKey("bucketSize")) { - ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue()); + 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 9be7cb4fd3..a08fad5972 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,7 +25,9 @@ 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; @@ -40,7 +42,7 @@ import com.mongodb.WriteConcern; import com.mongodb.connection.ClusterConnectionMode; import com.mongodb.connection.ClusterType; -import com.mongodb.connection.StreamFactoryFactory; +import com.mongodb.connection.TransportSettings; /** * A factory bean for construction of a {@link MongoClientSettings} instance to be used with a MongoDB driver. @@ -54,7 +56,10 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean { - for (String name : db.listCollectionNames()) { + for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { if (name.equals(collectionName)) { return true; } @@ -1965,7 +1967,7 @@ public List mapReduce(Query query, Class domainType, String inputColle } if (mapReduceOptions.getOutputSharded().isPresent()) { - mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get()); + MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce).sharded(mapReduceOptions.getOutputSharded().get()); } if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) { @@ -2340,7 +2342,7 @@ protected String replaceWithResourceIfNecessary(String function) { public Set getCollectionNames() { return execute(db -> { Set result = new LinkedHashSet<>(); - for (String name : db.listCollectionNames()) { + for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(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 c4096a2774..0f1b8905a2 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 @@ -46,6 +46,7 @@ import org.bson.types.ObjectId; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -117,6 +118,7 @@ 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; @@ -736,7 +738,7 @@ public Mono collectionExists(Class entityClass) { @Override public Mono collectionExists(String collectionName) { - return createMono(db -> Flux.from(db.listCollectionNames()) // + return createMono(db -> Flux.from(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()) // .filter(s -> s.equals(collectionName)) // .map(s -> true) // .single(false)); @@ -784,7 +786,7 @@ public ReactiveBulkOperations bulkOps(BulkMode mode, @Nullable Class entityTy @Override public Flux getCollectionNames() { - return createFlux(MongoDatabase::listCollectionNames); + return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()); } public Mono getMongoDatabase() { @@ -2172,7 +2174,7 @@ public Flux mapReduce(Query filterQuery, Class domainType, String inpu } if (options.getOutputSharded().isPresent()) { - publisher = publisher.sharded(options.getOutputSharded().get()); + MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get()); } if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) { 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 8492ad3bc5..822ade040e 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,6 +19,7 @@ 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; @@ -40,7 +41,7 @@ public class GeospatialIndex implements IndexDefinition { private @Nullable Integer max; private @Nullable Integer bits; private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D; - private Double bucketSize = 1.0; + private Double bucketSize = MongoClientVersion.isVersion5OrNewer() ? null : 1.0; private @Nullable String additionalField; private Optional filter = Optional.empty(); private Optional collation = Optional.empty(); @@ -207,7 +208,9 @@ public Document getIndexOptions() { case GEO_HAYSTACK: - document.put("bucketSize", bucketSize); + 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 d9494249cc..9a9cee26f9 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,6 +25,7 @@ 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; @@ -53,6 +54,7 @@ import org.springframework.data.mongodb.core.query.Collation; import org.springframework.data.mongodb.util.BsonUtils; import org.springframework.data.mongodb.util.DotPath; +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; @@ -708,7 +710,21 @@ protected IndexDefinitionHolder createGeoSpatialIndexDefinition(String dotPath, .named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty)); } - indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField()); + 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("Ignoring no longer supported default GeoSpatialIndexed.bucketSize on %s for Mongo Client 5 or newer.".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/observability/DefaultMongoHandlerObservationConvention.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java index 1c13201745..71a6ffc594 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 @@ -15,9 +15,12 @@ */ package org.springframework.data.mongodb.observability; +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; @@ -26,8 +29,6 @@ import com.mongodb.connection.ConnectionId; import com.mongodb.event.CommandStartedEvent; -import io.micrometer.common.KeyValues; - /** * Default {@link MongoHandlerObservationConvention} implementation. * @@ -78,7 +79,8 @@ public KeyValues getLowCardinalityKeyValues(MongoHandlerContext context) { LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()), LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort())); - InetSocketAddress socketAddress = serverAddress.getSocketAddress(); + InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress) + .getSocketAddress(); if (socketAddress != null) { 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 16b2daa878..bf02047625 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,8 +15,12 @@ */ package org.springframework.data.mongodb.util; +import org.springframework.data.util.Version; +import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; +import com.mongodb.internal.build.MongoDriverVersion; + /** * {@link MongoClientVersion} holds information about the used mongo-java client and is used to distinguish between * different versions. @@ -28,7 +32,8 @@ public class MongoClientVersion { private static final boolean SYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.MongoClient", - MongoClientVersion.class.getClassLoader()); + MongoClientVersion.class.getClassLoader()) + || ClassUtils.isPresent("com.mongodb.client.MongoClient", MongoClientVersion.class.getClassLoader()); private static final boolean ASYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.async.client.MongoClient", MongoClientVersion.class.getClassLoader()); @@ -36,6 +41,22 @@ public class MongoClientVersion { private static final boolean REACTIVE_CLIENT_PRESENT = ClassUtils .isPresent("com.mongodb.reactivestreams.client.MongoClient", MongoClientVersion.class.getClassLoader()); + private static final boolean IS_VERSION_5_OR_NEWER; + + private static final Version CLIENT_VERSION; + + static { + + ClassLoader classLoader = MongoClientVersion.class.getClassLoader(); + Version version = readVersionFromClass(classLoader); + if (version == null) { + version = guessDriverVersionFromClassPath(classLoader); + } + + CLIENT_VERSION = version; + IS_VERSION_5_OR_NEWER = CLIENT_VERSION.isGreaterThanOrEqualTo(Version.parse("5.0")); + } + /** * @return {@literal true} if the async MongoDB Java driver is on classpath. */ @@ -58,4 +79,33 @@ public static boolean isSyncClientPresent() { public static boolean isReactiveClientPresent() { return REACTIVE_CLIENT_PRESENT; } + + /** + * @return {@literal true} if the MongoDB Java driver version is 5 or newer. + * @since 4.3 + */ + public static boolean isVersion5OrNewer() { + return IS_VERSION_5_OR_NEWER; + } + + @Nullable + private static Version readVersionFromClass(ClassLoader classLoader) { + + if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) { + try { + return Version.parse(MongoDriverVersion.VERSION); + } catch (IllegalArgumentException exception) { + // well not much we can do, right? + } + } + return null; + } + + private static Version guessDriverVersionFromClassPath(ClassLoader classLoader) { + + if (ClassUtils.isPresent("com.mongodb.internal.connection.StreamFactoryFactory", classLoader)) { + return Version.parse("5"); + } + return Version.parse("4.11"); + } } 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 new file mode 100644 index 0000000000..aa1eddabd1 --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java @@ -0,0 +1,378 @@ +/* + * Copyright 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.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.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 + * @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 @Nullable Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, + "getStreamFactoryFactory"); + + private static final @Nullable Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", + Double.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 {@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 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); + } + + 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/aot/MongoRuntimeHintsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java new file mode 100644 index 0000000000..526ab39b1e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java @@ -0,0 +1,128 @@ +/* + * Copyright 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.aot; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.aot.hint.MemberCategory.*; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.*; + +import java.util.function.Predicate; + +import org.junit.jupiter.api.Test; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.data.mongodb.test.util.ClassPathExclusions; + +import com.mongodb.MongoClientSettings; +import com.mongodb.ServerAddress; +import com.mongodb.UnixServerAddress; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; + +/** + * Unit Tests for {@link MongoRuntimeHints}. + * + * @author Christoph Strobl + */ +class MongoRuntimeHintsUnitTests { + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client", "com.mongodb.reactivestreams.client" }) + void shouldRegisterGeneralCompatibilityHints() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MongoClientSettings.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(MongoClientSettings.Builder.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(IndexOptions.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(ServerAddress.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(UnixServerAddress.class).withMemberCategory(INVOKE_PUBLIC_METHODS)) + .and(reflection().onType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory")) + .withMemberCategory(INTROSPECT_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) + void shouldRegisterSyncCompatibilityHintsIfPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MapReduceIterable.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client" }) + void shouldNotRegisterSyncCompatibilityHintsIfClientNotPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(TypeReference.of("com.mongodb.client.MapReduceIterable")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate() + .and(reflection().onType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate()); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.client" }) + void shouldRegisterReactiveCompatibilityHintsIfPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection().onType(MapReducePublisher.class) + .withMemberCategory(INVOKE_PUBLIC_METHODS) + .and(reflection().onType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS)); + + assertThat(runtimeHints).matches(expected); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) + void shouldNotRegisterReactiveCompatibilityHintsIfClientNotPresent() { + + RuntimeHints runtimeHints = new RuntimeHints(); + + new MongoRuntimeHints().registerHints(runtimeHints, this.getClass().getClassLoader()); + + Predicate expected = reflection() + .onType(TypeReference.of("com.mongodb.reactivestreams.client.MapReducePublisher")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate() + .and(reflection().onType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl")) + .withMemberCategory(INVOKE_PUBLIC_METHODS).negate()); + + assertThat(runtimeHints).matches(expected); + } + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java index 0b0139c4e2..afaba3fa39 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java @@ -377,7 +377,7 @@ public void translateMongoBulkOperationExceptionWithWriteConcernError() { when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null, Collections.emptyList(), - new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress())); + new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress(), Collections.emptySet())); assertThatExceptionOfType(DataIntegrityViolationException.class) .isThrownBy(() -> ops.insert(new SomeDomainType()).execute()); @@ -389,7 +389,7 @@ public void translateMongoBulkOperationExceptionWithoutWriteConcernError() { when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null, Collections.singletonList(new BulkWriteError(42, "a write error happened", new BsonDocument(), 49)), null, - new ServerAddress())); + new ServerAddress(), Collections.emptySet())); assertThatExceptionOfType(BulkOperationException.class) .isThrownBy(() -> ops.insert(new SomeDomainType()).execute()); 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 00c325f839..ff74786cb4 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 @@ -21,6 +21,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.core.NestedRuntimeException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; @@ -96,7 +97,7 @@ void translateSocketExceptionSubclasses() { void translateCursorNotFound() { expectExceptionWithCauseMessage( - translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())), + translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new BsonDocument(), Mockito.mock(ServerAddress.class))), DataAccessResourceFailureException.class); } 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 c47b93967e..8acc112418 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 @@ -58,6 +58,7 @@ 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}. @@ -82,6 +83,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Mock MongoDatabase database; @Mock ClientSession clientSession; @Mock FindPublisher findPublisher; + Publisher collectionNamesPublisher; @Mock AggregatePublisher aggregatePublisher; @Mock DistinctPublisher distinctPublisher; @Mock Publisher resultPublisher; @@ -92,12 +94,13 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @Before public void setUp() { + mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType()); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString())).thenReturn(collection); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(findPublisher); + doReturn(collectionNamesPublisher).when(database).listCollectionNames(any(ClientSession.class)); when(database.createCollection(any(ClientSession.class), any(), any())).thenReturn(resultPublisher); when(database.runCommand(any(ClientSession.class), any(), any(Class.class))).thenReturn(resultPublisher); when(collection.find(any(ClientSession.class))).thenReturn(findPublisher); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java index 2859db0a4f..eca52d5bd8 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java @@ -96,9 +96,9 @@ public void setUp() { @Override public MongoDatabase getMongoDatabase() throws DataAccessException { - MongoDatabase spiedDatabse = Mockito.spy(super.getMongoDatabase()); - spiedDatabases.add(spiedDatabse); - return spiedDatabse; + MongoDatabase spiedDatabase = Mockito.spy(super.getMongoDatabase()); + spiedDatabases.add(spiedDatabase); + return spiedDatabase; } }; 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 e9ac251e98..bec7a7a057 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 @@ -21,6 +21,7 @@ import java.lang.reflect.Proxy; import java.util.Collections; +import com.mongodb.client.*; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -44,20 +45,11 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; -import com.mongodb.client.AggregateIterable; -import com.mongodb.client.ClientSession; -import com.mongodb.client.DistinctIterable; -import com.mongodb.client.FindIterable; -import com.mongodb.client.MapReduceIterable; -import com.mongodb.client.MongoClient; -import com.mongodb.client.MongoCollection; -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.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 @@ -84,6 +76,7 @@ public class SessionBoundMongoTemplateUnitTests { @Mock MongoClient client; @Mock ClientSession clientSession; @Mock FindIterable findIterable; + MongoIterable collectionNamesIterable; @Mock MongoIterable mongoIterable; @Mock DistinctIterable distinctIterable; @Mock AggregateIterable aggregateIterable; @@ -97,11 +90,12 @@ public class SessionBoundMongoTemplateUnitTests { @Before public void setUp() { + collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType()); when(client.getDatabase(anyString())).thenReturn(database); when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec()); when(database.getCodecRegistry()).thenReturn(codecRegistry); when(database.getCollection(anyString(), any())).thenReturn(collection); - when(database.listCollectionNames(any(ClientSession.class))).thenReturn(mongoIterable); + doReturn(collectionNamesIterable).when(database).listCollectionNames(any(ClientSession.class)); when(collection.find(any(ClientSession.class), any(), any())).thenReturn(findIterable); when(collection.aggregate(any(ClientSession.class), anyList(), any())).thenReturn(aggregateIterable); when(collection.distinct(any(ClientSession.class), any(), any(), any())).thenReturn(distinctIterable); @@ -113,6 +107,7 @@ public void setUp() { when(aggregateIterable.map(any())).thenReturn(aggregateIterable); when(aggregateIterable.into(any())).thenReturn(Collections.emptyList()); when(mongoIterable.iterator()).thenReturn(cursor); + when(collectionNamesIterable.iterator()).thenReturn(cursor); when(distinctIterable.map(any())).thenReturn(distinctIterable); when(distinctIterable.into(any())).thenReturn(Collections.emptyList()); when(mapReduceIterable.sort(any())).thenReturn(mapReduceIterable); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java index e20a934aaa..0257ab3395 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java @@ -21,6 +21,7 @@ import java.util.List; import java.util.Map; +import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; @@ -40,6 +41,7 @@ import com.mongodb.MongoException; import com.mongodb.WriteConcern; import com.mongodb.client.MongoCollection; +import org.springframework.data.mongodb.util.MongoClientVersion; /** * Integration tests for geo-spatial indexing. @@ -86,6 +88,8 @@ public void test2dSphereIndex() { @EnableIfMongoServerVersion(isLessThan = "5.0") public void testHaystackIndex() { + Assumptions.assumeThat(MongoClientVersion.isVersion5OrNewer()).isFalse(); + try { template.save(new GeoSpatialEntityHaystack(45.2, 4.6, "Paris")); assertThat(hasIndexOfType(GeoSpatialEntityHaystack.class, "geoHaystack")).isTrue(); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java index 3282c8693a..6f3607ca98 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java @@ -17,21 +17,23 @@ import static org.springframework.data.mongodb.test.util.Assertions.*; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import io.micrometer.observation.ObservationRegistry; +import io.micrometer.tracing.exporter.FinishedSpan; +import io.micrometer.tracing.test.SampleTestRunner; + import java.util.List; import org.junit.jupiter.api.extension.ExtendWith; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.mongodb.repository.Person; import org.springframework.data.mongodb.repository.PersonRepository; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit.jupiter.SpringExtension; -import io.micrometer.core.instrument.MeterRegistry; -import io.micrometer.core.instrument.simple.SimpleMeterRegistry; -import io.micrometer.observation.ObservationRegistry; -import io.micrometer.tracing.exporter.FinishedSpan; -import io.micrometer.tracing.test.SampleTestRunner; - /** * Collection of tests that log metrics and tracing with an external tracing tool. * @@ -81,8 +83,13 @@ public SampleTestRunnerConsumer yourCode() { assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP"); - assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", + if (MongoClientVersion.isVersion5OrNewer()) { + assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", + "db.mongodb.collection", "net.peer.name", "net.peer.port"); + } else { + assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation", "db.mongodb.collection", "net.peer.name", "net.peer.port", "net.sock.peer.addr", "net.sock.peer.port"); + } } }; } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java index 14a2d5a93c..b89384a0db 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java @@ -30,6 +30,7 @@ import org.bson.BsonString; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; + import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; import com.mongodb.RequestContext; @@ -70,7 +71,7 @@ void setup() { void commandStartedShouldNotInstrumentWhenAdminDatabase() { // when - listener.commandStarted(new CommandStartedEvent(null, 0, null, "admin", "", null)); + listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "admin", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -80,7 +81,7 @@ void commandStartedShouldNotInstrumentWhenAdminDatabase() { void commandStartedShouldNotInstrumentWhenNoRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(null, 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasNoMetrics(); @@ -90,7 +91,7 @@ void commandStartedShouldNotInstrumentWhenNoRequestContext() { void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() { // when - listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null)); + listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, 0, null, "some name", "", null)); // then assertThat(meterRegistry).hasMeterWithName("spring.data.mongodb.command.active"); @@ -104,14 +105,14 @@ void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestConte RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // new ServerAddress("localhost", 1234))), "database", "insert", // new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); // then assertThatTimerRegisteredWithTags(); @@ -125,14 +126,14 @@ void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTime RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // new ServerAddress("localhost", 1234))), // "database", "aggregate", // new BsonDocument("aggregate", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "aggregate", null, null, 0)); // then assertThatTimerRegisteredWithTags(); @@ -146,9 +147,9 @@ void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenP RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, null, "database", "insert", new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), @@ -165,7 +166,7 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { RequestContext traceRequestContext = getContext(); // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // + listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, 0, // new ConnectionDescription( // new ServerId( // new ClusterId("description"), // @@ -173,7 +174,7 @@ void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { "database", "insert", // new BsonDocument("collection", new BsonString("user")))); listener.commandFailed( // - new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); + new CommandFailedEvent(traceRequestContext, 0, 0, null, "db", "insert", 0, new IllegalAccessException())); // then assertThatTimerRegisteredWithTags(); @@ -189,7 +190,7 @@ void completionShouldIgnoreIncompatibleObservationContext() { traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); // when - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); + listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, 0, null, "insert", null, null, 0)); verify(observation).getContext(); verifyNoMoreInteractions(observation); @@ -205,7 +206,7 @@ void failureShouldIgnoreIncompatibleObservationContext() { traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); // when - listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); + listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, 0, null, "db", "insert", 0, null)); verify(observation).getContext(); verifyNoMoreInteractions(observation); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java new file mode 100644 index 0000000000..264c5bfa03 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java @@ -0,0 +1,45 @@ +/* + * Copyright 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.test.util; + +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.junit.jupiter.api.extension.ExtendWith; + +/** + * Annotation used to exclude entries from the classpath. + * Simplified version of ClassPathExclusions. + * + * @author Christoph Strobl + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ExtendWith(ClassPathExclusionsExtension.class) +public @interface ClassPathExclusions { + + /** + * One or more packages that should be excluded from the classpath. + * + * @return the excluded packages + */ + String[] packages(); + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java new file mode 100644 index 0000000000..9d4454a26f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java @@ -0,0 +1,129 @@ +/* + * Copyright 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.test.util; + +import java.lang.reflect.Method; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.platform.engine.discovery.DiscoverySelectors; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestPlan; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; +import org.junit.platform.launcher.listeners.SummaryGeneratingListener; +import org.junit.platform.launcher.listeners.TestExecutionSummary; +import org.springframework.util.CollectionUtils; + +/** + * Simplified version of ModifiedClassPathExtension. + * + * @author Christoph Strobl + */ +class ClassPathExclusionsExtension implements InvocationInterceptor { + + @Override + public void interceptBeforeAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptBeforeEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterEachMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptAfterAllMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + intercept(invocation, extensionContext); + } + + @Override + public void interceptTestMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + @Override + public void interceptTestTemplateMethod(Invocation invocation, + ReflectiveInvocationContext invocationContext, ExtensionContext extensionContext) throws Throwable { + interceptMethod(invocation, invocationContext, extensionContext); + } + + private void interceptMethod(Invocation invocation, ReflectiveInvocationContext invocationContext, + ExtensionContext extensionContext) throws Throwable { + + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + + Class testClass = extensionContext.getRequiredTestClass(); + Method testMethod = invocationContext.getExecutable(); + PackageExcludingClassLoader modifiedClassLoader = PackageExcludingClassLoader.get(testClass, testMethod); + if (modifiedClassLoader == null) { + invocation.proceed(); + return; + } + invocation.skip(); + ClassLoader originalClassLoader = Thread.currentThread().getContextClassLoader(); + Thread.currentThread().setContextClassLoader(modifiedClassLoader); + try { + runTest(extensionContext.getUniqueId()); + } finally { + Thread.currentThread().setContextClassLoader(originalClassLoader); + } + } + + private void runTest(String testId) throws Throwable { + + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(DiscoverySelectors.selectUniqueId(testId)).build(); + Launcher launcher = LauncherFactory.create(); + TestPlan testPlan = launcher.discover(request); + SummaryGeneratingListener listener = new SummaryGeneratingListener(); + launcher.registerTestExecutionListeners(listener); + launcher.execute(testPlan); + TestExecutionSummary summary = listener.getSummary(); + if (!CollectionUtils.isEmpty(summary.getFailures())) { + throw summary.getFailures().get(0).getException(); + } + } + + private void intercept(Invocation invocation, ExtensionContext extensionContext) throws Throwable { + if (isModifiedClassPathClassLoader(extensionContext)) { + invocation.proceed(); + return; + } + invocation.skip(); + } + + private boolean isModifiedClassPathClassLoader(ExtensionContext extensionContext) { + Class testClass = extensionContext.getRequiredTestClass(); + ClassLoader classLoader = testClass.getClassLoader(); + return classLoader.getClass().getName().equals(PackageExcludingClassLoader.class.getName()); + } +} 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 817e18e0cc..fb7f774e94 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 @@ -31,13 +31,14 @@ import org.mockito.junit.jupiter.MockitoExtension; 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.ListDatabasesIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoDatabase; +import com.mongodb.client.MongoIterable; /** * @author Christoph Strobl @@ -62,7 +63,7 @@ class CleanMongoDBTests { @SuppressWarnings({ "serial", "unchecked" }) @BeforeEach - void setUp() { + void setUp() throws ClassNotFoundException { // DB setup @@ -73,13 +74,13 @@ void setUp() { when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock); // collections have to exist - ListDatabasesIterable collectionIterable = mock(ListDatabasesIterable.class); + MongoIterable collectionIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db1mock).collectionNameIterableType()); when(collectionIterable.into(any(Collection.class))).thenReturn(Arrays.asList("db1collection1", "db1collection2")); - when(db1mock.listCollectionNames()).thenReturn(collectionIterable); + doReturn(collectionIterable).when(db1mock).listCollectionNames(); - ListDatabasesIterable collectionIterable2 = mock(ListDatabasesIterable.class); + MongoIterable collectionIterable2 = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db2mock).collectionNameIterableType()); when(collectionIterable2.into(any(Collection.class))).thenReturn(Collections.singletonList("db2collection1")); - when(db2mock.listCollectionNames()).thenReturn(collectionIterable2); + doReturn(collectionIterable2).when(db2mock).listCollectionNames(); // return collections according to names when(db1mock.getCollection(eq("db1collection1"))).thenReturn(db1collection1mock); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java new file mode 100644 index 0000000000..617bc5991e --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java @@ -0,0 +1,34 @@ +/* + * Copyright 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.test.util; + +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; + +/** + * @author Christoph Strobl + * @see ClassPathExclusions + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ClassPathExclusions(packages = { "com.mongodb.reactivestreams.client" }) +public @interface ExcludeReactiveClientFromClassPath { + +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java new file mode 100644 index 0000000000..ff8915386f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java @@ -0,0 +1,34 @@ +/* + * Copyright 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.test.util; + +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; + +/** + * @author Christoph Strobl + * @see ClassPathExclusions + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Documented +@ClassPathExclusions(packages = { "com.mongodb.client" }) +public @interface ExcludeSyncClientFromClassPath { + +} 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 d561966dd3..bab535cb56 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 @@ -29,6 +29,7 @@ import com.mongodb.MongoWriteException; import com.mongodb.client.MongoClient; import com.mongodb.client.MongoCollection; +import org.springframework.data.mongodb.util.MongoCompatibilityAdapter; /** * A {@link MongoTemplate} with configuration hooks and extension suitable for tests. @@ -94,7 +95,7 @@ public void flush() { } public void flushDatabase() { - flush(getDb().listCollectionNames()); + flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames()); } public void flush(Iterable collections) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java new file mode 100644 index 0000000000..ba8ed8b296 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java @@ -0,0 +1,142 @@ +/* + * Copyright 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.test.util; + +import java.io.File; +import java.lang.management.ManagementFactory; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; +import java.util.stream.Stream; + +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.util.ClassUtils; + +/** + * Simplified version of ModifiedClassPathClassLoader. + * + * @author Christoph Strobl + */ +class PackageExcludingClassLoader extends URLClassLoader { + + private final Set excludedPackages; + private final ClassLoader junitLoader; + + PackageExcludingClassLoader(URL[] urls, ClassLoader parent, Collection excludedPackages, + ClassLoader junitClassLoader) { + + super(urls, parent); + this.excludedPackages = Set.copyOf(excludedPackages); + this.junitLoader = junitClassLoader; + } + + @Override + public Class loadClass(String name) throws ClassNotFoundException { + + if (name.startsWith("org.junit") || name.startsWith("org.hamcrest")) { + return Class.forName(name, false, this.junitLoader); + } + + String packageName = ClassUtils.getPackageName(name); + if (this.excludedPackages.contains(packageName)) { + throw new ClassNotFoundException(name); + } + return super.loadClass(name); + } + + static PackageExcludingClassLoader get(Class testClass, Method testMethod) { + + List excludedPackages = readExcludedPackages(testClass, testMethod); + + if (excludedPackages.isEmpty()) { + return null; + } + + ClassLoader testClassClassLoader = testClass.getClassLoader(); + Stream urls = null; + if (testClassClassLoader instanceof URLClassLoader urlClassLoader) { + urls = Stream.of(urlClassLoader.getURLs()); + } else { + urls = Stream.of(ManagementFactory.getRuntimeMXBean().getClassPath().split(File.pathSeparator)) + .map(PackageExcludingClassLoader::toURL); + } + + return new PackageExcludingClassLoader(urls.toArray(URL[]::new), testClassClassLoader.getParent(), excludedPackages, + testClassClassLoader); + } + + private static List readExcludedPackages(Class testClass, Method testMethod) { + + return Stream.of( // + AnnotatedElementUtils.findMergedAnnotation(testClass, ClassPathExclusions.class), + AnnotatedElementUtils.findMergedAnnotation(testMethod, ClassPathExclusions.class) // + ).filter(Objects::nonNull) // + .map(ClassPathExclusions::packages) // + .collect(new CombingArrayCollector()); + } + + private static URL toURL(String entry) { + try { + return new File(entry).toURI().toURL(); + } catch (Exception ex) { + throw new IllegalArgumentException(ex); + } + } + + private static class CombingArrayCollector implements Collector, List> { + + @Override + public Supplier> supplier() { + return ArrayList::new; + } + + @Override + public BiConsumer, T[]> accumulator() { + return (target, values) -> target.addAll(Arrays.asList(values)); + } + + @Override + public BinaryOperator> combiner() { + return (r1, r2) -> { + r1.addAll(r2); + return r1; + }; + } + + @Override + public Function, List> finisher() { + return i -> (List) i; + } + + @Override + public Set characteristics() { + return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH)); + } + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java new file mode 100644 index 0000000000..534146083f --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java @@ -0,0 +1,44 @@ +/* + * Copyright 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.util; + +import static org.assertj.core.api.Assertions.*; + +import org.junit.jupiter.api.Test; +import org.springframework.data.mongodb.test.util.ClassPathExclusions; +import org.springframework.util.ClassUtils; + +import com.mongodb.internal.build.MongoDriverVersion; + +/** + * Tests for {@link MongoClientVersion}. + * + * @author Christoph Strobl + */ +class MongoClientVersionUnitTests { + + @Test // GH-4578 + void parsesClientVersionCorrectly() { + assertThat(MongoClientVersion.isVersion5OrNewer()).isEqualTo(MongoDriverVersion.VERSION.startsWith("5")); + } + + @Test // GH-4578 + @ClassPathExclusions(packages = { "com.mongodb.internal.build" }) + void fallsBackToClassLookupIfDriverVersionNotPresent() { + assertThat(MongoClientVersion.isVersion5OrNewer()).isEqualTo( + ClassUtils.isPresent("com.mongodb.internal.connection.StreamFactoryFactory", this.getClass().getClassLoader())); + } +} 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 new file mode 100644 index 0000000000..2e9b8c4f58 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java @@ -0,0 +1,49 @@ +/* + * Copyright 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.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)); + + } +} diff --git a/src/main/antora/modules/ROOT/nav.adoc b/src/main/antora/modules/ROOT/nav.adoc index 559aee6b2a..f308084bf8 100644 --- a/src/main/antora/modules/ROOT/nav.adoc +++ b/src/main/antora/modules/ROOT/nav.adoc @@ -5,6 +5,7 @@ *** xref:migration-guide/migration-guide-3.x-to-4.x.adoc[] * xref:mongodb.adoc[] +** xref:preface.adoc[] ** xref:mongodb/getting-started.adoc[] ** xref:mongodb/configuration.adoc[] diff --git a/src/main/antora/modules/ROOT/pages/preface.adoc b/src/main/antora/modules/ROOT/pages/preface.adoc index d21df39cac..793f7b803a 100644 --- a/src/main/antora/modules/ROOT/pages/preface.adoc +++ b/src/main/antora/modules/ROOT/pages/preface.adoc @@ -1,50 +1,12 @@ -[[preface]] -= Preface - -The Spring Data MongoDB project applies core Spring concepts to the development of solutions that use the MongoDB document style data store. We provide a "`template`" as a high-level abstraction for storing and querying documents. You may notice similarities to the JDBC support provided by the Spring Framework. - -This document is the reference guide for Spring Data - MongoDB Support. It explains MongoDB module concepts and semantics and syntax for various store namespaces. - -This section provides some basic introduction to Spring and Document databases. The rest of the document refers only to Spring Data MongoDB features and assumes the user is familiar with MongoDB and Spring concepts. - -[[get-started:first-steps:spring]] -== Learning Spring - -Spring Data uses Spring framework's link:{springDocsUrl}/core.html[core] functionality, including: - -* link:{springDocsUrl}/core.html#beans[IoC] container -* link:{springDocsUrl}/core.html#validation[type conversion system] -* link:{springDocsUrl}/core.html#expressions[expression language] -* link:{springDocsUrl}/integration.html#jmx[JMX integration] -* link:{springDocsUrl}/data-access.html#dao-exceptions[DAO exception hierarchy]. - -While you need not know the Spring APIs, understanding the concepts behind them is important. At a minimum, the idea behind Inversion of Control (IoC) should be familiar, and you should be familiar with whatever IoC container you choose to use. - -The core functionality of the MongoDB support can be used directly, with no need to invoke the IoC services of the Spring Container. This is much like `JdbcTemplate`, which can be used "'standalone'" without any other services of the Spring container. To leverage all the features of Spring Data MongoDB, such as the repository support, you need to configure some parts of the library to use Spring. - -To learn more about Spring, you can refer to the comprehensive documentation that explains the Spring Framework in detail. There are a lot of articles, blog entries, and books on the subject. See the Spring framework https://spring.io/docs[home page] for more information. - -[[get-started:first-steps:nosql]] -== Learning NoSQL and Document databases -NoSQL stores have taken the storage world by storm. It is a vast domain with a plethora of solutions, terms, and patterns (to make things worse, even the term itself has multiple https://www.google.com/search?q=nosoql+acronym[meanings]). While some of the principles are common, you must be familiar with MongoDB to some degree. The best way to get acquainted is to read the documentation and follow the examples. It usually does not take more then 5-10 minutes to go through them and, especially if you are coming from an RDMBS-only background, these exercises can be an eye opener. - -The starting point for learning about MongoDB is https://www.mongodb.org/[www.mongodb.org]. Here is a list of other useful resources: - -* The https://docs.mongodb.org/manual/[manual] introduces MongoDB and contains links to getting started guides, reference documentation, and tutorials. -* Visit https://learn.mongodb.com/[MongoDB University] for free training material and online courses. -* MongoDB https://docs.mongodb.org/ecosystem/drivers/java/[Java Language Center]. -* Several https://www.mongodb.org/books[books] you can purchase. -* Karl Seguin's online book: https://openmymind.net/mongodb.pdf[The Little MongoDB Book]. - [[requirements]] -== Requirements += Requirements The Spring Data MongoDB 4.x binaries require JDK level 17 and above and https://spring.io/docs[Spring Framework] {springVersion} and above. -In terms of document stores, you need at least version 3.6 of https://www.mongodb.org/[MongoDB], though we recommend a more recent version. +In terms of database and driver, you need at least version 4.x of https://www.mongodb.org/[MongoDB] and a compatible MongoDB Java Driver (4.x or 5.x). [[compatibility.matrix]] -=== Compatibility Matrix +== Compatibility Matrix The following compatibility matrix summarizes Spring Data versions to MongoDB driver/database versions. Database versions show the highest supported server version that pass the Spring Data test suite. @@ -59,6 +21,11 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit |Driver Version |Server Version +|2024.0 +|4.3.x +|4.11.x & 5.x +|6.x + |2023.0 |4.1.x |4.9.x @@ -108,32 +75,13 @@ See also the https://www.mongodb.com/docs/drivers/java/sync/current/compatibilit [[compatibility.changes]] [[compatibility.changes-4.4]] -==== Relevant Changes in MongoDB 4.4 +=== Relevant Changes in MongoDB 4.4 * Fields list must not contain text search score property when no `$text` criteria present. See also https://docs.mongodb.com/manual/reference/operator/query/text/[`$text` operator] * Sort must not be an empty document when running map reduce. [[compatibility.changes-4.2]] -==== Relevant Changes in MongoDB 4.2 +=== Relevant Changes in MongoDB 4.2 * Removal of `geoNear` command. See also https://docs.mongodb.com/manual/release-notes/4.2-compatibility/#remove-support-for-the-geonear-command[Removal of `geoNear`] * Removal of `eval` command. See also https://docs.mongodb.com/manual/release-notes/4.2-compatibility/#remove-support-for-the-eval-command[Removal of `eval`] - -[[get-started:help]] -== Additional Help Resources - -Learning a new framework is not always straightforward. -In this section, we try to provide what we think is an easy-to-follow guide for starting with the Spring Data MongoDB module. -However, if you encounter issues or you need advice, feel free to use one of the following links: - -[[get-started:help:community]] -Community Forum :: Spring Data on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow] is a tag for all Spring Data (not just Document) users to share information and help each other. -Note that registration is needed only for posting. - -[[get-started:help:professional]] -Professional Support :: Professional, from-the-source support, with guaranteed response time, is available from https://pivotal.io/[Pivotal Software, Inc.], the company behind Spring Data and Spring. - -[[get-started:up-to-date]] -== Following Development - -For information on the Spring Data Mongo source code repository, nightly builds, and snapshot artifacts, see the Spring Data Mongo https://spring.io/projects/spring-data-mongodb/[homepage]. You can help make Spring Data best serve the needs of the Spring community by interacting with developers through the Community on https://stackoverflow.com/questions/tagged/spring-data[Stack Overflow]. To follow developer activity, look for the mailing list information on the Spring Data Mongo https://spring.io/projects/spring-data-mongodb/[homepage]. If you encounter a bug or want to suggest an improvement, please create a ticket on the Spring Data https://github.com/spring-projects/spring-data-mongodb/issues[issue tracker]. To stay up to date with the latest news and announcements in the Spring eco system, subscribe to the Spring Community https://spring.io[Portal]. You can also follow the Spring https://spring.io/blog[blog] or the project team on Twitter (https://twitter.com/SpringData[SpringData]).