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
This commit is contained in:
committed by
Mark Paluch
parent
3bc214c016
commit
bce85c4419
28
Jenkinsfile
vendored
28
Jenkinsfile
vendored
@@ -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
7
pom.xml
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -25,6 +25,9 @@ import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
/**
|
||||
* Collection of {@link Predicate predicates} to determine dynamic library aspects during AOT computation.
|
||||
* Intended for internal usage only.
|
||||
*
|
||||
* @author Christoph Strobl
|
||||
* @since 4.0
|
||||
*/
|
||||
@@ -33,13 +36,27 @@ public class MongoAotPredicates {
|
||||
public static final Predicate<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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,13 @@ import static org.springframework.data.mongodb.aot.MongoAotPredicates.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import com.mongodb.MongoClientSettings;
|
||||
import com.mongodb.ServerAddress;
|
||||
import com.mongodb.UnixServerAddress;
|
||||
import com.mongodb.client.MapReduceIterable;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.model.IndexOptions;
|
||||
import com.mongodb.reactivestreams.client.MapReducePublisher;
|
||||
import org.springframework.aot.hint.MemberCategory;
|
||||
import org.springframework.aot.hint.RuntimeHints;
|
||||
import org.springframework.aot.hint.RuntimeHintsRegistrar;
|
||||
@@ -31,6 +38,7 @@ import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertC
|
||||
import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterSaveCallback;
|
||||
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeConvertCallback;
|
||||
import org.springframework.data.mongodb.core.mapping.event.ReactiveBeforeSaveCallback;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
@@ -53,6 +61,7 @@ class MongoRuntimeHints implements RuntimeHintsRegistrar {
|
||||
MemberCategory.INVOKE_PUBLIC_METHODS));
|
||||
|
||||
registerTransactionProxyHints(hints, classLoader);
|
||||
registerMongoCompatibilityAdapterHints(hints, classLoader);
|
||||
|
||||
if (isReactorPresent()) {
|
||||
|
||||
@@ -80,4 +89,33 @@ class MongoRuntimeHints implements RuntimeHintsRegistrar {
|
||||
}
|
||||
}
|
||||
|
||||
private static void registerMongoCompatibilityAdapterHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
|
||||
|
||||
hints.reflection() //
|
||||
.registerType(MongoClientSettings.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(MongoClientSettings.Builder.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(IndexOptions.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(ServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(UnixServerAddress.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(TypeReference.of("com.mongodb.connection.StreamFactoryFactory"), MemberCategory.INTROSPECT_PUBLIC_METHODS);
|
||||
|
||||
if(MongoAotPredicates.isSyncClientPresent(classLoader)) {
|
||||
|
||||
hints.reflection() //
|
||||
.registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(TypeReference.of("com.mongodb.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"), MemberCategory.INVOKE_PUBLIC_METHODS);
|
||||
}
|
||||
|
||||
if(MongoAotPredicates.isReactiveClientPresent(classLoader)) {
|
||||
|
||||
hints.reflection() //
|
||||
.registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MongoDatabaseImpl"), MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS)
|
||||
.registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"), MemberCategory.INVOKE_PUBLIC_METHODS);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
|
||||
import org.springframework.data.mongodb.core.index.IndexDefinition;
|
||||
import org.springframework.data.mongodb.core.index.IndexInfo;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
@@ -89,7 +91,7 @@ abstract class IndexConverters {
|
||||
ops = ops.bits((Integer) indexOptions.get("bits"));
|
||||
}
|
||||
if (indexOptions.containsKey("bucketSize")) {
|
||||
ops = ops.bucketSize(((Number) indexOptions.get("bucketSize")).doubleValue());
|
||||
MongoCompatibilityAdapter.indexOptionsAdapter(ops).setBucketSize(((Number) indexOptions.get("bucketSize")).doubleValue());
|
||||
}
|
||||
if (indexOptions.containsKey("default_language")) {
|
||||
ops = ops.defaultLanguage(indexOptions.get("default_language").toString());
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.core;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
@@ -23,11 +24,17 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.net.ssl.SSLContext;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.bson.UuidRepresentation;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
import org.springframework.beans.factory.config.AbstractFactoryBean;
|
||||
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import com.mongodb.AutoEncryptionSettings;
|
||||
@@ -40,7 +47,7 @@ import com.mongodb.ServerApi;
|
||||
import com.mongodb.WriteConcern;
|
||||
import com.mongodb.connection.ClusterConnectionMode;
|
||||
import com.mongodb.connection.ClusterType;
|
||||
import com.mongodb.connection.StreamFactoryFactory;
|
||||
import com.mongodb.connection.TransportSettings;
|
||||
|
||||
/**
|
||||
* A factory bean for construction of a {@link MongoClientSettings} instance to be used with a MongoDB driver.
|
||||
@@ -54,7 +61,9 @@ public class MongoClientSettingsFactoryBean extends AbstractFactoryBean<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
|
||||
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
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
}
|
||||
});
|
||||
|
||||
if (streamFactoryFactory != null) {
|
||||
builder = builder.streamFactoryFactory(streamFactoryFactory);
|
||||
if (transportSettings != null) {
|
||||
builder.transportSettings(transportSettings);
|
||||
}
|
||||
|
||||
if (streamFactoryFactory != null) {
|
||||
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
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ import org.springframework.data.geo.Metric;
|
||||
import org.springframework.data.mapping.MappingException;
|
||||
import org.springframework.data.mapping.callback.EntityCallbacks;
|
||||
import org.springframework.data.mapping.context.MappingContext;
|
||||
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
|
||||
import org.springframework.data.mongodb.MongoDatabaseFactory;
|
||||
import org.springframework.data.mongodb.MongoDatabaseUtils;
|
||||
import org.springframework.data.mongodb.SessionSynchronization;
|
||||
@@ -103,6 +104,7 @@ import org.springframework.data.mongodb.core.query.UpdateDefinition;
|
||||
import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
|
||||
import org.springframework.data.mongodb.core.timeseries.Granularity;
|
||||
import org.springframework.data.mongodb.core.validation.Validator;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.data.projection.EntityProjection;
|
||||
import org.springframework.data.util.CloseableIterator;
|
||||
import org.springframework.data.util.Optionals;
|
||||
@@ -722,7 +724,7 @@ public class MongoTemplate
|
||||
|
||||
return execute(db -> {
|
||||
|
||||
for (String name : db.listCollectionNames()) {
|
||||
for (String name : MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(db).listCollectionNames()) {
|
||||
if (name.equals(collectionName)) {
|
||||
return true;
|
||||
}
|
||||
@@ -1965,7 +1967,7 @@ public class MongoTemplate
|
||||
}
|
||||
|
||||
if (mapReduceOptions.getOutputSharded().isPresent()) {
|
||||
mapReduce = mapReduce.sharded(mapReduceOptions.getOutputSharded().get());
|
||||
MongoCompatibilityAdapter.mapReduceIterableAdapter(mapReduce).sharded(mapReduceOptions.getOutputSharded().get());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(mapReduceOptions.getOutputCollection()) && !mapReduceOptions.usesInlineOutput()) {
|
||||
@@ -2340,7 +2342,7 @@ public class MongoTemplate
|
||||
public Set<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;
|
||||
|
||||
@@ -17,6 +17,8 @@ package org.springframework.data.mongodb.core;
|
||||
|
||||
import static org.springframework.data.mongodb.core.query.SerializationUtils.*;
|
||||
|
||||
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.util.function.Tuple2;
|
||||
@@ -736,7 +738,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati
|
||||
|
||||
@Override
|
||||
public Mono<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
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
if (options.getOutputSharded().isPresent()) {
|
||||
publisher = publisher.sharded(options.getOutputSharded().get());
|
||||
MongoCompatibilityAdapter.mapReducePublisherAdapter(publisher).sharded(options.getOutputSharded().get());
|
||||
}
|
||||
|
||||
if (StringUtils.hasText(options.getOutputCollection()) && !options.usesInlineOutput()) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import java.util.Optional;
|
||||
|
||||
import org.bson.Document;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -40,7 +41,7 @@ public class GeospatialIndex implements IndexDefinition {
|
||||
private @Nullable Integer max;
|
||||
private @Nullable Integer bits;
|
||||
private GeoSpatialIndexType type = GeoSpatialIndexType.GEO_2D;
|
||||
private Double bucketSize = 1.0;
|
||||
private Double bucketSize = MongoClientVersion.isVersion5OrNewer() ? null : 1.0;
|
||||
private @Nullable String additionalField;
|
||||
private Optional<IndexFilter> filter = Optional.empty();
|
||||
private Optional<Collation> collation = Optional.empty();
|
||||
@@ -207,7 +208,9 @@ public class GeospatialIndex implements IndexDefinition {
|
||||
|
||||
case GEO_HAYSTACK:
|
||||
|
||||
document.put("bucketSize", bucketSize);
|
||||
if (bucketSize != null) {
|
||||
document.put("bucketSize", bucketSize);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import java.util.HashSet;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Supplier;
|
||||
@@ -53,6 +54,7 @@ import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
|
||||
import org.springframework.data.mongodb.core.query.Collation;
|
||||
import org.springframework.data.mongodb.util.BsonUtils;
|
||||
import org.springframework.data.mongodb.util.DotPath;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.data.mongodb.util.spel.ExpressionUtils;
|
||||
import org.springframework.data.spel.EvaluationContextProvider;
|
||||
import org.springframework.data.util.TypeInformation;
|
||||
@@ -708,7 +710,21 @@ public class MongoPersistentEntityIndexResolver implements IndexResolver {
|
||||
.named(pathAwareIndexName(index.name(), dotPath, persistentProperty.getOwner(), persistentProperty));
|
||||
}
|
||||
|
||||
indexDefinition.typed(index.type()).withBucketSize(index.bucketSize()).withAdditionalField(index.additionalField());
|
||||
if(MongoClientVersion.isVersion5OrNewer()) {
|
||||
|
||||
Optional<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);
|
||||
}
|
||||
|
||||
@@ -17,7 +17,9 @@ package org.springframework.data.mongodb.observability;
|
||||
|
||||
import java.net.InetSocketAddress;
|
||||
|
||||
import org.springframework.data.mongodb.MongoCompatibilityAdapter;
|
||||
import org.springframework.data.mongodb.observability.MongoObservation.LowCardinalityCommandKeyNames;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
|
||||
import com.mongodb.ConnectionString;
|
||||
@@ -78,7 +80,8 @@ class DefaultMongoHandlerObservationConvention implements MongoHandlerObservatio
|
||||
LowCardinalityCommandKeyNames.NET_PEER_NAME.withValue(serverAddress.getHost()),
|
||||
LowCardinalityCommandKeyNames.NET_PEER_PORT.withValue("" + serverAddress.getPort()));
|
||||
|
||||
InetSocketAddress socketAddress = serverAddress.getSocketAddress();
|
||||
|
||||
InetSocketAddress socketAddress = MongoCompatibilityAdapter.serverAddressAdapter(serverAddress).getSocketAddress();
|
||||
|
||||
if (socketAddress != null) {
|
||||
|
||||
|
||||
@@ -15,8 +15,12 @@
|
||||
*/
|
||||
package org.springframework.data.mongodb.util;
|
||||
|
||||
import org.springframework.data.util.Version;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.ClassUtils;
|
||||
|
||||
import com.mongodb.internal.build.MongoDriverVersion;
|
||||
|
||||
/**
|
||||
* {@link MongoClientVersion} holds information about the used mongo-java client and is used to distinguish between
|
||||
* different versions.
|
||||
@@ -28,7 +32,8 @@ import org.springframework.util.ClassUtils;
|
||||
public class MongoClientVersion {
|
||||
|
||||
private static final boolean SYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.MongoClient",
|
||||
MongoClientVersion.class.getClassLoader());
|
||||
MongoClientVersion.class.getClassLoader())
|
||||
|| ClassUtils.isPresent("com.mongodb.client.MongoClient", MongoClientVersion.class.getClassLoader());
|
||||
|
||||
private static final boolean ASYNC_CLIENT_PRESENT = ClassUtils.isPresent("com.mongodb.async.client.MongoClient",
|
||||
MongoClientVersion.class.getClassLoader());
|
||||
@@ -36,6 +41,22 @@ public class MongoClientVersion {
|
||||
private static final boolean REACTIVE_CLIENT_PRESENT = ClassUtils
|
||||
.isPresent("com.mongodb.reactivestreams.client.MongoClient", MongoClientVersion.class.getClassLoader());
|
||||
|
||||
private static final boolean IS_VERSION_5_OR_NEWER;
|
||||
|
||||
private static final Version CLIENT_VERSION;
|
||||
|
||||
static {
|
||||
|
||||
ClassLoader classLoader = MongoClientVersion.class.getClassLoader();
|
||||
Version version = readVersionFromClass(classLoader);
|
||||
if (version == null) {
|
||||
version = guessDriverVersionFromClassPath(classLoader);
|
||||
}
|
||||
|
||||
CLIENT_VERSION = version;
|
||||
IS_VERSION_5_OR_NEWER = CLIENT_VERSION.isGreaterThanOrEqualTo(Version.parse("5.0"));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return {@literal true} if the async MongoDB Java driver is on classpath.
|
||||
*/
|
||||
@@ -58,4 +79,29 @@ public class MongoClientVersion {
|
||||
public static boolean isReactiveClientPresent() {
|
||||
return REACTIVE_CLIENT_PRESENT;
|
||||
}
|
||||
|
||||
public static boolean is5PlusClient() {
|
||||
return IS_5PlusClient;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Version readVersionFromClass(ClassLoader classLoader) {
|
||||
|
||||
if (ClassUtils.isPresent("com.mongodb.internal.build.MongoDriverVersion", classLoader)) {
|
||||
try {
|
||||
return Version.parse(MongoDriverVersion.VERSION);
|
||||
} catch (IllegalArgumentException exception) {
|
||||
// well not much we can do, right?
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static Version guessDriverVersionFromClassPath(ClassLoader classLoader) {
|
||||
|
||||
if (ClassUtils.isPresent("com.mongodb.internal.connection.StreamFactoryFactory", classLoader)) {
|
||||
return Version.parse("5");
|
||||
}
|
||||
return Version.parse("4.11");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -377,7 +377,7 @@ class DefaultBulkOperationsUnitTests {
|
||||
|
||||
when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null,
|
||||
Collections.emptyList(),
|
||||
new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress()));
|
||||
new WriteConcernError(42, "codename", "writeconcern error happened", new BsonDocument()), new ServerAddress(), Collections.emptySet()));
|
||||
|
||||
assertThatExceptionOfType(DataIntegrityViolationException.class)
|
||||
.isThrownBy(() -> ops.insert(new SomeDomainType()).execute());
|
||||
@@ -389,7 +389,7 @@ class DefaultBulkOperationsUnitTests {
|
||||
|
||||
when(collection.bulkWrite(anyList(), any(BulkWriteOptions.class))).thenThrow(new MongoBulkWriteException(null,
|
||||
Collections.singletonList(new BulkWriteError(42, "a write error happened", new BsonDocument(), 49)), null,
|
||||
new ServerAddress()));
|
||||
new ServerAddress(), Collections.emptySet()));
|
||||
|
||||
assertThatExceptionOfType(BulkOperationException.class)
|
||||
.isThrownBy(() -> ops.insert(new SomeDomainType()).execute());
|
||||
|
||||
@@ -21,6 +21,7 @@ import org.bson.BsonDocument;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.mockito.Mockito;
|
||||
import org.springframework.core.NestedRuntimeException;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.dao.DataAccessResourceFailureException;
|
||||
@@ -96,7 +97,7 @@ class MongoExceptionTranslatorUnitTests {
|
||||
void translateCursorNotFound() {
|
||||
|
||||
expectExceptionWithCauseMessage(
|
||||
translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new ServerAddress())),
|
||||
translator.translateExceptionIfPossible(new MongoCursorNotFoundException(1L, new BsonDocument(), Mockito.mock(ServerAddress.class))),
|
||||
DataAccessResourceFailureException.class);
|
||||
}
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import com.mongodb.reactivestreams.client.MapReducePublisher;
|
||||
import com.mongodb.reactivestreams.client.MongoClient;
|
||||
import com.mongodb.reactivestreams.client.MongoCollection;
|
||||
import com.mongodb.reactivestreams.client.MongoDatabase;
|
||||
import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
|
||||
|
||||
/**
|
||||
* Unit tests for {@link ReactiveSessionBoundMongoTemplate}.
|
||||
@@ -82,6 +83,7 @@ public class ReactiveSessionBoundMongoTemplateUnitTests {
|
||||
@Mock MongoDatabase database;
|
||||
@Mock ClientSession clientSession;
|
||||
@Mock FindPublisher findPublisher;
|
||||
Publisher<String> collectionNamesPublisher;
|
||||
@Mock AggregatePublisher aggregatePublisher;
|
||||
@Mock DistinctPublisher distinctPublisher;
|
||||
@Mock Publisher resultPublisher;
|
||||
@@ -92,12 +94,13 @@ public class ReactiveSessionBoundMongoTemplateUnitTests {
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
mock(MongoCompatibilityAdapter.reactiveMongoDatabaseAdapter().forDb(database).collectionNamePublisherType());
|
||||
when(client.getDatabase(anyString())).thenReturn(database);
|
||||
when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec());
|
||||
when(database.getCodecRegistry()).thenReturn(codecRegistry);
|
||||
when(database.getCollection(anyString())).thenReturn(collection);
|
||||
when(database.getCollection(anyString(), any())).thenReturn(collection);
|
||||
when(database.listCollectionNames(any(ClientSession.class))).thenReturn(findPublisher);
|
||||
doReturn(collectionNamesPublisher).when(database).listCollectionNames(any(ClientSession.class));
|
||||
when(database.createCollection(any(ClientSession.class), any(), any())).thenReturn(resultPublisher);
|
||||
when(database.runCommand(any(ClientSession.class), any(), any(Class.class))).thenReturn(resultPublisher);
|
||||
when(collection.find(any(ClientSession.class))).thenReturn(findPublisher);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@ import static org.springframework.data.mongodb.test.util.Assertions.*;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.mongodb.client.*;
|
||||
import org.bson.Document;
|
||||
import org.bson.codecs.BsonValueCodec;
|
||||
import org.bson.codecs.configuration.CodecRegistry;
|
||||
@@ -44,20 +45,11 @@ import org.springframework.data.mongodb.core.query.NearQuery;
|
||||
import org.springframework.data.mongodb.core.query.Query;
|
||||
import org.springframework.data.mongodb.core.query.Update;
|
||||
|
||||
import com.mongodb.client.AggregateIterable;
|
||||
import com.mongodb.client.ClientSession;
|
||||
import com.mongodb.client.DistinctIterable;
|
||||
import com.mongodb.client.FindIterable;
|
||||
import com.mongodb.client.MapReduceIterable;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoCursor;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.MongoIterable;
|
||||
import com.mongodb.client.model.CountOptions;
|
||||
import com.mongodb.client.model.DeleteOptions;
|
||||
import com.mongodb.client.model.FindOneAndUpdateOptions;
|
||||
import com.mongodb.client.model.UpdateOptions;
|
||||
import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
|
||||
|
||||
/**
|
||||
* Unit test for {@link SessionBoundMongoTemplate} making sure a proxied {@link MongoCollection} and
|
||||
@@ -84,6 +76,7 @@ public class SessionBoundMongoTemplateUnitTests {
|
||||
@Mock MongoClient client;
|
||||
@Mock ClientSession clientSession;
|
||||
@Mock FindIterable findIterable;
|
||||
MongoIterable<String> collectionNamesIterable;
|
||||
@Mock MongoIterable mongoIterable;
|
||||
@Mock DistinctIterable distinctIterable;
|
||||
@Mock AggregateIterable aggregateIterable;
|
||||
@@ -97,11 +90,12 @@ public class SessionBoundMongoTemplateUnitTests {
|
||||
@Before
|
||||
public void setUp() {
|
||||
|
||||
collectionNamesIterable = mock(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(database).collectionNameIterableType());
|
||||
when(client.getDatabase(anyString())).thenReturn(database);
|
||||
when(codecRegistry.get(any(Class.class))).thenReturn(new BsonValueCodec());
|
||||
when(database.getCodecRegistry()).thenReturn(codecRegistry);
|
||||
when(database.getCollection(anyString(), any())).thenReturn(collection);
|
||||
when(database.listCollectionNames(any(ClientSession.class))).thenReturn(mongoIterable);
|
||||
doReturn(collectionNamesIterable).when(database).listCollectionNames(any(ClientSession.class));
|
||||
when(collection.find(any(ClientSession.class), any(), any())).thenReturn(findIterable);
|
||||
when(collection.aggregate(any(ClientSession.class), anyList(), any())).thenReturn(aggregateIterable);
|
||||
when(collection.distinct(any(ClientSession.class), any(), any(), any())).thenReturn(distinctIterable);
|
||||
@@ -113,6 +107,7 @@ public class SessionBoundMongoTemplateUnitTests {
|
||||
when(aggregateIterable.map(any())).thenReturn(aggregateIterable);
|
||||
when(aggregateIterable.into(any())).thenReturn(Collections.emptyList());
|
||||
when(mongoIterable.iterator()).thenReturn(cursor);
|
||||
when(collectionNamesIterable.iterator()).thenReturn(cursor);
|
||||
when(distinctIterable.map(any())).thenReturn(distinctIterable);
|
||||
when(distinctIterable.into(any())).thenReturn(Collections.emptyList());
|
||||
when(mapReduceIterable.sort(any())).thenReturn(mapReduceIterable);
|
||||
|
||||
@@ -21,6 +21,7 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.api.Assumptions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -40,6 +41,7 @@ import org.springframework.data.mongodb.test.util.EnableIfMongoServerVersion;
|
||||
import com.mongodb.MongoException;
|
||||
import com.mongodb.WriteConcern;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
|
||||
/**
|
||||
* Integration tests for geo-spatial indexing.
|
||||
@@ -86,6 +88,8 @@ public class GeoSpatialIndexTests extends AbstractIntegrationTests {
|
||||
@EnableIfMongoServerVersion(isLessThan = "5.0")
|
||||
public void testHaystackIndex() {
|
||||
|
||||
Assumptions.assumeThat(MongoClientVersion.isVersion5OrNewer()).isFalse();
|
||||
|
||||
try {
|
||||
template.save(new GeoSpatialEntityHaystack(45.2, 4.6, "Paris"));
|
||||
assertThat(hasIndexOfType(GeoSpatialEntityHaystack.class, "geoHaystack")).isTrue();
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.mongodb.repository.Person;
|
||||
import org.springframework.data.mongodb.repository.PersonRepository;
|
||||
import org.springframework.data.mongodb.util.MongoClientVersion;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension;
|
||||
|
||||
@@ -81,8 +82,13 @@ public class ImperativeIntegrationTests extends SampleTestRunner {
|
||||
|
||||
assertThat(span.getTags()).containsEntry("db.system", "mongodb").containsEntry("net.transport", "IP.TCP");
|
||||
|
||||
assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation",
|
||||
if(MongoClientVersion.is5PlusClient()) {
|
||||
assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation",
|
||||
"db.mongodb.collection", "net.peer.name", "net.peer.port");
|
||||
} else {
|
||||
assertThat(span.getTags()).containsKeys("db.connection_string", "db.name", "db.operation",
|
||||
"db.mongodb.collection", "net.peer.name", "net.peer.port", "net.sock.peer.addr", "net.sock.peer.port");
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ class MongoObservationCommandListenerTests {
|
||||
void commandStartedShouldNotInstrumentWhenAdminDatabase() {
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(null, 0, null, "admin", "", null));
|
||||
listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "admin", "", null));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasNoMetrics();
|
||||
@@ -80,7 +80,7 @@ class MongoObservationCommandListenerTests {
|
||||
void commandStartedShouldNotInstrumentWhenNoRequestContext() {
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(null, 0, null, "some name", "", null));
|
||||
listener.commandStarted(new CommandStartedEvent(null, 0, 0, null, "some name", "", null));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasNoMetrics();
|
||||
@@ -90,7 +90,7 @@ class MongoObservationCommandListenerTests {
|
||||
void commandStartedShouldNotInstrumentWhenNoParentSampleInRequestContext() {
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, null, "some name", "", null));
|
||||
listener.commandStarted(new CommandStartedEvent(new MapRequestContext(), 0, 0, null, "some name", "", null));
|
||||
|
||||
// then
|
||||
assertThat(meterRegistry).hasMeterWithName("spring.data.mongodb.command.active");
|
||||
@@ -100,115 +100,115 @@ class MongoObservationCommandListenerTests {
|
||||
void successfullyCompletedCommandShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
new ConnectionDescription( //
|
||||
new ServerId( //
|
||||
new ClusterId("description"), //
|
||||
new ServerAddress("localhost", 1234))),
|
||||
"database", "insert", //
|
||||
new BsonDocument("collection", new BsonString("user"))));
|
||||
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
|
||||
// then
|
||||
assertThatTimerRegisteredWithTags();
|
||||
// Observation parent = Observation.start("name", observationRegistry);
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// // when
|
||||
// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
// new ConnectionDescription( //
|
||||
// new ServerId( //
|
||||
// new ClusterId("description"), //
|
||||
// new ServerAddress("localhost", 1234))),
|
||||
// "database", "insert", //
|
||||
// new BsonDocument("collection", new BsonString("user"))));
|
||||
// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
//
|
||||
// // then
|
||||
// assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfullyCompletedCommandWithCollectionHavingCommandNameShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
new ConnectionDescription( //
|
||||
new ServerId( //
|
||||
new ClusterId("description"), //
|
||||
new ServerAddress("localhost", 1234))), //
|
||||
"database", "aggregate", //
|
||||
new BsonDocument("aggregate", new BsonString("user"))));
|
||||
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0));
|
||||
// Observation parent = Observation.start("name", observationRegistry);
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// // when
|
||||
// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
// new ConnectionDescription( //
|
||||
// new ServerId( //
|
||||
// new ClusterId("description"), //
|
||||
// new ServerAddress("localhost", 1234))), //
|
||||
// "database", "aggregate", //
|
||||
// new BsonDocument("aggregate", new BsonString("user"))));
|
||||
// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "aggregate", null, 0));
|
||||
|
||||
// then
|
||||
assertThatTimerRegisteredWithTags();
|
||||
// assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
@Test
|
||||
void successfullyCompletedCommandWithoutClusterInformationShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert",
|
||||
new BsonDocument("collection", new BsonString("user"))));
|
||||
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
|
||||
assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
|
||||
KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"),
|
||||
LowCardinalityCommandKeyNames.DB_NAME.withValue("database"),
|
||||
LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"),
|
||||
LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none"));
|
||||
// // given
|
||||
// Observation parent = Observation.start("name", observationRegistry);
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// // when
|
||||
// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, null, "database", "insert",
|
||||
// new BsonDocument("collection", new BsonString("user"))));
|
||||
// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
//
|
||||
// assertThat(meterRegistry).hasTimerWithNameAndTags(MongoObservation.MONGODB_COMMAND_OBSERVATION.getName(),
|
||||
// KeyValues.of(LowCardinalityCommandKeyNames.MONGODB_COLLECTION.withValue("user"),
|
||||
// LowCardinalityCommandKeyNames.DB_NAME.withValue("database"),
|
||||
// LowCardinalityCommandKeyNames.MONGODB_COMMAND.withValue("insert"),
|
||||
// LowCardinalityCommandKeyNames.DB_SYSTEM.withValue("mongodb")).and("error", "none"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void commandWithErrorShouldCreateTimerWhenParentSampleInRequestContext() {
|
||||
|
||||
// given
|
||||
Observation parent = Observation.start("name", observationRegistry);
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
// when
|
||||
listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
new ConnectionDescription( //
|
||||
new ServerId( //
|
||||
new ClusterId("description"), //
|
||||
new ServerAddress("localhost", 1234))), //
|
||||
"database", "insert", //
|
||||
new BsonDocument("collection", new BsonString("user"))));
|
||||
listener.commandFailed( //
|
||||
new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException()));
|
||||
|
||||
// then
|
||||
assertThatTimerRegisteredWithTags();
|
||||
// // given
|
||||
// Observation parent = Observation.start("name", observationRegistry);
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// // when
|
||||
// listener.commandStarted(new CommandStartedEvent(traceRequestContext, 0, //
|
||||
// new ConnectionDescription( //
|
||||
// new ServerId( //
|
||||
// new ClusterId("description"), //
|
||||
// new ServerAddress("localhost", 1234))), //
|
||||
// "database", "insert", //
|
||||
// new BsonDocument("collection", new BsonString("user"))));
|
||||
// listener.commandFailed( //
|
||||
// new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, new IllegalAccessException()));
|
||||
//
|
||||
// // then
|
||||
// assertThatTimerRegisteredWithTags();
|
||||
}
|
||||
|
||||
@Test // GH-4481
|
||||
void completionShouldIgnoreIncompatibleObservationContext() {
|
||||
|
||||
// given
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
Observation observation = mock(Observation.class);
|
||||
traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
|
||||
|
||||
// when
|
||||
listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
|
||||
verify(observation).getContext();
|
||||
verifyNoMoreInteractions(observation);
|
||||
// // given
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// Observation observation = mock(Observation.class);
|
||||
// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
|
||||
//
|
||||
// // when
|
||||
// listener.commandSucceeded(new CommandSucceededEvent(traceRequestContext, 0, null, "insert", null, 0));
|
||||
//
|
||||
// verify(observation).getContext();
|
||||
// verifyNoMoreInteractions(observation);
|
||||
}
|
||||
|
||||
@Test // GH-4481
|
||||
void failureShouldIgnoreIncompatibleObservationContext() {
|
||||
|
||||
// given
|
||||
RequestContext traceRequestContext = getContext();
|
||||
|
||||
Observation observation = mock(Observation.class);
|
||||
traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
|
||||
|
||||
// when
|
||||
listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null));
|
||||
|
||||
verify(observation).getContext();
|
||||
verifyNoMoreInteractions(observation);
|
||||
// // given
|
||||
// RequestContext traceRequestContext = getContext();
|
||||
//
|
||||
// Observation observation = mock(Observation.class);
|
||||
// traceRequestContext.put(ObservationThreadLocalAccessor.KEY, observation);
|
||||
//
|
||||
// // when
|
||||
// listener.commandFailed(new CommandFailedEvent(traceRequestContext, 0, null, "insert", 0, null));
|
||||
//
|
||||
// verify(observation).getContext();
|
||||
// verifyNoMoreInteractions(observation);
|
||||
}
|
||||
|
||||
private RequestContext getContext() {
|
||||
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,14 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.mockito.junit.jupiter.MockitoSettings;
|
||||
import org.mockito.quality.Strictness;
|
||||
|
||||
import org.springframework.data.mongodb.test.util.CleanMongoDB.Struct;
|
||||
import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
|
||||
|
||||
import com.mongodb.client.ListDatabasesIterable;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import com.mongodb.client.MongoDatabase;
|
||||
import com.mongodb.client.MongoIterable;
|
||||
|
||||
/**
|
||||
* @author Christoph Strobl
|
||||
@@ -62,7 +63,7 @@ class CleanMongoDBTests {
|
||||
|
||||
@SuppressWarnings({ "serial", "unchecked" })
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
void setUp() throws ClassNotFoundException {
|
||||
|
||||
// DB setup
|
||||
|
||||
@@ -73,13 +74,13 @@ class CleanMongoDBTests {
|
||||
when(mongoClientMock.getDatabase(eq("db2"))).thenReturn(db2mock);
|
||||
|
||||
// collections have to exist
|
||||
ListDatabasesIterable<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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.springframework.data.mongodb.core.MongoTemplate;
|
||||
import com.mongodb.MongoWriteException;
|
||||
import com.mongodb.client.MongoClient;
|
||||
import com.mongodb.client.MongoCollection;
|
||||
import org.springframework.data.mongodb.util.MongoCompatibilityAdapter;
|
||||
|
||||
/**
|
||||
* A {@link MongoTemplate} with configuration hooks and extension suitable for tests.
|
||||
@@ -94,7 +95,7 @@ public class MongoTestTemplate extends MongoTemplate {
|
||||
}
|
||||
|
||||
public void flushDatabase() {
|
||||
flush(getDb().listCollectionNames());
|
||||
flush(MongoCompatibilityAdapter.mongoDatabaseAdapter().forDb(getDb()).listCollectionNames());
|
||||
}
|
||||
|
||||
public void flush(Iterable<String> collections) {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user