Browse Source

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
pull/4633/head
Christoph Strobl 2 years ago committed by Mark Paluch
parent
commit
bce85c4419
No known key found for this signature in database
GPG Key ID: 55BC6374BAA9D973
  1. 28
      Jenkinsfile
  2. 7
      pom.xml
  3. 6
      spring-data-mongodb/pom.xml
  4. 188
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java
  5. 17
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
  6. 38
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
  7. 4
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/IndexConverters.java
  8. 89
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoClientSettingsFactoryBean.java
  9. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java
  10. 8
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java
  11. 7
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java
  12. 18
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java
  13. 5
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java
  14. 48
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java
  15. 378
      spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java
  16. 128
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java
  17. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java
  18. 3
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java
  19. 5
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java
  20. 6
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java
  21. 17
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java
  22. 4
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java
  23. 8
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java
  24. 168
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java
  25. 45
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java
  26. 129
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java
  27. 13
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java
  28. 34
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java
  29. 34
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java
  30. 3
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/MongoTestTemplate.java
  31. 142
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java
  32. 44
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java
  33. 49
      spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java

28
Jenkinsfile vendored

@ -265,6 +265,34 @@ pipeline { @@ -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'

7
pom.xml

@ -132,6 +132,13 @@ @@ -132,6 +132,13 @@
<module>spring-data-mongodb-benchmarks</module>
</modules>
</profile>
<profile>
<id>mongo-5.0</id>
<properties>
<mongo>5.0.0-beta0</mongo>
</properties>
</profile>
</profiles>
<dependencies>

6
spring-data-mongodb/pom.xml

@ -260,6 +260,12 @@ @@ -260,6 +260,12 @@
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-launcher</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>jakarta.transaction</groupId>
<artifactId>jakarta.transaction-api</artifactId>

188
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoCompatibilityAdapter.java

@ -0,0 +1,188 @@ @@ -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> 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> T getStreamFactoryFactory();
}
public interface ClientSettingsBuilderAdapter {
<T> 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);
}
}
}
}
}

17
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java

@ -25,6 +25,9 @@ import org.springframework.lang.Nullable; @@ -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 { @@ -33,13 +36,27 @@ public class MongoAotPredicates {
public static final Predicate<Class<?>> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type) || TypeUtils.type(type).isPartOf("org.bson");
public static final Predicate<ReactiveLibrary> IS_REACTIVE_LIBARARY_AVAILABLE = ReactiveWrappers::isAvailable;
public static final Predicate<ClassLoader> IS_SYNC_CLIENT_PRESENT = (classLoader) -> ClassUtils.isPresent("com.mongodb.client.MongoClient", classLoader);
public static final Predicate<ClassLoader> 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);
}
}

38
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.*; @@ -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 @@ -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 { @@ -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 { @@ -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);
}
}
}

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

@ -19,8 +19,10 @@ import java.util.concurrent.TimeUnit; @@ -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 { @@ -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());

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

