Browse Source
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: #4578pull/4633/head
33 changed files with 1557 additions and 128 deletions
@ -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); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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); |
||||||
|
} |
||||||
|
|
||||||
|
} |
||||||
@ -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()); |
||||||
|
} |
||||||
|
} |
||||||
@ -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 { |
||||||
|
|
||||||
|
} |
||||||
@ -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)); |
||||||
|
|
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue