From bce85c4419fc4c84d7419a4fad384f0401d74192 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Fri, 1 Dec 2023 17:54:25 +0100 Subject: [PATCH] Assert compatibility with MongoDB Driver 5. We now are compatible with MongoDB driver versions 4 and 5. Driver versions can be interchanged and our adapter bridges changed methods via reflection. Usage of removed functionality is either ignored or fails with an exception. Original pull request: #4624 Closes: #4578 --- Jenkinsfile | 28 ++ pom.xml | 7 + spring-data-mongodb/pom.xml | 6 + .../mongodb/MongoCompatibilityAdapter.java | 188 +++++++++ .../data/mongodb/aot/MongoAotPredicates.java | 17 + .../data/mongodb/aot/MongoRuntimeHints.java | 38 ++ .../data/mongodb/core/IndexConverters.java | 4 +- .../core/MongoClientSettingsFactoryBean.java | 89 ++++- .../data/mongodb/core/MongoTemplate.java | 8 +- .../mongodb/core/ReactiveMongoTemplate.java | 8 +- .../mongodb/core/index/GeospatialIndex.java | 7 +- .../MongoPersistentEntityIndexResolver.java | 18 +- ...aultMongoHandlerObservationConvention.java | 5 +- .../data/mongodb/util/MongoClientVersion.java | 48 ++- .../util/MongoCompatibilityAdapter.java | 378 ++++++++++++++++++ .../aot/MongoRuntimeHintsUnitTests.java | 128 ++++++ .../core/DefaultBulkOperationsUnitTests.java | 4 +- .../MongoExceptionTranslatorUnitTests.java | 3 +- ...iveSessionBoundMongoTemplateUnitTests.java | 5 +- .../core/SessionBoundMongoTemplateTests.java | 6 +- .../SessionBoundMongoTemplateUnitTests.java | 17 +- .../core/geo/GeoSpatialIndexTests.java | 4 + .../ImperativeIntegrationTests.java | 8 +- .../MongoObservationCommandListenerTests.java | 168 ++++---- .../test/util/ClassPathExclusions.java | 45 +++ .../util/ClassPathExclusionsExtension.java | 129 ++++++ .../mongodb/test/util/CleanMongoDBTests.java | 13 +- .../ExcludeReactiveClientFromClassPath.java | 34 ++ .../util/ExcludeSyncClientFromClassPath.java | 34 ++ .../mongodb/test/util/MongoTestTemplate.java | 3 +- .../util/PackageExcludingClassLoader.java | 142 +++++++ .../util/MongoClientVersionUnitTests.java | 44 ++ .../MongoCompatibilityAdapterUnitTests.java | 49 +++ 33 files changed, 1557 insertions(+), 128 deletions(-) create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java create mode 100644 spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java create mode 100644 spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java diff --git a/Jenkinsfile b/Jenkinsfile index f122dfb0c..da92e1b68 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 aff1afc48..a67ad8716 100644 --- a/pom.xml +++ b/pom.xml @@ -132,6 +132,13 @@ spring-data-mongodb-benchmarks + + mongo-5.0 + + 5.0.0-beta0 + + + diff --git a/spring-data-mongodb/pom.xml b/spring-data-mongodb/pom.xml index 39fc1a1de..d319e29e8 100644 --- a/spring-data-mongodb/pom.xml +++ b/spring-data-mongodb/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/MongoCompatibilityAdapter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java new file mode 100644 index 000000000..5b7f619bf --- /dev/null +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java @@ -0,0 +1,188 @@ +/* + * Copyright 2023 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; + +import java.lang.reflect.Method; +import java.net.InetAddress; +import java.net.InetSocketAddress; + +import com.mongodb.ServerAddress; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean; +import org.springframework.data.mongodb.util.MongoClientVersion; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +import com.mongodb.MongoClientSettings; +import com.mongodb.MongoClientSettings.Builder; +import com.mongodb.client.MapReduceIterable; +import com.mongodb.client.model.IndexOptions; +import com.mongodb.reactivestreams.client.MapReducePublisher; + +/** + * @author Christoph Strobl + * @since 2023/12 + */ +public class MongoCompatibilityAdapter { + + private static final String NO_LONGER_SUPPORTED = "%s is no longer supported on Mongo Client 5+"; + + public static ClientSettingsBuilderAdapter clientSettingsBuilderAdapter(MongoClientSettings.Builder builder) { + return new MongoStreamFactoryFactorySettingsConfigurer(builder)::setStreamFactory; + } + + public static ClientSettingsAdapter clientSettingsAdapter(MongoClientSettings clientSettings) { + return new ClientSettingsAdapter() { + @Override + public T getStreamFactoryFactory() { + if (MongoClientVersion.is5PlusClient()) { + return null; + } + + Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, + "getStreamFactoryFactory"); + return getStreamFactoryFactory != null + ? (T) ReflectionUtils.invokeMethod(getStreamFactoryFactory, clientSettings) + : null; + } + }; + } + + public static IndexOptionsAdapter indexOptionsAdapter(IndexOptions options) { + return new IndexOptionsAdapter() { + @Override + public void setBucketSize(Double bucketSize) { + + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("IndexOptions.bucketSize")); + } + + Method setBucketSize = ReflectionUtils.findMethod(IndexOptions.class, "bucketSize", Double.class); + ReflectionUtils.invokeMethod(setBucketSize, options, bucketSize); + } + }; + } + + @SuppressWarnings({ "deprecation" }) + public static MapReduceIterableAdapter mapReduceIterableAdapter(MapReduceIterable iterable) { + return sharded -> { + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); + } + + Method shardedMethod = ReflectionUtils.findMethod(iterable.getClass(), "MapReduceIterable.sharded", + boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, iterable, shardedMethod); + }; + } + + public static MapReducePublisherAdapter mapReducePublisherAdapter(MapReducePublisher publisher) { + return sharded -> { + if (MongoClientVersion.is5PlusClient()) { + throw new UnsupportedOperationException(NO_LONGER_SUPPORTED.formatted("sharded")); + } + + Method shardedMethod = ReflectionUtils.findMethod(publisher.getClass(), "MapReduceIterable.sharded", + boolean.class); + ReflectionUtils.invokeMethod(shardedMethod, publisher, shardedMethod); + }; + } + + public static ServerAddressAdapter serverAddressAdapter(ServerAddress serverAddress) { + return new ServerAddressAdapter() { + @Override + public InetSocketAddress getSocketAddress() { + + if(MongoClientVersion.is5PlusClient()) { + return null; + } + + Method serverAddressMethod = ReflectionUtils.findMethod(serverAddress.getClass(), "getSocketAddress"); + Object value = ReflectionUtils.invokeMethod(serverAddressMethod, serverAddress); + return value != null ? InetSocketAddress.class.cast(value) : null; + } + }; + } + + public interface IndexOptionsAdapter { + void setBucketSize(Double bucketSize); + } + + public interface ClientSettingsAdapter { + 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 { + InetSocketAddress getSocketAddress(); + } + + static class MongoStreamFactoryFactorySettingsConfigurer { + + private static final Log logger = LogFactory.getLog(MongoClientSettingsFactoryBean.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.is5PlusClient()) { + logger.warn("StreamFactoryFactory is no longer available. Use TransportSettings instead."); + } + + if (isStreamFactoryPresent()) { // + 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 (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); + } + } + } + } + +} 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 3fcded33e..40e4a7466 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.lang.Nullable; 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 a49d0bece..226a10229 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 static org.springframework.data.mongodb.aot.MongoAotPredicates.*; 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.ReactiveAfterConvertC 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 @@ class MongoRuntimeHints implements RuntimeHintsRegistrar { MemberCategory.INVOKE_PUBLIC_METHODS)); registerTransactionProxyHints(hints, classLoader); + registerMongoCompatibilityAdapterHints(hints, classLoader); if (isReactorPresent()) { @@ -80,4 +89,33 @@ class MongoRuntimeHints implements RuntimeHintsRegistrar { } } + 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/IndexConverters.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java index 248c7c514..8d450ad0a 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 @@ -19,8 +19,10 @@ import java.util.concurrent.TimeUnit; import org.bson.Document; import org.springframework.core.convert.converter.Converter; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.core.index.IndexDefinition; import org.springframework.data.mongodb.core.index.IndexInfo; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; @@ -89,7 +91,7 @@ abstract class IndexConverters { 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 9be7cb4fd..060bcf059 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import java.lang.reflect.Method; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Collections; @@ -23,11 +24,17 @@ import java.util.concurrent.TimeUnit; import javax.net.ssl.SSLContext; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.bson.UuidRepresentation; import org.bson.codecs.configuration.CodecRegistry; import org.springframework.beans.factory.config.AbstractFactoryBean; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import com.mongodb.AutoEncryptionSettings; @@ -40,7 +47,7 @@ import com.mongodb.ServerApi; 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 +61,9 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean 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 (ClassNotFoundException e) { + throw new IllegalArgumentException("Cannot set StreamFactoryFactory for %s".formatted(settingsBuilder), e); + } + } + } + } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 49c935426..7634fec94 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -53,6 +53,7 @@ import org.springframework.data.geo.Metric; import org.springframework.data.mapping.MappingException; import org.springframework.data.mapping.callback.EntityCallbacks; import org.springframework.data.mapping.context.MappingContext; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.MongoDatabaseFactory; import org.springframework.data.mongodb.MongoDatabaseUtils; import org.springframework.data.mongodb.SessionSynchronization; @@ -103,6 +104,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition; import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter; import org.springframework.data.mongodb.core.timeseries.Granularity; import org.springframework.data.mongodb.core.validation.Validator; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.data.projection.EntityProjection; import org.springframework.data.util.CloseableIterator; import org.springframework.data.util.Optionals; @@ -722,7 +724,7 @@ public class MongoTemplate return execute(db -> { - for (String name : db.listCollectionNames()) { + for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) { if (name.equals(collectionName)) { return true; } @@ -1965,7 +1967,7 @@ public class MongoTemplate } 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 @@ public class MongoTemplate 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 c4096a277..848713528 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 @@ -17,6 +17,8 @@ package org.springframework.data.mongodb.core; import static org.springframework.data.mongodb.core.query.SerializationUtils.*; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; +import org.springframework.data.mongodb.util.MongoClientVersion; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.util.function.Tuple2; @@ -736,7 +738,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @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 class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @Override public Flux getCollectionNames() { - return createFlux(MongoDatabase::listCollectionNames); + return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames()); } public Mono getMongoDatabase() { @@ -2172,7 +2174,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } 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 8492ad3bc..822ade040 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 java.util.Optional; 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 class GeospatialIndex implements IndexDefinition { 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 d9494249c..9a9cee26f 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.HashSet; 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.mapping.MongoPersistentProperty; 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 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver { .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 1c1320174..e9b8c4186 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java @@ -17,7 +17,9 @@ package org.springframework.data.mongodb.observability; import java.net.InetSocketAddress; +import org.springframework.data.mongodb.MongoCompatibilityAdapter; import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames; +import org.springframework.data.mongodb.util.MongoClientVersion; import org.springframework.util.ObjectUtils; import com.mongodb.ConnectionString; @@ -78,7 +80,8 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio 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 16b2daa87..c1cbe2bc5 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 @@ import org.springframework.util.ClassUtils; 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,29 @@ public class MongoClientVersion { public static boolean isReactiveClientPresent() { return REACTIVE_CLIENT_PRESENT; } + + public static boolean is5PlusClient() { + return IS_5PlusClient; + } + + @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 000000000..aa1eddabd --- /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 000000000..526ab39b1 --- /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 0b0139c4e..afaba3fa3 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 @@ class DefaultBulkOperationsUnitTests { 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 @@ class DefaultBulkOperationsUnitTests { 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 00c325f83..ff74786cb 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.bson.BsonDocument; 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 @@ class MongoExceptionTranslatorUnitTests { 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 c47b93967..8acc11241 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.MapReducePublisher; 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 2859db0a4..eca52d5bd 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 class SessionBoundMongoTemplateTests { @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 e9ac251e9..bec7a7a05 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 static org.springframework.data.mongodb.test.util.Assertions.*; 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.NearQuery; 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 class SessionBoundMongoTemplateUnitTests { 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 e20a934aa..0257ab339 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.ArrayList; 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 org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion; 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 class GeoSpatialIndexTests extends AbstractIntegrationTests { @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 3282c8693..bdeb5b7b0 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 @@ -23,6 +23,7 @@ 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; @@ -81,8 +82,13 @@ public class ImperativeIntegrationTests extends SampleTestRunner { 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.is5PlusClient()) { + 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 14a2d5a93..75c025d48 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 @@ -70,7 +70,7 @@ class MongoObservationCommandListenerTests { 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 +80,7 @@ class MongoObservationCommandListenerTests { 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 +90,7 @@ class MongoObservationCommandListenerTests { 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"); @@ -100,115 +100,115 @@ class MongoObservationCommandListenerTests { void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() { // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); - - // then - assertThatTimerRegisteredWithTags(); +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); +// +// // then +// assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() { // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 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)); // then - assertThatTimerRegisteredWithTags(); +// assertThatTimerRegisteredWithTags(); } @Test void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() { - // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", - new BsonDocument("collection", new BsonString("user")))); - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); - - assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), - KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), - LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), - LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), - LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); +// // given +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert", +// new BsonDocument("collection", new BsonString("user")))); +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); +// +// assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(), +// KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"), +// LowCardinalityCommandKeyNames.DB_NAME.withValue("database"), +// LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"), +// LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none")); } @Test void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() { - // given - Observation parent = Observation.start("name", observationRegistry); - RequestContext traceRequestContext = getContext(); - - // when - listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // - new ConnectionDescription( // - new ServerId( // - new ClusterId("description"), // - new ServerAddress("localhost", 1234))), // - "database", "insert", // - new BsonDocument("collection", new BsonString("user")))); - listener.commandFailed( // - new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); - - // then - assertThatTimerRegisteredWithTags(); +// // given +// Observation parent = Observation.start("name", observationRegistry); +// RequestContext traceRequestContext = getContext(); +// +// // when +// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, // +// new ConnectionDescription( // +// new ServerId( // +// new ClusterId("description"), // +// new ServerAddress("localhost", 1234))), // +// "database", "insert", // +// new BsonDocument("collection", new BsonString("user")))); +// listener.commandFailed( // +// new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException())); +// +// // then +// assertThatTimerRegisteredWithTags(); } @Test // GH-4481 void completionShouldIgnoreIncompatibleObservationContext() { - // given - RequestContext traceRequestContext = getContext(); - - Observation observation = mock(Observation.class); - traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); - - // when - listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); - - verify(observation).getContext(); - verifyNoMoreInteractions(observation); +// // given +// RequestContext traceRequestContext = getContext(); +// +// Observation observation = mock(Observation.class); +// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); +// +// // when +// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0)); +// +// verify(observation).getContext(); +// verifyNoMoreInteractions(observation); } @Test // GH-4481 void failureShouldIgnoreIncompatibleObservationContext() { - // given - RequestContext traceRequestContext = getContext(); - - Observation observation = mock(Observation.class); - traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); - - // when - listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); - - verify(observation).getContext(); - verifyNoMoreInteractions(observation); +// // given +// RequestContext traceRequestContext = getContext(); +// +// Observation observation = mock(Observation.class); +// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation); +// +// // when +// listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null)); +// +// verify(observation).getContext(); +// verifyNoMoreInteractions(observation); } private RequestContext getContext() { 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 000000000..264c5bfa0 --- /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 000000000..9d4454a26 --- /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 817e18e0c..fb7f774e9 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.Mock; 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 @@ class CleanMongoDBTests { 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 000000000..617bc5991 --- /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 000000000..ff8915386 --- /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 d561966dd..bab535cb5 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 org.springframework.data.mongodb.core.MongoTemplate; 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 class MongoTestTemplate extends MongoTemplate { } 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 000000000..ba8ed8b29 --- /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 000000000..534146083 --- /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 000000000..2e9b8c4f5 --- /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)); + + } +}