@ -15,6 +15,7 @@ @@ -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; @@ -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; @@ -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<MongoCli @@ -54,7 +61,9 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
private static final MongoClientSettings DEFAULT_MONGO_SETTINGS = MongoClientSettings.builder().build();
private CodecRegistry codecRegistry = DEFAULT_MONGO_SETTINGS.getCodecRegistry();
private StreamFactoryFactory streamFactoryFactory = DEFAULT_MONGO_SETTINGS.getStreamFactoryFactory();
@Nullable private Object streamFactoryFactory;
@Nullable private TransportSettings transportSettings;
private ReadPreference readPreference = DEFAULT_MONGO_SETTINGS.getReadPreference();
private ReadConcern readConcern = DEFAULT_MONGO_SETTINGS.getReadConcern();
@ -116,6 +125,10 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli @@ -116,6 +125,10 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
private @Nullable AutoEncryptionSettings autoEncryptionSettings;
private @Nullable ServerApi serverApi;
{
streamFactoryFactory = MongoCompatibilityAdapter.clientSettingsAdapter(DEFAULT_MONGO_SETTINGS).getStreamFactoryFactory();
}
/**
* @param socketConnectTimeoutMS in msec
* @see com.mongodb.connection.SocketSettings.Builder#connectTimeout(int, TimeUnit)
@ -367,13 +380,16 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli @@ -367,13 +380,16 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
}
/**
* @param streamFactoryFactory
* @see MongoClientSettings.Builder#streamFactoryFactory(StreamFactoryFactory)
* @param streamFactoryFactory // * @see MongoClientSettings.Builder#streamFactoryFactory(StreamFactoryFactory)
*/
public void setStreamFactoryFactory(StreamFactoryFactory streamFactoryFactory) {
public void setStreamFactoryFactory(Object streamFactoryFactory) {
this.streamFactoryFactory = streamFactoryFactory;
}
public void setTransportSettings(@Nullable TransportSettings transportSettings) {
this.transportSettings = transportSettings;
}
/**
* @param codecRegistry
* @see MongoClientSettings.Builder#codecRegistry(CodecRegistry)
@ -478,12 +494,18 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli @@ -478,12 +494,18 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
}
});
if (transportSettings != null) {
builder.transportSettings(transportSettings);
}
if (streamFactoryFactory != null) {
builder = builder.streamFactoryFactory(streamFactoryFactory);
MongoCompatibilityAdapter.clientSettingsBuilderAdapter(builder).setStreamFactoryFactory(streamFactoryFactory);
}
if (retryReads != null) {
builder = builder.retryReads(retryReads);
}
if (retryWrites != null) {
builder = builder.retryWrites(retryWrites);
}
@ -496,4 +518,59 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli @@ -496,4 +518,59 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<MongoCli
return builder.build();
}
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,
MongoStreamFactoryFactorySettingsConfigurer.class.getClassLoader());
private final MongoClientSettings.Builder settingsBuilder;
static boolean isStreamFactoryPresent() {
return STREAM_FACTORY_PRESENT;
}
static Object getDefaultStreamFactoryFactory() {
if (MongoClientVersion.is5PlusClient()) {
return null;
}
Method getStreamFactoryFactory = ReflectionUtils.findMethod(MongoClientSettings.class, "getStreamFactoryFactory");
return getStreamFactoryFactory != null
? ReflectionUtils.invokeMethod(getStreamFactoryFactory, DEFAULT_MONGO_SETTINGS)
: null;
}
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);
}
}
}
}
}

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

@ -53,6 +53,7 @@ import org.springframework.data.geo.Metric; @@ -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; @@ -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 @@ -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 @@ -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 @@ -2340,7 +2342,7 @@ public class MongoTemplate
public Set<String> getCollectionNames() {
return execute(db -> {
Set<String> result = new LinkedHashSet<>();
for (String name : db.listCollectionNames()) {
for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) {
result.add(name);
}
return result;

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

@ -17,6 +17,8 @@ package org.springframework.data.mongodb.core; @@ -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 @@ -736,7 +738,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public Mono<Boolean> 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 @@ -784,7 +786,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
@Override
public Flux<String> getCollectionNames() {
return createFlux(MongoDatabase::listCollectionNames);
return createFlux(db -> MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(db).listCollectionNames());
}
public Mono<MongoDatabase> getMongoDatabase() {
@ -2172,7 +2174,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @@ -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()) {

7
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/GeospatialIndex.java

@ -19,6 +19,7 @@ import java.util.Optional; @@ -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 { @@ -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<IndexFilter> filter = Optional.empty();
private Optional<Collation> collation = Optional.empty();
@ -207,7 +208,9 @@ public class GeospatialIndex implements IndexDefinition { @@ -207,7 +208,9 @@ public class GeospatialIndex implements IndexDefinition {
case GEO_HAYSTACK:
document.put("bucketSize", bucketSize);
if (bucketSize != null) {
document.put("bucketSize", bucketSize);
}
break;
}

18
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexResolver.java

@ -25,6 +25,7 @@ import java.util.HashSet; @@ -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; @@ -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 { @@ -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<Double> 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);
}

5
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/observability/DefaultMongoHandlerObservationConvention.java

@ -17,7 +17,9 @@ package org.springframework.data.mongodb.observability; @@ -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 @@ -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) {

48
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoClientVersion.java

@ -15,8 +15,12 @@ @@ -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; @@ -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 { @@ -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 { @@ -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");
}
}

378
spring-data-mongodb/src/main/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapter.java

@ -0,0 +1,378 @@ @@ -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.
* <p>
* 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> 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> T getStreamFactoryFactory();
}
public interface ClientSettingsBuilderAdapter {
<T> 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<? extends MongoIterable<String>> collectionNameIterableType() {
return (Class<? extends MongoIterable<String>>) collectionNamesReturnType;
}
public MongoIterable<String> listCollectionNames() {
Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db));
return (MongoIterable<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db);
}
public MongoIterable<String> listCollectionNames(ClientSession clientSession) {
Assert.state(LIST_COLLECTION_NAMES_METHOD != null,
"No method listCollectionNames(ClientSession) present for %s".formatted(db));
return (MongoIterable<String>) 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<? extends Publisher<String>> collectionNamePublisherType() {
return (Class<? extends Publisher<String>>) collectionNamesReturnType;
}
public Publisher<String> listCollectionNames() {
Assert.state(LIST_COLLECTION_NAMES_METHOD != null, "No method listCollectionNames present for %s".formatted(db));
return (Publisher<String>) ReflectionUtils.invokeMethod(LIST_COLLECTION_NAMES_METHOD, db);
}
public Publisher<String> 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<String>) 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);
}
}
}
}

128
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/aot/MongoRuntimeHintsUnitTests.java

@ -0,0 +1,128 @@ @@ -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<RuntimeHints> 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<RuntimeHints> 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<RuntimeHints> 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<RuntimeHints> 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<RuntimeHints> 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);
}
}

4
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/DefaultBulkOperationsUnitTests.java

@ -377,7 +377,7 @@ class DefaultBulkOperationsUnitTests { @@ -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 { @@ -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());

3
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/MongoExceptionTranslatorUnitTests.java

@ -21,6 +21,7 @@ import org.bson.BsonDocument; @@ -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 { @@ -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);
}

5
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java

@ -58,6 +58,7 @@ import com.mongodb.reactivestreams.client.MapReducePublisher; @@ -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 { @@ -82,6 +83,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests {
@Mock MongoDatabase database;
@Mock ClientSession clientSession;
@Mock FindPublisher findPublisher;
Publisher<String> collectionNamesPublisher;
@Mock AggregatePublisher aggregatePublisher;
@Mock DistinctPublisher distinctPublisher;
@Mock Publisher resultPublisher;
@ -92,12 +94,13 @@ public class ReactiveSessionBoundMongoTemplateUnitTests { @@ -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);

6
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java

@ -96,9 +96,9 @@ public class SessionBoundMongoTemplateTests { @@ -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;
}
};

17
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.*; @@ -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; @@ -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 { @@ -84,6 +76,7 @@ public class SessionBoundMongoTemplateUnitTests {
@Mock MongoClient client;
@Mock ClientSession clientSession;
@Mock FindIterable findIterable;
MongoIterable<String> collectionNamesIterable;
@Mock MongoIterable mongoIterable;
@Mock DistinctIterable distinctIterable;
@Mock AggregateIterable aggregateIterable;
@ -97,11 +90,12 @@ public class SessionBoundMongoTemplateUnitTests { @@ -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 { @@ -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);

4
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/geo/GeoSpatialIndexTests.java

@ -21,6 +21,7 @@ import java.util.ArrayList; @@ -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; @@ -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 { @@ -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();

8
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/ImperativeIntegrationTests.java

@ -23,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith; @@ -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 { @@ -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");
}
}
};
}

168
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/observability/MongoObservationCommandListenerTests.java

@ -70,7 +70,7 @@ class MongoObservationCommandListenerTests { @@ -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 { @@ -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 { @@ -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 { @@ -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() {

45
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusions.java

@ -0,0 +1,45 @@ @@ -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 <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ClassPathExclusions.java">ClassPathExclusions</a>.
*
* @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();
}

129
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ClassPathExclusionsExtension.java

@ -0,0 +1,129 @@ @@ -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 <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java">ModifiedClassPathExtension</a>.
*
* @author Christoph Strobl
*/
class ClassPathExclusionsExtension implements InvocationInterceptor {
@Override
public void interceptBeforeAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept(invocation, extensionContext);
}
@Override
public void interceptBeforeEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept(invocation, extensionContext);
}
@Override
public void interceptAfterEachMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept(invocation, extensionContext);
}
@Override
public void interceptAfterAllMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
intercept(invocation, extensionContext);
}
@Override
public void interceptTestMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> invocationContext,
ExtensionContext extensionContext) throws Throwable {
interceptMethod(invocation, invocationContext, extensionContext);
}
@Override
public void interceptTestTemplateMethod(Invocation<Void> invocation,
ReflectiveInvocationContext<Method> invocationContext, ExtensionContext extensionContext) throws Throwable {
interceptMethod(invocation, invocationContext, extensionContext);
}
private void interceptMethod(Invocation<Void> invocation, ReflectiveInvocationContext<Method> 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<Void> 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());
}
}

13
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/CleanMongoDBTests.java

@ -31,13 +31,14 @@ import org.mockito.Mock; @@ -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 { @@ -62,7 +63,7 @@ class CleanMongoDBTests {
@SuppressWarnings({ "serial", "unchecked" })
@BeforeEach
void setUp() {
void setUp() throws ClassNotFoundException {
// DB setup
@ -73,13 +74,13 @@ class CleanMongoDBTests { @@ -73,13 +74,13 @@ class CleanMongoDBTests {
when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock);
// collections have to exist
ListDatabasesIterable<String> collectionIterable = mock(ListDatabasesIterable.class);
MongoIterable<String> 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<String> collectionIterable2 = mock(ListDatabasesIterable.class);
MongoIterable<String> 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);

34
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeReactiveClientFromClassPath.java

@ -0,0 +1,34 @@ @@ -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 {
}

34
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ExcludeSyncClientFromClassPath.java

@ -0,0 +1,34 @@ @@ -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 {
}

3
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; @@ -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 { @@ -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<String> collections) {

142
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/PackageExcludingClassLoader.java

@ -0,0 +1,142 @@ @@ -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 <a href="https://github.com/spring-projects/spring-boot/blob/main/spring-boot-project/spring-boot-tools/spring-boot-test-support/src/main/java/org/springframework/boot/testsupport/classpath/ModifiedClassPathClassLoader.java">ModifiedClassPathClassLoader</a>.
*
* @author Christoph Strobl
*/
class PackageExcludingClassLoader extends URLClassLoader {
private final Set<String> excludedPackages;
private final ClassLoader junitLoader;
PackageExcludingClassLoader(URL[] urls, ClassLoader parent, Collection<String> 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<String> excludedPackages = readExcludedPackages(testClass, testMethod);
if (excludedPackages.isEmpty()) {
return null;
}
ClassLoader testClassClassLoader = testClass.getClassLoader();
Stream<URL> 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<String> 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<String>());
}
private static URL toURL(String entry) {
try {
return new File(entry).toURI().toURL();
} catch (Exception ex) {
throw new IllegalArgumentException(ex);
}
}
private static class CombingArrayCollector<T> implements Collector<T[], List<T>, List<T>> {
@Override
public Supplier<List<T>> supplier() {
return ArrayList::new;
}
@Override
public BiConsumer<List<T>, T[]> accumulator() {
return (target, values) -> target.addAll(Arrays.asList(values));
}
@Override
public BinaryOperator<List<T>> combiner() {
return (r1, r2) -> {
r1.addAll(r2);
return r1;
};
}
@Override
public Function<List<T>, List<T>> finisher() {
return i -> (List<T>) i;
}
@Override
public Set<Characteristics> characteristics() {
return Collections.unmodifiableSet(EnumSet.of(Characteristics.IDENTITY_FINISH));
}
}
}

44
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoClientVersionUnitTests.java

@ -0,0 +1,44 @@ @@ -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()));
}
}

49
spring-data-mongodb/src/test/java/org/springframework/data/mongodb/util/MongoCompatibilityAdapterUnitTests.java

@ -0,0 +1,49 @@ @@ -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));
}
}
Loading…
Cancel
Save