mappingFunction) {
+ return mappingFunction.apply(this);
+ }
+
+ /**
+ * @return MongoDB driver native {@link TransactionOptions}.
+ * @see MongoTransactionOptions#map(Function)
+ */
+ @Nullable
+ default TransactionOptions toDriverOptions() {
+
+ return map(it -> {
+
+ if (MongoTransactionOptions.NONE.equals(it)) {
+ return null;
+ }
+
+ TransactionOptions.Builder builder = TransactionOptions.builder();
+ if (it.hasMaxCommitTime()) {
+ builder.maxCommitTime(it.getMaxCommitTime().toMillis(), TimeUnit.MILLISECONDS);
+ }
+ if (it.hasReadConcern()) {
+ builder.readConcern(it.getReadConcern());
+ }
+ if (it.hasReadPreference()) {
+ builder.readPreference(it.getReadPreference());
+ }
+ if (it.hasWriteConcern()) {
+ builder.writeConcern(it.getWriteConcern());
+ }
+ return builder.build();
+ });
+ }
+
+ /**
+ * Factory method to wrap given MongoDB driver native {@link TransactionOptions} into {@link MongoTransactionOptions}.
+ *
+ * @param options
+ * @return {@link MongoTransactionOptions#NONE} if given object is {@literal null}.
+ */
+ static MongoTransactionOptions of(@Nullable TransactionOptions options) {
+
+ if (options == null) {
+ return NONE;
+ }
+
+ return new MongoTransactionOptions() {
+
+ @Nullable
+ @Override
+ public Duration getMaxCommitTime() {
+
+ Long millis = options.getMaxCommitTime(TimeUnit.MILLISECONDS);
+ return millis != null ? Duration.ofMillis(millis) : null;
+ }
+
+ @Nullable
+ @Override
+ public ReadConcern getReadConcern() {
+ return options.getReadConcern();
+ }
+
+ @Nullable
+ @Override
+ public ReadPreference getReadPreference() {
+ return options.getReadPreference();
+ }
+
+ @Nullable
+ @Override
+ public WriteConcern getWriteConcern() {
+ return options.getWriteConcern();
+ }
+
+ @Nullable
+ @Override
+ public TransactionOptions toDriverOptions() {
+ return options;
+ }
+ };
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
new file mode 100644
index 0000000000..b73b079a99
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoTransactionOptionsResolver.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2024-2025 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.util.Map;
+import java.util.stream.Collectors;
+
+import org.springframework.lang.Nullable;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.interceptor.TransactionAttribute;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+/**
+ * A {@link TransactionOptionResolver} reading MongoDB specific {@link MongoTransactionOptions transaction options} from
+ * a {@link TransactionDefinition}. Implementations of {@link MongoTransactionOptions} may choose a specific
+ * {@link #getLabelPrefix() prefix} for {@link TransactionAttribute#getLabels() transaction attribute labels} to avoid
+ * evaluating non-store specific ones.
+ *
+ * {@link TransactionAttribute#getLabels()} evaluated by default should follow the property style using {@code =} to
+ * separate key and value pairs.
+ *
+ * By default {@link #resolve(TransactionDefinition)} will filter labels by the {@link #getLabelPrefix() prefix} and
+ * strip the prefix from the label before handing the pruned {@link Map} to the {@link #convert(Map)} function.
+ *
+ * A transaction definition with labels targeting MongoDB may look like the following:
+ *
+ *
+ * @Transactional(label = { "mongo:readConcern=majority" })
+ *
+ *
+ * @author Christoph Strobl
+ * @since 4.3
+ */
+public interface MongoTransactionOptionsResolver extends TransactionOptionResolver {
+
+ /**
+ * Obtain the default {@link MongoTransactionOptionsResolver} implementation using a {@literal mongo:}
+ * {@link #getLabelPrefix() prefix}.
+ *
+ * @return instance of default {@link MongoTransactionOptionsResolver} implementation.
+ */
+ static MongoTransactionOptionsResolver defaultResolver() {
+ return DefaultMongoTransactionOptionsResolver.INSTANCE;
+ }
+
+ /**
+ * Get the prefix used to filter applicable {@link TransactionAttribute#getLabels() labels}.
+ *
+ * @return {@literal null} if no label defined.
+ */
+ @Nullable
+ String getLabelPrefix();
+
+ /**
+ * Resolve {@link MongoTransactionOptions} from a given {@link TransactionDefinition} by evaluating
+ * {@link TransactionAttribute#getLabels()} labels if possible.
+ *
+ * Splits applicable labels property style using {@literal =} as deliminator and removes a potential
+ * {@link #getLabelPrefix() prefix} before calling {@link #convert(Map)} with filtered label values.
+ *
+ * @param definition
+ * @return {@link MongoTransactionOptions#NONE} in case the given {@link TransactionDefinition} is not a
+ * {@link TransactionAttribute} if no matching {@link TransactionAttribute#getLabels() labels} could be found.
+ * @throws IllegalArgumentException for options that do not map to valid transactions options or malformatted labels.
+ */
+ @Override
+ default MongoTransactionOptions resolve(TransactionDefinition definition) {
+
+ if (!(definition instanceof TransactionAttribute attribute)) {
+ return MongoTransactionOptions.NONE;
+ }
+
+ if (attribute.getLabels().isEmpty()) {
+ return MongoTransactionOptions.NONE;
+ }
+
+ Map attributeMap = attribute.getLabels().stream()
+ .filter(it -> !StringUtils.hasText(getLabelPrefix()) || it.startsWith(getLabelPrefix()))
+ .map(it -> StringUtils.hasText(getLabelPrefix()) ? it.substring(getLabelPrefix().length()) : it).map(it -> {
+
+ String[] kvPair = StringUtils.split(it, "=");
+ Assert.isTrue(kvPair != null && kvPair.length == 2,
+ () -> "No value present for transaction option %s".formatted(kvPair != null ? kvPair[0] : it));
+ return kvPair;
+ })
+
+ .collect(Collectors.toMap(it -> it[0].trim(), it -> it[1].trim()));
+
+ return attributeMap.isEmpty() ? MongoTransactionOptions.NONE : convert(attributeMap);
+ }
+
+ /**
+ * Convert the given {@link Map} into an instance of {@link MongoTransactionOptions}.
+ *
+ * @param options never {@literal null}.
+ * @return never {@literal null}.
+ * @throws IllegalArgumentException for invalid options.
+ */
+ MongoTransactionOptions convert(Map options);
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java
new file mode 100644
index 0000000000..f2a6714a95
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseFactory.java
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2016-2025 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 reactor.core.publisher.Mono;
+
+import org.bson.codecs.configuration.CodecRegistry;
+import org.springframework.dao.DataAccessException;
+import org.springframework.dao.support.PersistenceExceptionTranslator;
+import org.springframework.data.mongodb.core.MongoExceptionTranslator;
+
+import com.mongodb.ClientSessionOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+
+/**
+ * Interface for factories creating reactive {@link MongoDatabase} instances.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @author Mathieu Ouellet
+ * @since 2.0
+ */
+public interface ReactiveMongoDatabaseFactory extends CodecRegistryProvider {
+
+ /**
+ * Creates a default {@link MongoDatabase} instance.
+ *
+ * @return never {@literal null}.
+ * @throws DataAccessException
+ */
+ Mono getMongoDatabase() throws DataAccessException;
+
+ /**
+ * Obtain a {@link MongoDatabase} instance to access the database with the given name.
+ *
+ * @param dbName must not be {@literal null} or empty.
+ * @return never {@literal null}.
+ * @throws DataAccessException
+ */
+ Mono getMongoDatabase(String dbName) throws DataAccessException;
+
+ /**
+ * Exposes a shared {@link MongoExceptionTranslator}.
+ *
+ * @return will never be {@literal null}.
+ */
+ PersistenceExceptionTranslator getExceptionTranslator();
+
+ /**
+ * Get the underlying {@link CodecRegistry} used by the reactive MongoDB Java driver.
+ *
+ * @return never {@literal null}.
+ */
+ CodecRegistry getCodecRegistry();
+
+ /**
+ * Obtain a {@link Mono} emitting a {@link ClientSession} for given {@link ClientSessionOptions options}.
+ *
+ * @param options must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 2.1
+ */
+ Mono getSession(ClientSessionOptions options);
+
+ /**
+ * Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoDatabaseFactory} returning
+ * {@link MongoDatabase} instances that are aware and bound to the given session.
+ *
+ * @param session must not be {@literal null}.
+ * @return never {@literal null}.
+ * @since 2.1
+ */
+ ReactiveMongoDatabaseFactory withSession(ClientSession session);
+
+ /**
+ * Returns if the given {@link ReactiveMongoDatabaseFactory} is bound to a
+ * {@link com.mongodb.reactivestreams.client.ClientSession} that has an
+ * {@link com.mongodb.reactivestreams.client.ClientSession#hasActiveTransaction() active transaction}.
+ *
+ * @return {@literal true} if there's an active transaction, {@literal false} otherwise.
+ * @since 2.2
+ */
+ default boolean isTransactionActive() {
+ return false;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
new file mode 100644
index 0000000000..f397818a4c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoDatabaseUtils.java
@@ -0,0 +1,266 @@
+/*
+ * Copyright 2019-2025 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 reactor.core.publisher.Mono;
+import reactor.util.context.Context;
+
+import org.springframework.lang.Nullable;
+import org.springframework.transaction.NoTransactionException;
+import org.springframework.transaction.reactive.ReactiveResourceSynchronization;
+import org.springframework.transaction.reactive.TransactionSynchronization;
+import org.springframework.transaction.reactive.TransactionSynchronizationManager;
+import org.springframework.transaction.support.ResourceHolderSynchronization;
+import org.springframework.util.Assert;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.ClientSessionOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+import com.mongodb.reactivestreams.client.MongoCollection;
+import com.mongodb.reactivestreams.client.MongoDatabase;
+
+/**
+ * Helper class for managing reactive {@link MongoDatabase} instances via {@link ReactiveMongoDatabaseFactory}. Used for
+ * obtaining {@link ClientSession session bound} resources, such as {@link MongoDatabase} and {@link MongoCollection}
+ * suitable for transactional usage.
+ *
+ * Note: Intended for internal usage only.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @author Mathieu Ouellet
+ * @since 2.2
+ */
+public class ReactiveMongoDatabaseUtils {
+
+ /**
+ * Check if the {@link ReactiveMongoDatabaseFactory} is actually bound to a
+ * {@link com.mongodb.reactivestreams.client.ClientSession} that has an active transaction, or if a
+ * {@link org.springframework.transaction.reactive.TransactionSynchronization} has been registered for the
+ * {@link ReactiveMongoDatabaseFactory resource} and if the associated
+ * {@link com.mongodb.reactivestreams.client.ClientSession} has an
+ * {@link com.mongodb.reactivestreams.client.ClientSession#hasActiveTransaction() active transaction}.
+ *
+ * @param databaseFactory the resource to check transactions for. Must not be {@literal null}.
+ * @return a {@link Mono} emitting {@literal true} if the factory has an ongoing transaction.
+ */
+ public static Mono isTransactionActive(ReactiveMongoDatabaseFactory databaseFactory) {
+
+ if (databaseFactory.isTransactionActive()) {
+ return Mono.just(true);
+ }
+
+ return TransactionSynchronizationManager.forCurrentTransaction() //
+ .map(it -> {
+
+ ReactiveMongoResourceHolder holder = (ReactiveMongoResourceHolder) it.getResource(databaseFactory);
+ return holder != null && holder.hasActiveTransaction();
+ }) //
+ .onErrorResume(NoTransactionException.class, e -> Mono.just(false));
+ }
+
+ /**
+ * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory} using
+ * {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
+ * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
+ *
+ * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
+ * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
+ */
+ public static Mono getDatabase(ReactiveMongoDatabaseFactory factory) {
+ return doGetMongoDatabase(null, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
+ }
+
+ /**
+ * Obtain the default {@link MongoDatabase database} form the given {@link ReactiveMongoDatabaseFactory factory}.
+ *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
+ * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
+ *
+ * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
+ * @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
+ * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
+ */
+ public static Mono getDatabase(ReactiveMongoDatabaseFactory factory,
+ SessionSynchronization sessionSynchronization) {
+ return doGetMongoDatabase(null, factory, sessionSynchronization);
+ }
+
+ /**
+ * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
+ * factory} using {@link SessionSynchronization#ON_ACTUAL_TRANSACTION native session synchronization}.
+ *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
+ * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
+ *
+ * @param dbName the name of the {@link MongoDatabase} to get.
+ * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
+ * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
+ */
+ public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory) {
+ return doGetMongoDatabase(dbName, factory, SessionSynchronization.ON_ACTUAL_TRANSACTION);
+ }
+
+ /**
+ * Obtain the {@link MongoDatabase database} with given name form the given {@link ReactiveMongoDatabaseFactory
+ * factory}.
+ *
+ * Registers a {@link MongoSessionSynchronization MongoDB specific transaction synchronization} within the subscriber
+ * {@link Context} if {@link TransactionSynchronizationManager#isSynchronizationActive() synchronization is active}.
+ *
+ * @param dbName the name of the {@link MongoDatabase} to get.
+ * @param factory the {@link ReactiveMongoDatabaseFactory} to get the {@link MongoDatabase} from.
+ * @param sessionSynchronization the synchronization to use. Must not be {@literal null}.
+ * @return the {@link MongoDatabase} that is potentially associated with a transactional {@link ClientSession}.
+ */
+ public static Mono getDatabase(String dbName, ReactiveMongoDatabaseFactory factory,
+ SessionSynchronization sessionSynchronization) {
+ return doGetMongoDatabase(dbName, factory, sessionSynchronization);
+ }
+
+ private static Mono doGetMongoDatabase(@Nullable String dbName, ReactiveMongoDatabaseFactory factory,
+ SessionSynchronization sessionSynchronization) {
+
+ Assert.notNull(factory, "DatabaseFactory must not be null");
+
+ if (sessionSynchronization == SessionSynchronization.NEVER) {
+ return getMongoDatabaseOrDefault(dbName, factory);
+ }
+
+ return TransactionSynchronizationManager.forCurrentTransaction()
+ .filter(TransactionSynchronizationManager::isSynchronizationActive) //
+ .flatMap(synchronizationManager -> {
+
+ return doGetSession(synchronizationManager, factory, sessionSynchronization) //
+ .flatMap(it -> getMongoDatabaseOrDefault(dbName, factory.withSession(it)));
+ }) //
+ .onErrorResume(NoTransactionException.class, e -> getMongoDatabaseOrDefault(dbName, factory))
+ .switchIfEmpty(getMongoDatabaseOrDefault(dbName, factory));
+ }
+
+ private static Mono getMongoDatabaseOrDefault(@Nullable String dbName,
+ ReactiveMongoDatabaseFactory factory) {
+ return StringUtils.hasText(dbName) ? factory.getMongoDatabase(dbName) : factory.getMongoDatabase();
+ }
+
+ private static Mono doGetSession(TransactionSynchronizationManager synchronizationManager,
+ ReactiveMongoDatabaseFactory dbFactory, SessionSynchronization sessionSynchronization) {
+
+ final ReactiveMongoResourceHolder registeredHolder = (ReactiveMongoResourceHolder) synchronizationManager
+ .getResource(dbFactory);
+
+ // check for native MongoDB transaction
+ if (registeredHolder != null
+ && (registeredHolder.hasSession() || registeredHolder.isSynchronizedWithTransaction())) {
+
+ return registeredHolder.hasSession() ? Mono.just(registeredHolder.getSession())
+ : createClientSession(dbFactory).map(registeredHolder::setSessionIfAbsent);
+ }
+
+ if (SessionSynchronization.ON_ACTUAL_TRANSACTION.equals(sessionSynchronization)) {
+ return Mono.empty();
+ }
+
+ // init a non native MongoDB transaction by registering a MongoSessionSynchronization
+ return createClientSession(dbFactory).map(session -> {
+
+ ReactiveMongoResourceHolder newHolder = new ReactiveMongoResourceHolder(session, dbFactory);
+ newHolder.getRequiredSession().startTransaction();
+
+ synchronizationManager
+ .registerSynchronization(new MongoSessionSynchronization(synchronizationManager, newHolder, dbFactory));
+ newHolder.setSynchronizedWithTransaction(true);
+ synchronizationManager.bindResource(dbFactory, newHolder);
+
+ return newHolder.getSession();
+ });
+ }
+
+ private static Mono createClientSession(ReactiveMongoDatabaseFactory dbFactory) {
+ return dbFactory.getSession(ClientSessionOptions.builder().causallyConsistent(true).build());
+ }
+
+ /**
+ * MongoDB specific {@link ResourceHolderSynchronization} for resource cleanup at the end of a transaction when
+ * participating in a non-native MongoDB transaction, such as a R2CBC transaction.
+ *
+ * @author Mark Paluch
+ * @since 2.2
+ */
+ private static class MongoSessionSynchronization
+ extends ReactiveResourceSynchronization {
+
+ private final ReactiveMongoResourceHolder resourceHolder;
+
+ MongoSessionSynchronization(TransactionSynchronizationManager synchronizationManager,
+ ReactiveMongoResourceHolder resourceHolder, ReactiveMongoDatabaseFactory dbFactory) {
+
+ super(resourceHolder, dbFactory, synchronizationManager);
+ this.resourceHolder = resourceHolder;
+ }
+
+ @Override
+ protected boolean shouldReleaseBeforeCompletion() {
+ return false;
+ }
+
+ @Override
+ protected Mono processResourceAfterCommit(ReactiveMongoResourceHolder resourceHolder) {
+
+ if (isTransactionActive(resourceHolder)) {
+ return Mono.from(resourceHolder.getRequiredSession().commitTransaction());
+ }
+
+ return Mono.empty();
+ }
+
+ @Override
+ public Mono afterCompletion(int status) {
+
+ return Mono.defer(() -> {
+
+ if (status == TransactionSynchronization.STATUS_ROLLED_BACK && isTransactionActive(this.resourceHolder)) {
+
+ return Mono.from(resourceHolder.getRequiredSession().abortTransaction()) //
+ .then(super.afterCompletion(status));
+ }
+
+ return super.afterCompletion(status);
+ });
+ }
+
+ @Override
+ protected Mono releaseResource(ReactiveMongoResourceHolder resourceHolder, Object resourceKey) {
+
+ return Mono.fromRunnable(() -> {
+ if (resourceHolder.hasActiveSession()) {
+ resourceHolder.getRequiredSession().close();
+ }
+ });
+ }
+
+ private boolean isTransactionActive(ReactiveMongoResourceHolder resourceHolder) {
+
+ if (!resourceHolder.hasSession()) {
+ return false;
+ }
+
+ return resourceHolder.getRequiredSession().hasActiveTransaction();
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java
new file mode 100644
index 0000000000..33caa5e7fe
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoResourceHolder.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2019-2025 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 org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.lang.Nullable;
+import org.springframework.transaction.support.ResourceHolderSupport;
+
+import com.mongodb.reactivestreams.client.ClientSession;
+
+/**
+ * MongoDB specific resource holder, wrapping a {@link ClientSession}. {@link ReactiveMongoTransactionManager} binds
+ * instances of this class to the subscriber context.
+ *
+ * Note: Intended for internal usage only.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 2.2
+ * @see ReactiveMongoTransactionManager
+ * @see ReactiveMongoTemplate
+ */
+class ReactiveMongoResourceHolder extends ResourceHolderSupport {
+
+ private @Nullable ClientSession session;
+ private ReactiveMongoDatabaseFactory databaseFactory;
+
+ /**
+ * Create a new {@link ReactiveMongoResourceHolder} for a given {@link ClientSession session}.
+ *
+ * @param session the associated {@link ClientSession}. Can be {@literal null}.
+ * @param databaseFactory the associated {@link MongoDatabaseFactory}. must not be {@literal null}.
+ */
+ ReactiveMongoResourceHolder(@Nullable ClientSession session, ReactiveMongoDatabaseFactory databaseFactory) {
+
+ this.session = session;
+ this.databaseFactory = databaseFactory;
+ }
+
+ /**
+ * @return the associated {@link ClientSession}. Can be {@literal null}.
+ */
+ @Nullable
+ ClientSession getSession() {
+ return session;
+ }
+
+ /**
+ * @return the required associated {@link ClientSession}.
+ * @throws IllegalStateException if no session is associated.
+ */
+ ClientSession getRequiredSession() {
+
+ ClientSession session = getSession();
+
+ if (session == null) {
+ throw new IllegalStateException("No ClientSession associated");
+ }
+ return session;
+ }
+
+ /**
+ * @return the associated {@link ReactiveMongoDatabaseFactory}.
+ */
+ public ReactiveMongoDatabaseFactory getDatabaseFactory() {
+ return databaseFactory;
+ }
+
+ /**
+ * Set the {@link ClientSession} to guard.
+ *
+ * @param session can be {@literal null}.
+ */
+ public void setSession(@Nullable ClientSession session) {
+ this.session = session;
+ }
+
+ /**
+ * @return {@literal true} if session is not {@literal null}.
+ */
+ boolean hasSession() {
+ return session != null;
+ }
+
+ /**
+ * If the {@link ReactiveMongoResourceHolder} is {@link #hasSession() not already associated} with a
+ * {@link ClientSession} the given value is {@link #setSession(ClientSession) set} and returned, otherwise the current
+ * bound session is returned.
+ *
+ * @param session
+ * @return
+ */
+ @Nullable
+ public ClientSession setSessionIfAbsent(@Nullable ClientSession session) {
+
+ if (!hasSession()) {
+ setSession(session);
+ }
+
+ return session;
+ }
+
+ /**
+ * @return {@literal true} if the session is active and has not been closed.
+ */
+ boolean hasActiveSession() {
+
+ if (!hasSession()) {
+ return false;
+ }
+
+ return hasServerSession() && !getRequiredSession().getServerSession().isClosed();
+ }
+
+ /**
+ * @return {@literal true} if the session has an active transaction.
+ * @see #hasActiveSession()
+ */
+ boolean hasActiveTransaction() {
+
+ if (!hasActiveSession()) {
+ return false;
+ }
+
+ return getRequiredSession().hasActiveTransaction();
+ }
+
+ /**
+ * @return {@literal true} if the {@link ClientSession} has a {@link com.mongodb.session.ServerSession} associated
+ * that is accessible via {@link ClientSession#getServerSession()}.
+ */
+ boolean hasServerSession() {
+
+ try {
+ return getRequiredSession().getServerSession() != null;
+ } catch (IllegalStateException serverSessionClosed) {
+ // ignore
+ }
+
+ return false;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
new file mode 100644
index 0000000000..2c65c26b79
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/ReactiveMongoTransactionManager.java
@@ -0,0 +1,501 @@
+/*
+ * Copyright 2019-2025 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 reactor.core.publisher.Mono;
+
+import org.springframework.beans.factory.InitializingBean;
+import org.springframework.lang.Nullable;
+import org.springframework.transaction.TransactionDefinition;
+import org.springframework.transaction.TransactionException;
+import org.springframework.transaction.TransactionSystemException;
+import org.springframework.transaction.reactive.AbstractReactiveTransactionManager;
+import org.springframework.transaction.reactive.GenericReactiveTransaction;
+import org.springframework.transaction.reactive.TransactionSynchronizationManager;
+import org.springframework.transaction.support.SmartTransactionObject;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+import com.mongodb.ClientSessionOptions;
+import com.mongodb.MongoException;
+import com.mongodb.TransactionOptions;
+import com.mongodb.reactivestreams.client.ClientSession;
+
+/**
+ * A {@link org.springframework.transaction.ReactiveTransactionManager} implementation that manages
+ * {@link com.mongodb.reactivestreams.client.ClientSession} based transactions for a single
+ * {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory}.
+ * Binds a {@link ClientSession} from the specified
+ * {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory} to the subscriber
+ * {@link reactor.util.context.Context}.
+ * {@link org.springframework.transaction.TransactionDefinition#isReadOnly() Readonly} transactions operate on a
+ * {@link ClientSession} and enable causal consistency, and also {@link ClientSession#startTransaction() start},
+ * {@link com.mongodb.reactivestreams.client.ClientSession#commitTransaction() commit} or
+ * {@link ClientSession#abortTransaction() abort} a transaction.
+ * Application code is required to retrieve the {@link com.mongodb.reactivestreams.client.MongoDatabase} via
+ * {@link org.springframework.data.mongodb.ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory)} instead
+ * of a standard {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory#getMongoDatabase()} call. Spring
+ * classes such as {@link org.springframework.data.mongodb.core.ReactiveMongoTemplate} use this strategy implicitly.
+ *
+ * By default failure of a {@literal commit} operation raises a {@link TransactionSystemException}. You can override
+ * {@link #doCommit(TransactionSynchronizationManager, ReactiveMongoTransactionObject)} to implement the
+ * Retry Commit Operation
+ * behavior as outlined in the MongoDB reference manual.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 2.2
+ * @see MongoDB Transaction Documentation
+ * @see ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory, SessionSynchronization)
+ */
+public class ReactiveMongoTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean {
+
+ private @Nullable ReactiveMongoDatabaseFactory databaseFactory;
+ private @Nullable MongoTransactionOptions options;
+ private final MongoTransactionOptionsResolver transactionOptionsResolver;
+
+ /**
+ * Create a new {@link ReactiveMongoTransactionManager} for bean-style usage.
+ * Note: The {@link org.springframework.data.mongodb.ReactiveMongoDatabaseFactory db factory} has to
+ * be {@link #setDatabaseFactory(ReactiveMongoDatabaseFactory)} set} before using the instance. Use this constructor
+ * to prepare a {@link ReactiveMongoTransactionManager} via a {@link org.springframework.beans.factory.BeanFactory}.
+ *
+ * Optionally it is possible to set default {@link TransactionOptions transaction options} defining
+ * {@link com.mongodb.ReadConcern} and {@link com.mongodb.WriteConcern}.
+ *
+ * @see #setDatabaseFactory(ReactiveMongoDatabaseFactory)
+ */
+ public ReactiveMongoTransactionManager() {
+ this.transactionOptionsResolver = MongoTransactionOptionsResolver.defaultResolver();
+ }
+
+ /**
+ * Create a new {@link ReactiveMongoTransactionManager} obtaining sessions from the given
+ * {@link ReactiveMongoDatabaseFactory}.
+ *
+ * @param databaseFactory must not be {@literal null}.
+ */
+ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory) {
+ this(databaseFactory, null);
+ }
+
+ /**
+ * Create a new {@link ReactiveMongoTransactionManager} obtaining sessions from the given
+ * {@link ReactiveMongoDatabaseFactory} applying the given {@link TransactionOptions options}, if present, when
+ * starting a new transaction.
+ *
+ * @param databaseFactory must not be {@literal null}.
+ * @param options can be {@literal null}.
+ */
+ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory,
+ @Nullable TransactionOptions options) {
+ this(databaseFactory, MongoTransactionOptionsResolver.defaultResolver(), MongoTransactionOptions.of(options));
+ }
+
+ /**
+ * Create a new {@link ReactiveMongoTransactionManager} obtaining sessions from the given
+ * {@link ReactiveMongoDatabaseFactory} applying the given {@link TransactionOptions options}, if present, when
+ * starting a new transaction.
+ *
+ * @param databaseFactory must not be {@literal null}.
+ * @param transactionOptionsResolver must not be {@literal null}.
+ * @param defaultTransactionOptions can be {@literal null}.
+ * @since 4.3
+ */
+ public ReactiveMongoTransactionManager(ReactiveMongoDatabaseFactory databaseFactory,
+ MongoTransactionOptionsResolver transactionOptionsResolver,
+ @Nullable MongoTransactionOptions defaultTransactionOptions) {
+
+ Assert.notNull(databaseFactory, "DatabaseFactory must not be null");
+ Assert.notNull(transactionOptionsResolver, "MongoTransactionOptionsResolver must not be null");
+
+ this.databaseFactory = databaseFactory;
+ this.transactionOptionsResolver = transactionOptionsResolver;
+ this.options = defaultTransactionOptions;
+ }
+
+ @Override
+ protected Object doGetTransaction(TransactionSynchronizationManager synchronizationManager)
+ throws TransactionException {
+
+ ReactiveMongoResourceHolder resourceHolder = (ReactiveMongoResourceHolder) synchronizationManager
+ .getResource(getRequiredDatabaseFactory());
+ return new ReactiveMongoTransactionObject(resourceHolder);
+ }
+
+ @Override
+ protected boolean isExistingTransaction(Object transaction) throws TransactionException {
+ return extractMongoTransaction(transaction).hasResourceHolder();
+ }
+
+ @Override
+ protected Mono doBegin(TransactionSynchronizationManager synchronizationManager, Object transaction,
+ TransactionDefinition definition) throws TransactionException {
+
+ return Mono.defer(() -> {
+
+ ReactiveMongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
+
+ Mono holder = newResourceHolder(definition,
+ ClientSessionOptions.builder().causallyConsistent(true).build());
+
+ return holder.doOnNext(resourceHolder -> {
+
+ mongoTransactionObject.setResourceHolder(resourceHolder);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(
+ String.format("About to start transaction for session %s.", debugString(resourceHolder.getSession())));
+ }
+
+ }).doOnNext(resourceHolder -> {
+
+ MongoTransactionOptions mongoTransactionOptions = transactionOptionsResolver.resolve(definition)
+ .mergeWith(options);
+ mongoTransactionObject.startTransaction(mongoTransactionOptions.toDriverOptions());
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("Started transaction for session %s.", debugString(resourceHolder.getSession())));
+ }
+
+ })//
+ .onErrorMap(
+ ex -> new TransactionSystemException(String.format("Could not start Mongo transaction for session %s.",
+ debugString(mongoTransactionObject.getSession())), ex))
+ .doOnSuccess(resourceHolder -> {
+
+ synchronizationManager.bindResource(getRequiredDatabaseFactory(), resourceHolder);
+ }).then();
+ });
+ }
+
+ @Override
+ protected Mono doSuspend(TransactionSynchronizationManager synchronizationManager, Object transaction)
+ throws TransactionException {
+
+ return Mono.fromSupplier(() -> {
+
+ ReactiveMongoTransactionObject mongoTransactionObject = extractMongoTransaction(transaction);
+ mongoTransactionObject.setResourceHolder(null);
+
+ return synchronizationManager.unbindResource(getRequiredDatabaseFactory());
+ });
+ }
+
+ @Override
+ protected Mono doResume(TransactionSynchronizationManager synchronizationManager, @Nullable Object transaction,
+ Object suspendedResources) {
+ return Mono
+ .fromRunnable(() -> synchronizationManager.bindResource(getRequiredDatabaseFactory(), suspendedResources));
+ }
+
+ @Override
+ protected final Mono doCommit(TransactionSynchronizationManager synchronizationManager,
+ GenericReactiveTransaction status) throws TransactionException {
+
+ return Mono.defer(() -> {
+
+ ReactiveMongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("About to commit transaction for session %s.",
+ debugString(mongoTransactionObject.getSession())));
+ }
+
+ return doCommit(synchronizationManager, mongoTransactionObject).onErrorMap(ex -> {
+ return new TransactionSystemException(String.format("Could not commit Mongo transaction for session %s.",
+ debugString(mongoTransactionObject.getSession())), ex);
+ });
+ });
+ }
+
+ /**
+ * Customization hook to perform an actual commit of the given transaction.
+ * If a commit operation encounters an error, the MongoDB driver throws a {@link MongoException} holding
+ * {@literal error labels}.
+ * By default those labels are ignored, nevertheless one might check for
+ * {@link MongoException#UNKNOWN_TRANSACTION_COMMIT_RESULT_LABEL transient commit errors labels} and retry the the
+ * commit.
+ *
+ * @param synchronizationManager reactive synchronization manager.
+ * @param transactionObject never {@literal null}.
+ */
+ protected Mono doCommit(TransactionSynchronizationManager synchronizationManager,
+ ReactiveMongoTransactionObject transactionObject) {
+ return transactionObject.commitTransaction();
+ }
+
+ @Override
+ protected Mono doRollback(TransactionSynchronizationManager synchronizationManager,
+ GenericReactiveTransaction status) {
+
+ return Mono.defer(() -> {
+
+ ReactiveMongoTransactionObject mongoTransactionObject = extractMongoTransaction(status);
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("About to abort transaction for session %s.",
+ debugString(mongoTransactionObject.getSession())));
+ }
+
+ return mongoTransactionObject.abortTransaction().onErrorResume(MongoException.class, ex -> {
+ return Mono
+ .error(new TransactionSystemException(String.format("Could not abort Mongo transaction for session %s.",
+ debugString(mongoTransactionObject.getSession())), ex));
+ });
+ });
+ }
+
+ @Override
+ protected Mono doSetRollbackOnly(TransactionSynchronizationManager synchronizationManager,
+ GenericReactiveTransaction status) throws TransactionException {
+
+ return Mono.fromRunnable(() -> {
+ ReactiveMongoTransactionObject transactionObject = extractMongoTransaction(status);
+ transactionObject.getRequiredResourceHolder().setRollbackOnly();
+ });
+ }
+
+ @Override
+ protected Mono doCleanupAfterCompletion(TransactionSynchronizationManager synchronizationManager,
+ Object transaction) {
+
+ Assert.isInstanceOf(ReactiveMongoTransactionObject.class, transaction,
+ () -> String.format("Expected to find a %s but it turned out to be %s.", ReactiveMongoTransactionObject.class,
+ transaction.getClass()));
+
+ return Mono.fromRunnable(() -> {
+ ReactiveMongoTransactionObject mongoTransactionObject = (ReactiveMongoTransactionObject) transaction;
+
+ // Remove the connection holder from the thread.
+ synchronizationManager.unbindResource(getRequiredDatabaseFactory());
+ mongoTransactionObject.getRequiredResourceHolder().clear();
+
+ if (logger.isDebugEnabled()) {
+ logger.debug(String.format("About to release Session %s after transaction.",
+ debugString(mongoTransactionObject.getSession())));
+ }
+
+ mongoTransactionObject.closeSession();
+ });
+ }
+
+ /**
+ * Set the {@link ReactiveMongoDatabaseFactory} that this instance should manage transactions for.
+ *
+ * @param databaseFactory must not be {@literal null}.
+ */
+ public void setDatabaseFactory(ReactiveMongoDatabaseFactory databaseFactory) {
+
+ Assert.notNull(databaseFactory, "DatabaseFactory must not be null");
+ this.databaseFactory = databaseFactory;
+ }
+
+ /**
+ * Set the {@link TransactionOptions} to be applied when starting transactions.
+ *
+ * @param options can be {@literal null}.
+ */
+ public void setOptions(@Nullable TransactionOptions options) {
+ this.options = MongoTransactionOptions.of(options);
+ }
+
+ /**
+ * Get the {@link ReactiveMongoDatabaseFactory} that this instance manages transactions for.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public ReactiveMongoDatabaseFactory getDatabaseFactory() {
+ return databaseFactory;
+ }
+
+ @Override
+ public void afterPropertiesSet() {
+ getRequiredDatabaseFactory();
+ }
+
+ private Mono newResourceHolder(TransactionDefinition definition,
+ ClientSessionOptions options) {
+
+ ReactiveMongoDatabaseFactory dbFactory = getRequiredDatabaseFactory();
+
+ return dbFactory.getSession(options).map(session -> new ReactiveMongoResourceHolder(session, dbFactory));
+ }
+
+ /**
+ * @throws IllegalStateException if {@link #databaseFactory} is {@literal null}.
+ */
+ private ReactiveMongoDatabaseFactory getRequiredDatabaseFactory() {
+
+ Assert.state(databaseFactory != null,
+ "ReactiveMongoTransactionManager operates upon a ReactiveMongoDatabaseFactory; Did you forget to provide one; It's required");
+
+ return databaseFactory;
+ }
+
+ private static ReactiveMongoTransactionObject extractMongoTransaction(Object transaction) {
+
+ Assert.isInstanceOf(ReactiveMongoTransactionObject.class, transaction,
+ () -> String.format("Expected to find a %s but it turned out to be %s.", ReactiveMongoTransactionObject.class,
+ transaction.getClass()));
+
+ return (ReactiveMongoTransactionObject) transaction;
+ }
+
+ private static ReactiveMongoTransactionObject extractMongoTransaction(GenericReactiveTransaction status) {
+
+ Assert.isInstanceOf(ReactiveMongoTransactionObject.class, status.getTransaction(),
+ () -> String.format("Expected to find a %s but it turned out to be %s.", ReactiveMongoTransactionObject.class,
+ status.getTransaction().getClass()));
+
+ return (ReactiveMongoTransactionObject) status.getTransaction();
+ }
+
+ private static String debugString(@Nullable ClientSession session) {
+
+ if (session == null) {
+ return "null";
+ }
+
+ String debugString = String.format("[%s@%s ", ClassUtils.getShortName(session.getClass()),
+ Integer.toHexString(session.hashCode()));
+
+ try {
+ if (session.getServerSession() != null) {
+ debugString += String.format("id = %s, ", session.getServerSession().getIdentifier());
+ debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
+ debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
+ debugString += String.format("txNumber = %d, ", session.getServerSession().getTransactionNumber());
+ debugString += String.format("closed = %b, ", session.getServerSession().isClosed());
+ debugString += String.format("clusterTime = %s", session.getClusterTime());
+ } else {
+ debugString += "id = n/a";
+ debugString += String.format("causallyConsistent = %s, ", session.isCausallyConsistent());
+ debugString += String.format("txActive = %s, ", session.hasActiveTransaction());
+ debugString += String.format("clusterTime = %s", session.getClusterTime());
+ }
+ } catch (RuntimeException e) {
+ debugString += String.format("error = %s", e.getMessage());
+ }
+
+ debugString += "]";
+
+ return debugString;
+ }
+
+ /**
+ * MongoDB specific transaction object, representing a {@link MongoResourceHolder}. Used as transaction object by
+ * {@link ReactiveMongoTransactionManager}.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 2.2
+ * @see ReactiveMongoResourceHolder
+ */
+ protected static class ReactiveMongoTransactionObject implements SmartTransactionObject {
+
+ private @Nullable ReactiveMongoResourceHolder resourceHolder;
+
+ ReactiveMongoTransactionObject(@Nullable ReactiveMongoResourceHolder resourceHolder) {
+ this.resourceHolder = resourceHolder;
+ }
+
+ /**
+ * Set the {@link MongoResourceHolder}.
+ *
+ * @param resourceHolder can be {@literal null}.
+ */
+ void setResourceHolder(@Nullable ReactiveMongoResourceHolder resourceHolder) {
+ this.resourceHolder = resourceHolder;
+ }
+
+ /**
+ * @return {@literal true} if a {@link MongoResourceHolder} is set.
+ */
+ final boolean hasResourceHolder() {
+ return resourceHolder != null;
+ }
+
+ /**
+ * Start a MongoDB transaction optionally given {@link TransactionOptions}.
+ *
+ * @param options can be {@literal null}
+ */
+ void startTransaction(@Nullable TransactionOptions options) {
+
+ ClientSession session = getRequiredSession();
+ if (options != null) {
+ session.startTransaction(options);
+ } else {
+ session.startTransaction();
+ }
+ }
+
+ /**
+ * Commit the transaction.
+ */
+ public Mono commitTransaction() {
+ return Mono.from(getRequiredSession().commitTransaction());
+ }
+
+ /**
+ * Rollback (abort) the transaction.
+ */
+ public Mono abortTransaction() {
+ return Mono.from(getRequiredSession().abortTransaction());
+ }
+
+ /**
+ * Close a {@link ClientSession} without regard to its transactional state.
+ */
+ void closeSession() {
+
+ ClientSession session = getRequiredSession();
+ if (session.getServerSession() != null && !session.getServerSession().isClosed()) {
+ session.close();
+ }
+ }
+
+ @Nullable
+ public ClientSession getSession() {
+ return resourceHolder != null ? resourceHolder.getSession() : null;
+ }
+
+ private ReactiveMongoResourceHolder getRequiredResourceHolder() {
+
+ Assert.state(resourceHolder != null, "ReactiveMongoResourceHolder is required but not present; o_O");
+ return resourceHolder;
+ }
+
+ private ClientSession getRequiredSession() {
+
+ ClientSession session = getSession();
+ Assert.state(session != null, "A Session is required but it turned out to be null");
+ return session;
+ }
+
+ @Override
+ public boolean isRollbackOnly() {
+ return this.resourceHolder != null && this.resourceHolder.isRollbackOnly();
+ }
+
+ @Override
+ public void flush() {
+ throw new UnsupportedOperationException("flush() not supported");
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java
new file mode 100644
index 0000000000..93dbf5db69
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionAwareMethodInterceptor.java
@@ -0,0 +1,207 @@
+/*
+ * Copyright 2018-2025 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.lang.reflect.Proxy;
+import java.util.Optional;
+import java.util.function.BiFunction;
+
+import org.aopalliance.intercept.MethodInterceptor;
+import org.aopalliance.intercept.MethodInvocation;
+import org.springframework.core.MethodClassKey;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ConcurrentReferenceHashMap;
+import org.springframework.util.ReflectionUtils;
+
+import com.mongodb.WriteConcern;
+import com.mongodb.session.ClientSession;
+
+/**
+ * {@link MethodInterceptor} implementation looking up and invoking an alternative target method having
+ * {@link ClientSession} as its first argument. This allows seamless integration with the existing code base.
+ *
+ * The {@link MethodInterceptor} is aware of methods on {@code MongoCollection} that my return new instances of itself
+ * like (eg. {@link com.mongodb.reactivestreams.client.MongoCollection#withWriteConcern(WriteConcern)} and decorate them
+ * if not already proxied.
+ *
+ * @param Type of the actual Mongo Database.
+ * @param Type of the actual Mongo Collection.
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 2.1
+ */
+public class SessionAwareMethodInterceptor implements MethodInterceptor {
+
+ private static final MethodCache METHOD_CACHE = new MethodCache();
+
+ private final ClientSession session;
+ private final ClientSessionOperator collectionDecorator;
+ private final ClientSessionOperator databaseDecorator;
+ private final Object target;
+ private final Class> targetType;
+ private final Class> collectionType;
+ private final Class> databaseType;
+ private final Class extends ClientSession> sessionType;
+
+ /**
+ * Create a new SessionAwareMethodInterceptor for given target.
+ *
+ * @param session the {@link ClientSession} to be used on invocation.
+ * @param target the original target object.
+ * @param databaseType the MongoDB database type
+ * @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
+ * {@code MongoDatabase}.
+ * @param collectionType the MongoDB collection type.
+ * @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive
+ * {@code MongoCollection}.
+ * @param target object type.
+ */
+ public SessionAwareMethodInterceptor(ClientSession session, T target, Class extends ClientSession> sessionType,
+ Class databaseType, ClientSessionOperator databaseDecorator, Class collectionType,
+ ClientSessionOperator collectionDecorator) {
+
+ Assert.notNull(session, "ClientSession must not be null");
+ Assert.notNull(target, "Target must not be null");
+ Assert.notNull(sessionType, "SessionType must not be null");
+ Assert.notNull(databaseType, "Database type must not be null");
+ Assert.notNull(databaseDecorator, "Database ClientSessionOperator must not be null");
+ Assert.notNull(collectionType, "Collection type must not be null");
+ Assert.notNull(collectionDecorator, "Collection ClientSessionOperator must not be null");
+
+ this.session = session;
+ this.target = target;
+ this.databaseType = ClassUtils.getUserClass(databaseType);
+ this.collectionType = ClassUtils.getUserClass(collectionType);
+ this.collectionDecorator = collectionDecorator;
+ this.databaseDecorator = databaseDecorator;
+
+ this.targetType = ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseType : collectionType;
+ this.sessionType = sessionType;
+ }
+
+ @Nullable
+ @Override
+ public Object invoke(MethodInvocation methodInvocation) throws Throwable {
+
+ if (requiresDecoration(methodInvocation.getMethod())) {
+
+ Object target = methodInvocation.proceed();
+ if (target instanceof Proxy) {
+ return target;
+ }
+
+ return decorate(target);
+ }
+
+ if (!requiresSession(methodInvocation.getMethod())) {
+ return methodInvocation.proceed();
+ }
+
+ Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType, sessionType);
+
+ return !targetMethod.isPresent() ? methodInvocation.proceed()
+ : ReflectionUtils.invokeMethod(targetMethod.get(), target,
+ prependSessionToArguments(session, methodInvocation));
+ }
+
+ private boolean requiresDecoration(Method method) {
+
+ return ClassUtils.isAssignable(databaseType, method.getReturnType())
+ || ClassUtils.isAssignable(collectionType, method.getReturnType());
+ }
+
+ @SuppressWarnings("unchecked")
+ protected Object decorate(Object target) {
+
+ return ClassUtils.isAssignable(databaseType, target.getClass()) ? databaseDecorator.apply(session, target)
+ : collectionDecorator.apply(session, target);
+ }
+
+ private static boolean requiresSession(Method method) {
+
+ return method.getParameterCount() == 0
+ || !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0]);
+ }
+
+ private static Object[] prependSessionToArguments(ClientSession session, MethodInvocation invocation) {
+
+ Object[] args = new Object[invocation.getArguments().length + 1];
+
+ args[0] = session;
+ System.arraycopy(invocation.getArguments(), 0, args, 1, invocation.getArguments().length);
+
+ return args;
+ }
+
+ /**
+ * Simple {@link Method} to {@link Method} caching facility for {@link ClientSession} overloaded targets.
+ *
+ * @since 2.1
+ * @author Christoph Strobl
+ */
+ static class MethodCache {
+
+ private final ConcurrentReferenceHashMap> cache = new ConcurrentReferenceHashMap<>();
+
+ /**
+ * Lookup the target {@link Method}.
+ *
+ * @param method
+ * @param targetClass
+ * @return
+ */
+ Optional lookup(Method method, Class> targetClass, Class extends ClientSession> sessionType) {
+
+ return cache.computeIfAbsent(new MethodClassKey(method, targetClass),
+ val -> Optional.ofNullable(findTargetWithSession(method, targetClass, sessionType)));
+ }
+
+ @Nullable
+ private Method findTargetWithSession(Method sourceMethod, Class> targetType,
+ Class extends ClientSession> sessionType) {
+
+ Class>[] argTypes = sourceMethod.getParameterTypes();
+ Class>[] args = new Class>[argTypes.length + 1];
+ args[0] = sessionType;
+ System.arraycopy(argTypes, 0, args, 1, argTypes.length);
+
+ return ReflectionUtils.findMethod(targetType, sourceMethod.getName(), args);
+ }
+
+ /**
+ * Check whether the cache contains an entry for {@link Method} and {@link Class}.
+ *
+ * @param method
+ * @param targetClass
+ * @return
+ */
+ boolean contains(Method method, Class> targetClass) {
+ return cache.containsKey(new MethodClassKey(method, targetClass));
+ }
+ }
+
+ /**
+ * Represents an operation upon two operands of the same type, producing a result of the same type as the operands
+ * accepting {@link ClientSession}. This is a specialization of {@link BiFunction} for the case where the operands and
+ * the result are all of the same type.
+ *
+ * @param the type of the operands and result of the operator
+ */
+ public interface ClientSessionOperator extends BiFunction {}
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java
new file mode 100644
index 0000000000..07b5c31586
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SessionSynchronization.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2018-2025 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 org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+
+/**
+ * {@link SessionSynchronization} is used along with {@code MongoTemplate} to define in which type of transactions to
+ * participate if any.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 2.1
+ * @see MongoTemplate#setSessionSynchronization(SessionSynchronization)
+ * @see MongoDatabaseUtils#getDatabase(MongoDatabaseFactory, SessionSynchronization)
+ * @see ReactiveMongoTemplate#setSessionSynchronization(SessionSynchronization)
+ * @see ReactiveMongoDatabaseUtils#getDatabase(ReactiveMongoDatabaseFactory, SessionSynchronization)
+ */
+public enum SessionSynchronization {
+
+ /**
+ * Synchronize with any transaction even with empty transactions and initiate a MongoDB transaction when doing so by
+ * registering a MongoDB specific {@link org.springframework.transaction.support.ResourceHolderSynchronization}.
+ */
+ ALWAYS,
+
+ /**
+ * Synchronize with native MongoDB transactions initiated via {@link MongoTransactionManager}.
+ */
+ ON_ACTUAL_TRANSACTION,
+
+ /**
+ * Do not participate in ongoing transactions.
+ *
+ * @since 3.2.5
+ */
+ NEVER
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
new file mode 100644
index 0000000000..b52fc0bd71
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SimpleMongoTransactionOptions.java
@@ -0,0 +1,154 @@
+/*
+ * Copyright 2024-2025 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.time.Duration;
+import java.util.Arrays;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+
+import com.mongodb.Function;
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadConcernLevel;
+import com.mongodb.ReadPreference;
+import com.mongodb.WriteConcern;
+
+/**
+ * Trivial implementation of {@link MongoTransactionOptions}.
+ *
+ * @author Christoph Strobl
+ * @since 4.3
+ */
+class SimpleMongoTransactionOptions implements MongoTransactionOptions {
+
+ static final Set KNOWN_KEYS = Arrays.stream(OptionKey.values()).map(OptionKey::getKey)
+ .collect(Collectors.toSet());
+
+ private final Duration maxCommitTime;
+ private final ReadConcern readConcern;
+ private final ReadPreference readPreference;
+ private final WriteConcern writeConcern;
+
+ static SimpleMongoTransactionOptions of(Map options) {
+ return new SimpleMongoTransactionOptions(options);
+ }
+
+ private SimpleMongoTransactionOptions(Map options) {
+
+ this.maxCommitTime = doGetMaxCommitTime(options);
+ this.readConcern = doGetReadConcern(options);
+ this.readPreference = doGetReadPreference(options);
+ this.writeConcern = doGetWriteConcern(options);
+ }
+
+ @Nullable
+ @Override
+ public Duration getMaxCommitTime() {
+ return maxCommitTime;
+ }
+
+ @Nullable
+ @Override
+ public ReadConcern getReadConcern() {
+ return readConcern;
+ }
+
+ @Nullable
+ @Override
+ public ReadPreference getReadPreference() {
+ return readPreference;
+ }
+
+ @Nullable
+ @Override
+ public WriteConcern getWriteConcern() {
+ return writeConcern;
+ }
+
+ @Override
+ public String toString() {
+
+ return "DefaultMongoTransactionOptions{" + "maxCommitTime=" + maxCommitTime + ", readConcern=" + readConcern
+ + ", readPreference=" + readPreference + ", writeConcern=" + writeConcern + '}';
+ }
+
+ @Nullable
+ private static Duration doGetMaxCommitTime(Map options) {
+
+ return getValue(options, OptionKey.MAX_COMMIT_TIME, value -> {
+
+ Duration timeout = Duration.parse(value);
+ Assert.isTrue(!timeout.isNegative(), "%s cannot be negative".formatted(OptionKey.MAX_COMMIT_TIME));
+ return timeout;
+ });
+ }
+
+ @Nullable
+ private static ReadConcern doGetReadConcern(Map options) {
+ return getValue(options, OptionKey.READ_CONCERN, value -> new ReadConcern(ReadConcernLevel.fromString(value)));
+ }
+
+ @Nullable
+ private static ReadPreference doGetReadPreference(Map options) {
+ return getValue(options, OptionKey.READ_PREFERENCE, ReadPreference::valueOf);
+ }
+
+ @Nullable
+ private static WriteConcern doGetWriteConcern(Map options) {
+
+ return getValue(options, OptionKey.WRITE_CONCERN, value -> {
+
+ WriteConcern writeConcern = WriteConcern.valueOf(value);
+ if (writeConcern == null) {
+ throw new IllegalArgumentException("'%s' is not a valid WriteConcern".formatted(options.get("writeConcern")));
+ }
+ return writeConcern;
+ });
+ }
+
+ @Nullable
+ private static T getValue(Map options, OptionKey key, Function convertFunction) {
+
+ String value = options.get(key.getKey());
+ return value != null ? convertFunction.apply(value) : null;
+ }
+
+ enum OptionKey {
+
+ MAX_COMMIT_TIME("maxCommitTime"), READ_CONCERN("readConcern"), READ_PREFERENCE("readPreference"), WRITE_CONCERN(
+ "writeConcern");
+
+ final String key;
+
+ OptionKey(String key) {
+ this.key = key;
+ }
+
+ public String getKey() {
+ return key;
+ }
+
+ @Override
+ public String toString() {
+ return getKey();
+ }
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SpringDataMongoDB.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SpringDataMongoDB.java
new file mode 100644
index 0000000000..a3d600270f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/SpringDataMongoDB.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2020-2025 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 org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.data.util.Version;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.MongoDriverInformation;
+
+/**
+ * Class that exposes the SpringData MongoDB specific information like the current {@link Version} or
+ * {@link MongoDriverInformation driver information}.
+ *
+ * @author Christoph Strobl
+ * @since 3.0
+ */
+public class SpringDataMongoDB {
+
+ private static final Log LOGGER = LogFactory.getLog(SpringDataMongoDB.class);
+
+ private static final Version FALLBACK_VERSION = new Version(3);
+ private static final MongoDriverInformation DRIVER_INFORMATION = MongoDriverInformation
+ .builder(MongoDriverInformation.builder().build()).driverName("spring-data").build();
+
+ /**
+ * Obtain the SpringData MongoDB specific driver information.
+ *
+ * @return never {@literal null}.
+ */
+ public static MongoDriverInformation driverInformation() {
+ return DRIVER_INFORMATION;
+ }
+
+ /**
+ * Fetches the "Implementation-Version" manifest attribute from the jar file.
+ *
+ * Note that some ClassLoaders do not expose the package metadata, hence this class might not be able to determine the
+ * version in all environments. In this case the current Major version is returned as a fallback.
+ *
+ * @return never {@literal null}.
+ */
+ public static Version version() {
+
+ Package pkg = SpringDataMongoDB.class.getPackage();
+ String versionString = (pkg != null ? pkg.getImplementationVersion() : null);
+
+ if (!StringUtils.hasText(versionString)) {
+
+ LOGGER.debug("Unable to find Spring Data MongoDB version.");
+ return FALLBACK_VERSION;
+ }
+
+ try {
+ return Version.parse(versionString);
+ } catch (Exception e) {
+ LOGGER.debug(String.format("Cannot read Spring Data MongoDB version '%s'.", versionString));
+ }
+
+ return FALLBACK_VERSION;
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
new file mode 100644
index 0000000000..cd5f58d5b1
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionMetadata.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2024-2025 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.time.Duration;
+
+import org.springframework.lang.Nullable;
+
+/**
+ * MongoDB-specific transaction metadata.
+ *
+ * @author Christoph Strobl
+ * @since 4.3
+ */
+public interface TransactionMetadata {
+
+ /**
+ * @return the maximum commit time. Can be {@literal null} if not configured.
+ */
+ @Nullable
+ Duration getMaxCommitTime();
+
+ /**
+ * @return {@literal true} if the max commit time is configured; {@literal false} otherwise.
+ */
+ default boolean hasMaxCommitTime() {
+ return getMaxCommitTime() != null;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
new file mode 100644
index 0000000000..37c7e3686b
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransactionOptionResolver.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2024-2025 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 org.springframework.lang.Nullable;
+import org.springframework.transaction.TransactionDefinition;
+
+/**
+ * Interface that defines a resolver for {@link TransactionMetadata} based on a {@link TransactionDefinition}.
+ * Transaction metadata is used to enrich the MongoDB transaction with additional information.
+ *
+ * @author Christoph Strobl
+ * @since 4.3
+ */
+interface TransactionOptionResolver {
+
+ /**
+ * Resolves the transaction metadata from a given {@link TransactionDefinition}.
+ *
+ * @param definition the {@link TransactionDefinition}.
+ * @return the resolved {@link TransactionMetadata} or {@literal null} if the resolver cannot resolve any metadata.
+ */
+ @Nullable
+ T resolve(TransactionDefinition definition);
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java
new file mode 100644
index 0000000000..5446170ff9
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientClientSessionException.java
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2018-2025 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 org.springframework.dao.TransientDataAccessException;
+
+/**
+ * {@link TransientDataAccessException} specific to MongoDB {@link com.mongodb.session.ClientSession} related data
+ * access failures such as reading data using an already closed session.
+ *
+ * @author Christoph Strobl
+ * @since 4.4
+ */
+public class TransientClientSessionException extends TransientMongoDbException {
+
+ /**
+ * Constructor for {@link TransientClientSessionException}.
+ *
+ * @param msg the detail message.
+ * @param cause the root cause.
+ */
+ public TransientClientSessionException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java
new file mode 100644
index 0000000000..cad05ca17c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/TransientMongoDbException.java
@@ -0,0 +1,39 @@
+/*
+ * Copyright 2018-2025 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 org.springframework.dao.TransientDataAccessException;
+
+/**
+ * Root of the hierarchy of MongoDB specific data access exceptions that are considered transient such as
+ * {@link com.mongodb.MongoException MongoExceptions} carrying {@link com.mongodb.MongoException#hasErrorLabel(String)
+ * specific labels}.
+ *
+ * @author Christoph Strobl
+ * @since 4.4
+ */
+public class TransientMongoDbException extends TransientDataAccessException {
+
+ /**
+ * Constructor for {@link TransientMongoDbException}.
+ *
+ * @param msg the detail message.
+ * @param cause the root cause.
+ */
+ public TransientMongoDbException(String msg, Throwable cause) {
+ super(msg, cause);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
index 7d33b0871a..bec05d0d68 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/UncategorizedMongoDbException.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2010-2011 the original author or authors.
+ * Copyright 2010-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -16,12 +16,13 @@
package org.springframework.data.mongodb;
import org.springframework.dao.UncategorizedDataAccessException;
+import org.springframework.lang.Nullable;
public class UncategorizedMongoDbException extends UncategorizedDataAccessException {
private static final long serialVersionUID = -2336595514062364929L;
- public UncategorizedMongoDbException(String msg, Throwable cause) {
+ public UncategorizedMongoDbException(String msg, @Nullable Throwable cause) {
super(msg, cause);
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java
new file mode 100644
index 0000000000..2254b3c9a8
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/LazyLoadingProxyAotProcessor.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2022-2025 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 java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.aot.generate.GenerationContext;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.core.annotation.MergedAnnotations;
+import org.springframework.data.annotation.Reference;
+import org.springframework.data.mongodb.core.convert.LazyLoadingProxyFactory;
+import org.springframework.data.mongodb.core.convert.LazyLoadingProxyFactory.LazyLoadingInterceptor;
+import org.springframework.data.mongodb.core.mapping.DBRef;
+import org.springframework.data.mongodb.core.mapping.DocumentReference;
+
+/**
+ * @author Christoph Strobl
+ * @since 4.0
+ */
+public class LazyLoadingProxyAotProcessor {
+
+ private boolean generalLazyLoadingProxyContributed = false;
+
+ public void registerLazyLoadingProxyIfNeeded(Class> type, GenerationContext generationContext) {
+
+ Set refFields = getFieldsWithAnnotationPresent(type, Reference.class);
+ if (refFields.isEmpty()) {
+ return;
+ }
+
+ refFields.stream() //
+ .filter(LazyLoadingProxyAotProcessor::isLazyLoading) //
+ .forEach(field -> {
+
+ if (!generalLazyLoadingProxyContributed) {
+ generationContext.getRuntimeHints().proxies().registerJdkProxy(
+ TypeReference.of(org.springframework.data.mongodb.core.convert.LazyLoadingProxy.class),
+ TypeReference.of(org.springframework.aop.SpringProxy.class),
+ TypeReference.of(org.springframework.aop.framework.Advised.class),
+ TypeReference.of(org.springframework.core.DecoratingProxy.class));
+ generalLazyLoadingProxyContributed = true;
+ }
+
+ if (field.getType().isInterface()) {
+
+ List> interfaces = new ArrayList<>(
+ Arrays.asList(LazyLoadingProxyFactory.prepareFactory(field.getType()).getProxiedInterfaces()));
+ interfaces.add(org.springframework.aop.SpringProxy.class);
+ interfaces.add(org.springframework.aop.framework.Advised.class);
+ interfaces.add(org.springframework.core.DecoratingProxy.class);
+
+ generationContext.getRuntimeHints().proxies().registerJdkProxy(interfaces.toArray(Class[]::new));
+ } else {
+
+ Class> proxyClass = LazyLoadingProxyFactory.resolveProxyType(field.getType(),
+ LazyLoadingInterceptor::none);
+
+ // see: spring-projects/spring-framework/issues/29309
+ generationContext.getRuntimeHints().reflection().registerType(proxyClass, MongoAotReflectionHelper::cglibProxyReflectionMemberAccess);
+ }
+ });
+ }
+
+ private static boolean isLazyLoading(Field field) {
+ if (AnnotatedElementUtils.isAnnotated(field, DBRef.class)) {
+ return AnnotatedElementUtils.findMergedAnnotation(field, DBRef.class).lazy();
+ }
+ if (AnnotatedElementUtils.isAnnotated(field, DocumentReference.class)) {
+ return AnnotatedElementUtils.findMergedAnnotation(field, DocumentReference.class).lazy();
+ }
+ return false;
+ }
+
+ private static Set getFieldsWithAnnotationPresent(Class> type, Class extends Annotation> annotation) {
+
+ Set fields = new LinkedHashSet<>();
+ for (Field field : type.getDeclaredFields()) {
+ if (MergedAnnotations.from(field).get(annotation).isPresent()) {
+ fields.add(field);
+ }
+ }
+ return fields;
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
new file mode 100644
index 0000000000..2fe27a2c9e
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotPredicates.java
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022-2025 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 java.util.function.Predicate;
+
+import org.springframework.data.mongodb.core.mapping.MongoSimpleTypes;
+import org.springframework.data.util.ReactiveWrappers;
+import org.springframework.data.util.ReactiveWrappers.ReactiveLibrary;
+import org.springframework.data.util.TypeUtils;
+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
+ */
+public class MongoAotPredicates {
+
+ public static final Predicate> IS_SIMPLE_TYPE = (type) -> MongoSimpleTypes.HOLDER.isSimpleType(type)
+ || TypeUtils.type(type).isPartOf("org.bson");
+ public static final Predicate IS_REACTIVE_LIBARARY_AVAILABLE = ReactiveWrappers::isAvailable;
+ public static final Predicate IS_SYNC_CLIENT_PRESENT = (classLoader) -> ClassUtils
+ .isPresent("com.mongodb.client.MongoClient", classLoader);
+ public static final Predicate IS_REACTIVE_CLIENT_PRESENT = (classLoader) -> ClassUtils
+ .isPresent("com.mongodb.reactivestreams.client.MongoClient", classLoader);
+
+ /**
+ * @return {@literal true} if the Project Reactor is present.
+ */
+ public static boolean isReactorPresent() {
+ return IS_REACTIVE_LIBARARY_AVAILABLE.test(ReactiveWrappers.ReactiveLibrary.PROJECT_REACTOR);
+ }
+
+ /**
+ * @param classLoader can be {@literal null}.
+ * @return {@literal true} if the {@link com.mongodb.client.MongoClient} is present.
+ * @since 4.0
+ */
+ public static boolean isSyncClientPresent(@Nullable ClassLoader classLoader) {
+ return IS_SYNC_CLIENT_PRESENT.test(classLoader);
+ }
+
+ /**
+ * @param classLoader can be {@literal null}.
+ * @return {@literal true} if the {@link com.mongodb.reactivestreams.client.MongoClient} is present.
+ * @since 4.3
+ */
+ public static boolean isReactiveClientPresent(@Nullable ClassLoader classLoader) {
+ return IS_REACTIVE_CLIENT_PRESENT.test(classLoader);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java
new file mode 100644
index 0000000000..ff8d04b382
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoAotReflectionHelper.java
@@ -0,0 +1,31 @@
+/*
+ * Copyright 2024-2025 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 org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.TypeHint.Builder;
+
+/**
+ * @author Christoph Strobl
+ */
+public final class MongoAotReflectionHelper {
+
+ public static void cglibProxyReflectionMemberAccess(Builder builder) {
+
+ builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS,
+ MemberCategory.DECLARED_FIELDS);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java
new file mode 100644
index 0000000000..a33f20ffb6
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoManagedTypesBeanRegistrationAotProcessor.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright 2022-2025 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 org.springframework.aot.generate.GenerationContext;
+import org.springframework.core.ResolvableType;
+import org.springframework.data.aot.ManagedTypesBeanRegistrationAotProcessor;
+import org.springframework.data.mongodb.MongoManagedTypes;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
+
+/**
+ * @author Christoph Strobl
+ * @since 2022/06
+ */
+class MongoManagedTypesBeanRegistrationAotProcessor extends ManagedTypesBeanRegistrationAotProcessor {
+
+ private final LazyLoadingProxyAotProcessor lazyLoadingProxyAotProcessor = new LazyLoadingProxyAotProcessor();
+
+ public MongoManagedTypesBeanRegistrationAotProcessor() {
+ setModuleIdentifier("mongo");
+ }
+
+ @Override
+ protected boolean isMatch(@Nullable Class> beanType, @Nullable String beanName) {
+ return isMongoManagedTypes(beanType) || super.isMatch(beanType, beanName);
+ }
+
+ protected boolean isMongoManagedTypes(@Nullable Class> beanType) {
+ return beanType != null && ClassUtils.isAssignable(MongoManagedTypes.class, beanType);
+ }
+
+ @Override
+ protected void contributeType(ResolvableType type, GenerationContext generationContext) {
+
+ if (MongoAotPredicates.IS_SIMPLE_TYPE.test(type.toClass())) {
+ return;
+ }
+
+ super.contributeType(type, generationContext);
+ lazyLoadingProxyAotProcessor.registerLazyLoadingProxyIfNeeded(type.toClass(), generationContext);
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
new file mode 100644
index 0000000000..538fe4e812
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/aot/MongoRuntimeHints.java
@@ -0,0 +1,129 @@
+/*
+ * Copyright 2022-2025 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.springframework.data.mongodb.aot.MongoAotPredicates.*;
+
+import java.util.Arrays;
+
+import org.springframework.aot.hint.MemberCategory;
+import org.springframework.aot.hint.RuntimeHints;
+import org.springframework.aot.hint.RuntimeHintsRegistrar;
+import org.springframework.aot.hint.TypeReference;
+import org.springframework.data.mongodb.core.mapping.event.AfterConvertCallback;
+import org.springframework.data.mongodb.core.mapping.event.AfterSaveCallback;
+import org.springframework.data.mongodb.core.mapping.event.BeforeConvertCallback;
+import org.springframework.data.mongodb.core.mapping.event.BeforeSaveCallback;
+import org.springframework.data.mongodb.core.mapping.event.ReactiveAfterConvertCallback;
+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.lang.Nullable;
+import org.springframework.util.ClassUtils;
+
+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;
+
+/**
+ * {@link RuntimeHintsRegistrar} for repository types and entity callbacks.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 4.0
+ */
+class MongoRuntimeHints implements RuntimeHintsRegistrar {
+
+ @Override
+ public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+
+ hints.reflection().registerTypes(
+ Arrays.asList(TypeReference.of(BeforeConvertCallback.class), TypeReference.of(BeforeSaveCallback.class),
+ TypeReference.of(AfterConvertCallback.class), TypeReference.of(AfterSaveCallback.class)),
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_METHODS));
+
+ registerTransactionProxyHints(hints, classLoader);
+ registerMongoCompatibilityAdapterHints(hints, classLoader);
+
+ if (isReactorPresent()) {
+
+ hints.reflection()
+ .registerTypes(Arrays.asList(TypeReference.of(ReactiveBeforeConvertCallback.class),
+ TypeReference.of(ReactiveBeforeSaveCallback.class), TypeReference.of(ReactiveAfterConvertCallback.class),
+ TypeReference.of(ReactiveAfterSaveCallback.class)),
+ builder -> builder.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ MemberCategory.INVOKE_PUBLIC_METHODS));
+ }
+ }
+
+ private static void registerTransactionProxyHints(RuntimeHints hints, @Nullable ClassLoader classLoader) {
+
+ if (MongoAotPredicates.isSyncClientPresent(classLoader)
+ && ClassUtils.isPresent("org.springframework.aop.SpringProxy", classLoader)) {
+
+ hints.proxies().registerJdkProxy(TypeReference.of("com.mongodb.client.MongoDatabase"),
+ TypeReference.of("org.springframework.aop.SpringProxy"),
+ TypeReference.of("org.springframework.core.DecoratingProxy"));
+ hints.proxies().registerJdkProxy(TypeReference.of("com.mongodb.client.MongoCollection"),
+ TypeReference.of("org.springframework.aop.SpringProxy"),
+ TypeReference.of("org.springframework.core.DecoratingProxy"));
+ }
+ }
+
+ @SuppressWarnings("deprecation")
+ 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)
+ .registerType(TypeReference.of("com.mongodb.internal.connection.StreamFactoryFactory"),
+ MemberCategory.INTROSPECT_PUBLIC_METHODS)
+ .registerType(TypeReference.of("com.mongodb.internal.build.MongoDriverVersion"), MemberCategory.PUBLIC_FIELDS);
+
+ if (MongoAotPredicates.isSyncClientPresent(classLoader)) {
+
+ hints.reflection() //
+ .registerType(MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(TypeReference.of("com.mongodb.client.internal.MongoDatabaseImpl"),
+ MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(MapReduceIterable.class, MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(TypeReference.of("com.mongodb.client.internal.MapReduceIterableImpl"),
+ MemberCategory.INVOKE_PUBLIC_METHODS);
+ }
+
+ if (MongoAotPredicates.isReactiveClientPresent(classLoader)) {
+
+ hints.reflection() //
+ .registerType(com.mongodb.reactivestreams.client.MongoDatabase.class, MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MongoDatabaseImpl"),
+ MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(MapReducePublisher.class, MemberCategory.INVOKE_PUBLIC_METHODS)
+ .registerType(TypeReference.of("com.mongodb.reactivestreams.client.internal.MapReducePublisherImpl"),
+ MemberCategory.INVOKE_PUBLIC_METHODS);
+ }
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoClientConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoClientConfiguration.java
new file mode 100644
index 0000000000..93033417fb
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoClientConfiguration.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2018-2025 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.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.MongoDatabaseFactory;
+import org.springframework.data.mongodb.SpringDataMongoDB;
+import org.springframework.data.mongodb.core.MongoTemplate;
+import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
+import org.springframework.data.mongodb.core.convert.DbRefResolver;
+import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoClientSettings.Builder;
+import com.mongodb.client.MongoClient;
+import com.mongodb.client.MongoClients;
+
+/**
+ * Base class for Spring Data MongoDB configuration using JavaConfig with {@link com.mongodb.client.MongoClient}.
+ *
+ * @author Christoph Strobl
+ * @since 2.1
+ * @see MongoConfigurationSupport
+ */
+@Configuration(proxyBeanMethods = false)
+public abstract class AbstractMongoClientConfiguration extends MongoConfigurationSupport {
+
+ /**
+ * Return the {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want to expose a
+ * {@link MongoClient} instance to the {@link org.springframework.context.ApplicationContext}.
+ * Override {@link #mongoClientSettings()} to configure connection details.
+ *
+ * @return never {@literal null}.
+ * @see #mongoClientSettings()
+ * @see #configureClientSettings(Builder)
+ */
+ public MongoClient mongoClient() {
+ return createMongoClient(mongoClientSettings());
+ }
+
+ /**
+ * Creates a {@link MongoTemplate}.
+ *
+ * @see #mongoDbFactory()
+ * @see #mappingMongoConverter(MongoDatabaseFactory, MongoCustomConversions, MongoMappingContext)
+ */
+ @Bean
+ public MongoTemplate mongoTemplate(MongoDatabaseFactory databaseFactory, MappingMongoConverter converter) {
+ return new MongoTemplate(databaseFactory, converter);
+ }
+
+ /**
+ * Creates a {@link org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory} to be used by the
+ * {@link MongoTemplate}. Will use the {@link MongoClient} instance configured in {@link #mongoClient()}.
+ *
+ * @see #mongoClient()
+ * @see #mongoTemplate(MongoDatabaseFactory, MappingMongoConverter)
+ */
+ @Bean
+ public MongoDatabaseFactory mongoDbFactory() {
+ return new SimpleMongoClientDatabaseFactory(mongoClient(), getDatabaseName());
+ }
+
+ /**
+ * Creates a {@link MappingMongoConverter} using the configured {@link #mongoDbFactory()} and
+ * {@link #mongoMappingContext(MongoCustomConversions, org.springframework.data.mongodb.MongoManagedTypes)}. Will get {@link #customConversions()} applied.
+ *
+ * @see #customConversions()
+ * @see #mongoMappingContext(MongoCustomConversions, org.springframework.data.mongodb.MongoManagedTypes)
+ * @see #mongoDbFactory()
+ */
+ @Bean
+ public MappingMongoConverter mappingMongoConverter(MongoDatabaseFactory databaseFactory,
+ MongoCustomConversions customConversions, MongoMappingContext mappingContext) {
+
+ DbRefResolver dbRefResolver = new DefaultDbRefResolver(databaseFactory);
+ MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mappingContext);
+ converter.setCustomConversions(customConversions);
+ converter.setCodecRegistryProvider(databaseFactory);
+
+ return converter;
+ }
+
+ /**
+ * Create the Reactive Streams {@link com.mongodb.reactivestreams.client.MongoClient} instance with given
+ * {@link MongoClientSettings}.
+ *
+ * @return never {@literal null}.
+ * @since 3.0
+ */
+ protected MongoClient createMongoClient(MongoClientSettings settings) {
+ return MongoClients.create(settings, SpringDataMongoDB.driverInformation());
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java
deleted file mode 100644
index b3915c7530..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractMongoConfiguration.java
+++ /dev/null
@@ -1,254 +0,0 @@
-/*
- * Copyright 2011-2015 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
- *
- * http://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.config;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.core.convert.converter.Converter;
-import org.springframework.core.type.filter.AnnotationTypeFilter;
-import org.springframework.data.annotation.Persistent;
-import org.springframework.data.authentication.UserCredentials;
-import org.springframework.data.mapping.context.MappingContextIsNewStrategyFactory;
-import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
-import org.springframework.data.mapping.model.FieldNamingStrategy;
-import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
-import org.springframework.data.mongodb.MongoDbFactory;
-import org.springframework.data.mongodb.core.MongoTemplate;
-import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
-import org.springframework.data.mongodb.core.convert.CustomConversions;
-import org.springframework.data.mongodb.core.convert.DbRefResolver;
-import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver;
-import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
-import org.springframework.data.mongodb.core.mapping.Document;
-import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.data.support.CachingIsNewStrategyFactory;
-import org.springframework.data.support.IsNewStrategyFactory;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.StringUtils;
-
-import com.mongodb.Mongo;
-import com.mongodb.MongoClient;
-
-/**
- * Base class for Spring Data MongoDB configuration using JavaConfig.
- *
- * @author Mark Pollack
- * @author Oliver Gierke
- * @author Thomas Darimont
- * @author Ryan Tenney
- * @author Christoph Strobl
- */
-@Configuration
-public abstract class AbstractMongoConfiguration {
-
- /**
- * Return the name of the database to connect to.
- *
- * @return must not be {@literal null}.
- */
- protected abstract String getDatabaseName();
-
- /**
- * Return the name of the authentication database to use. Defaults to {@literal null} and will turn into the value
- * returned by {@link #getDatabaseName()} later on effectively.
- *
- * @return
- * @deprecated since 1.7. {@link MongoClient} should hold authentication data within
- * {@link MongoClient#getCredentialsList()}
- */
- @Deprecated
- protected String getAuthenticationDatabaseName() {
- return null;
- }
-
- /**
- * Return the {@link Mongo} instance to connect to. Annotate with {@link Bean} in case you want to expose a
- * {@link Mongo} instance to the {@link org.springframework.context.ApplicationContext}.
- *
- * @return
- * @throws Exception
- */
- public abstract Mongo mongo() throws Exception;
-
- /**
- * Creates a {@link MongoTemplate}.
- *
- * @return
- * @throws Exception
- */
- @Bean
- public MongoTemplate mongoTemplate() throws Exception {
- return new MongoTemplate(mongoDbFactory(), mappingMongoConverter());
- }
-
- /**
- * Creates a {@link SimpleMongoDbFactory} to be used by the {@link MongoTemplate}. Will use the {@link Mongo} instance
- * configured in {@link #mongo()}.
- *
- * @see #mongo()
- * @see #mongoTemplate()
- * @return
- * @throws Exception
- */
- @Bean
- public MongoDbFactory mongoDbFactory() throws Exception {
- return new SimpleMongoDbFactory(mongo(), getDatabaseName(), getUserCredentials(), getAuthenticationDatabaseName());
- }
-
- /**
- * Return the base package to scan for mapped {@link Document}s. Will return the package name of the configuration
- * class' (the concrete class, not this one here) by default. So if you have a {@code com.acme.AppConfig} extending
- * {@link AbstractMongoConfiguration} the base package will be considered {@code com.acme} unless the method is
- * overriden to implement alternate behaviour.
- *
- * @return the base package to scan for mapped {@link Document} classes or {@literal null} to not enable scanning for
- * entities.
- */
- protected String getMappingBasePackage() {
-
- Package mappingBasePackage = getClass().getPackage();
- return mappingBasePackage == null ? null : mappingBasePackage.getName();
- }
-
- /**
- * Return {@link UserCredentials} to be used when connecting to the MongoDB instance or {@literal null} if none shall
- * be used.
- *
- * @return
- * @deprecated since 1.7. {@link MongoClient} should hold authentication data within
- * {@link MongoClient#getCredentialsList()}
- */
- @Deprecated
- protected UserCredentials getUserCredentials() {
- return null;
- }
-
- /**
- * Creates a {@link MongoMappingContext} equipped with entity classes scanned from the mapping base package.
- *
- * @see #getMappingBasePackage()
- * @return
- * @throws ClassNotFoundException
- */
- @Bean
- public MongoMappingContext mongoMappingContext() throws ClassNotFoundException {
-
- MongoMappingContext mappingContext = new MongoMappingContext();
- mappingContext.setInitialEntitySet(getInitialEntitySet());
- mappingContext.setSimpleTypeHolder(customConversions().getSimpleTypeHolder());
- mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
-
- return mappingContext;
- }
-
- /**
- * Returns a {@link MappingContextIsNewStrategyFactory} wrapped into a {@link CachingIsNewStrategyFactory}.
- *
- * @return
- * @throws ClassNotFoundException
- */
- @Bean
- public IsNewStrategyFactory isNewStrategyFactory() throws ClassNotFoundException {
- return new CachingIsNewStrategyFactory(new MappingContextIsNewStrategyFactory(mongoMappingContext()));
- }
-
- /**
- * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These
- * {@link CustomConversions} will be registered with the {@link #mappingMongoConverter()} and
- * {@link #mongoMappingContext()}. Returns an empty {@link CustomConversions} instance by default.
- *
- * @return must not be {@literal null}.
- */
- @Bean
- public CustomConversions customConversions() {
- return new CustomConversions(Collections.emptyList());
- }
-
- /**
- * Creates a {@link MappingMongoConverter} using the configured {@link #mongoDbFactory()} and
- * {@link #mongoMappingContext()}. Will get {@link #customConversions()} applied.
- *
- * @see #customConversions()
- * @see #mongoMappingContext()
- * @see #mongoDbFactory()
- * @return
- * @throws Exception
- */
- @Bean
- public MappingMongoConverter mappingMongoConverter() throws Exception {
-
- DbRefResolver dbRefResolver = new DefaultDbRefResolver(mongoDbFactory());
- MappingMongoConverter converter = new MappingMongoConverter(dbRefResolver, mongoMappingContext());
- converter.setCustomConversions(customConversions());
-
- return converter;
- }
-
- /**
- * Scans the mapping base package for classes annotated with {@link Document}.
- *
- * @see #getMappingBasePackage()
- * @return
- * @throws ClassNotFoundException
- */
- protected Set> getInitialEntitySet() throws ClassNotFoundException {
-
- String basePackage = getMappingBasePackage();
- Set> initialEntitySet = new HashSet>();
-
- if (StringUtils.hasText(basePackage)) {
- ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
- false);
- componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
- componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
-
- for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
- initialEntitySet.add(ClassUtils.forName(candidate.getBeanClassName(),
- AbstractMongoConfiguration.class.getClassLoader()));
- }
- }
-
- return initialEntitySet;
- }
-
- /**
- * Configures whether to abbreviate field names for domain objects by configuring a
- * {@link CamelCaseAbbreviatingFieldNamingStrategy} on the {@link MongoMappingContext} instance created. For advanced
- * customization needs, consider overriding {@link #mappingMongoConverter()}.
- *
- * @return
- */
- protected boolean abbreviateFieldNames() {
- return false;
- }
-
- /**
- * Configures a {@link FieldNamingStrategy} on the {@link MongoMappingContext} instance created.
- *
- * @return
- * @since 1.5
- */
- protected FieldNamingStrategy fieldNamingStrategy() {
- return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy()
- : PropertyNameFieldNamingStrategy.INSTANCE;
- }
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfiguration.java
new file mode 100644
index 0000000000..f93c4ae708
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/AbstractReactiveMongoConfiguration.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright 2016-2025 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.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory;
+import org.springframework.data.mongodb.SpringDataMongoDB;
+import org.springframework.data.mongodb.core.ReactiveMongoOperations;
+import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
+import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
+import org.springframework.data.mongodb.core.convert.NoOpDbRefResolver;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoClientSettings.Builder;
+import com.mongodb.reactivestreams.client.MongoClient;
+import com.mongodb.reactivestreams.client.MongoClients;
+
+/**
+ * Base class for reactive Spring Data MongoDB configuration using JavaConfig.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 2.0
+ * @see MongoConfigurationSupport
+ */
+@Configuration(proxyBeanMethods = false)
+public abstract class AbstractReactiveMongoConfiguration extends MongoConfigurationSupport {
+
+ /**
+ * Return the Reactive Streams {@link MongoClient} instance to connect to. Annotate with {@link Bean} in case you want
+ * to expose a {@link MongoClient} instance to the {@link org.springframework.context.ApplicationContext}.
+ * Override {@link #mongoClientSettings()} to configure connection details.
+ *
+ * @return never {@literal null}.
+ * @see #mongoClientSettings()
+ * @see #configureClientSettings(Builder)
+ */
+ public MongoClient reactiveMongoClient() {
+ return createReactiveMongoClient(mongoClientSettings());
+ }
+
+ /**
+ * Creates {@link ReactiveMongoOperations}.
+ *
+ * @see #reactiveMongoDbFactory()
+ * @see #mappingMongoConverter(ReactiveMongoDatabaseFactory, MongoCustomConversions, MongoMappingContext)
+ * @return never {@literal null}.
+ */
+ @Bean
+ public ReactiveMongoTemplate reactiveMongoTemplate(ReactiveMongoDatabaseFactory databaseFactory,
+ MappingMongoConverter mongoConverter) {
+ return new ReactiveMongoTemplate(databaseFactory, mongoConverter);
+ }
+
+ /**
+ * Creates a {@link ReactiveMongoDatabaseFactory} to be used by the {@link ReactiveMongoOperations}. Will use the
+ * {@link MongoClient} instance configured in {@link #reactiveMongoClient()}.
+ *
+ * @see #reactiveMongoClient()
+ * @see #reactiveMongoTemplate(ReactiveMongoDatabaseFactory, MappingMongoConverter)
+ * @return never {@literal null}.
+ */
+ @Bean
+ public ReactiveMongoDatabaseFactory reactiveMongoDbFactory() {
+ return new SimpleReactiveMongoDatabaseFactory(reactiveMongoClient(), getDatabaseName());
+ }
+
+ /**
+ * Creates a {@link MappingMongoConverter} using the configured {@link #reactiveMongoDbFactory()} and
+ * {@link #mongoMappingContext(MongoCustomConversions, org.springframework.data.mongodb.MongoManagedTypes)}. Will get {@link #customConversions()} applied.
+ *
+ * @see #customConversions()
+ * @see #mongoMappingContext(MongoCustomConversions, org.springframework.data.mongodb.MongoManagedTypes)
+ * @see #reactiveMongoDbFactory()
+ * @return never {@literal null}.
+ */
+ @Bean
+ public MappingMongoConverter mappingMongoConverter(ReactiveMongoDatabaseFactory databaseFactory,
+ MongoCustomConversions customConversions, MongoMappingContext mappingContext) {
+
+ MappingMongoConverter converter = new MappingMongoConverter(NoOpDbRefResolver.INSTANCE, mappingContext);
+ converter.setCustomConversions(customConversions);
+ converter.setCodecRegistryProvider(databaseFactory);
+
+ return converter;
+ }
+
+ /**
+ * Create the Reactive Streams {@link MongoClient} instance with given {@link MongoClientSettings}.
+ *
+ * @return never {@literal null}.
+ * @since 3.0
+ */
+ protected MongoClient createReactiveMongoClient(MongoClientSettings settings) {
+ return MongoClients.create(settings, SpringDataMongoDB.driverInformation());
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java
index e3da277512..584fbfba30 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/BeanNames.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2014 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -17,17 +17,18 @@
/**
* Constants to declare bean names used by the namespace configuration.
- *
+ *
* @author Jon Brisbin
* @author Oliver Gierke
* @author Martin Baumgartner
+ * @author Christoph Strobl
*/
public abstract class BeanNames {
public static final String MAPPING_CONTEXT_BEAN_NAME = "mongoMappingContext";
static final String INDEX_HELPER_BEAN_NAME = "indexCreationHelper";
- static final String MONGO_BEAN_NAME = "mongo";
+ static final String MONGO_BEAN_NAME = "mongoClient";
static final String DB_FACTORY_BEAN_NAME = "mongoDbFactory";
static final String VALIDATING_EVENT_LISTENER_BEAN_NAME = "validatingMongoEventListener";
static final String IS_NEW_STRATEGY_FACTORY_BEAN_NAME = "isNewStrategyFactory";
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java
new file mode 100644
index 0000000000..b070a0190f
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ConnectionStringPropertyEditor.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2019-2025 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.config;
+
+import java.beans.PropertyEditorSupport;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.ConnectionString;
+
+/**
+ * Parse a {@link String} to a {@link com.mongodb.ConnectionString}.
+ *
+ * @author Christoph Strobl
+ * @since 3.0
+ */
+public class ConnectionStringPropertyEditor extends PropertyEditorSupport {
+
+ @Override
+ public void setAsText(@Nullable String connectionString) {
+
+ if (!StringUtils.hasText(connectionString)) {
+ return;
+ }
+
+ setValue(new ConnectionString(connectionString));
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableMongoAuditing.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableMongoAuditing.java
index fff1b9f3df..d6ce19f3ee 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableMongoAuditing.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableMongoAuditing.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -28,7 +28,7 @@
/**
* Annotation to enable auditing in MongoDB via annotation configuration.
- *
+ *
* @author Thomas Darimont
* @author Oliver Gierke
*/
@@ -41,30 +41,30 @@
/**
* Configures the {@link AuditorAware} bean to be used to lookup the current principal.
- *
- * @return
+ *
+ * @return empty {@link String} by default.
*/
String auditorAwareRef() default "";
/**
* Configures whether the creation and modification dates are set. Defaults to {@literal true}.
- *
- * @return
+ *
+ * @return {@literal true} by default.
*/
boolean setDates() default true;
/**
* Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}.
- *
- * @return
+ *
+ * @return {@literal true} by default.
*/
boolean modifyOnCreate() default true;
/**
- * Configures a {@link DateTimeProvider} bean name that allows customizing the {@link org.joda.time.DateTime} to be
- * used for setting creation and modification dates.
- *
- * @return
+ * Configures a {@link DateTimeProvider} bean name that allows customizing the timestamp to be used for setting
+ * creation and modification dates.
+ *
+ * @return empty {@link String} by default.
*/
String dateTimeProviderRef() default "";
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableReactiveMongoAuditing.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableReactiveMongoAuditing.java
new file mode 100644
index 0000000000..21fadf86c6
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/EnableReactiveMongoAuditing.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2020-2025 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.config;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Inherited;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+import org.springframework.context.annotation.Import;
+import org.springframework.data.auditing.DateTimeProvider;
+import org.springframework.data.domain.ReactiveAuditorAware;
+
+/**
+ * Annotation to enable auditing in MongoDB using reactive infrastructure via annotation configuration.
+ *
+ * @author Mark Paluch
+ * @since 3.1
+ */
+@Inherited
+@Documented
+@Target(ElementType.TYPE)
+@Retention(RetentionPolicy.RUNTIME)
+@Import(ReactiveMongoAuditingRegistrar.class)
+public @interface EnableReactiveMongoAuditing {
+
+ /**
+ * Configures the {@link ReactiveAuditorAware} bean to be used to lookup the current principal.
+ *
+ * @return empty {@link String} by default.
+ */
+ String auditorAwareRef() default "";
+
+ /**
+ * Configures whether the creation and modification dates are set. Defaults to {@literal true}.
+ *
+ * @return {@literal true} by default.
+ */
+ boolean setDates() default true;
+
+ /**
+ * Configures whether the entity shall be marked as modified on creation. Defaults to {@literal true}.
+ *
+ * @return {@literal true} by default.
+ */
+ boolean modifyOnCreate() default true;
+
+ /**
+ * Configures a {@link DateTimeProvider} bean name that allows customizing the timestamp to be used for setting
+ * creation and modification dates.
+ *
+ * @return empty {@link String} by default.
+ */
+ String dateTimeProviderRef() default "";
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/GeoJsonConfiguration.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GeoJsonConfiguration.java
similarity index 71%
rename from spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/GeoJsonConfiguration.java
rename to spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GeoJsonConfiguration.java
index cb29dca57d..3b10019cc0 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/GeoJsonConfiguration.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GeoJsonConfiguration.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -13,19 +13,19 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-package org.springframework.data.mongodb.core;
+package org.springframework.data.mongodb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.data.mongodb.core.geo.GeoJsonModule;
-import org.springframework.data.web.config.SpringDataWebConfigurationMixin;
+import org.springframework.data.web.config.SpringDataJacksonModules;
/**
* Configuration class to expose {@link GeoJsonModule} as a Spring bean.
- *
+ *
* @author Oliver Gierke
+ * @author Jens Schauder
*/
-@SpringDataWebConfigurationMixin
-public class GeoJsonConfiguration {
+public class GeoJsonConfiguration implements SpringDataJacksonModules {
@Bean
public GeoJsonModule geoJsonModule() {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GridFsTemplateParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GridFsTemplateParser.java
index 83da976e1f..b86da91dad 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GridFsTemplateParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/GridFsTemplateParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013-2014 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -29,15 +29,11 @@
/**
* {@link BeanDefinitionParser} to parse {@code gridFsTemplate} elements into {@link BeanDefinition}s.
- *
+ *
* @author Martin Baumgartner
*/
class GridFsTemplateParser extends AbstractBeanDefinitionParser {
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
@@ -46,10 +42,6 @@ protected String resolveId(Element element, AbstractBeanDefinition definition, P
return StringUtils.hasText(id) ? id : BeanNames.GRID_FS_TEMPLATE_BEAN_NAME;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parseInternal(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
index 3aae756891..164b4defb6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MappingMongoConverterParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2014 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -18,13 +18,10 @@
import static org.springframework.data.mongodb.config.BeanNames.*;
import java.io.IOException;
-import java.util.Arrays;
-import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.springframework.beans.BeanMetadataElement;
-import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.BeanDefinitionHolder;
import org.springframework.beans.factory.config.RuntimeBeanReference;
@@ -51,33 +48,38 @@
import org.springframework.core.type.filter.TypeFilter;
import org.springframework.data.annotation.Persistent;
import org.springframework.data.config.BeanComponentDefinitionBuilder;
-import org.springframework.data.mapping.context.MappingContextIsNewStrategyFactory;
import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
-import org.springframework.data.mongodb.core.convert.CustomConversions;
import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
import org.springframework.data.mongodb.core.index.MongoPersistentEntityIndexCreator;
import org.springframework.data.mongodb.core.mapping.Document;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
import org.springframework.data.mongodb.core.mapping.event.ValidatingMongoEventListener;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.util.xml.DomUtils;
import org.w3c.dom.Element;
/**
* Bean definition parser for the {@code mapping-converter} element.
- *
+ *
* @author Jon Brisbin
* @author Oliver Gierke
* @author Maciej Walkowiak
* @author Thomas Darimont
* @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Zied Yaich
+ * @author Tomasz Forys
*/
public class MappingMongoConverterParser implements BeanDefinitionParser {
private static final String BASE_PACKAGE = "base-package";
- private static final boolean JSR_303_PRESENT = ClassUtils.isPresent("javax.validation.Validator",
+ private static final boolean JSR_303_PRESENT = ClassUtils.isPresent("jakarta.validation.Validator",
MappingMongoConverterParser.class.getClassLoader());
/* (non-Javadoc)
@@ -93,12 +95,12 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
String id = element.getAttribute(AbstractBeanDefinitionParser.ID_ATTRIBUTE);
id = StringUtils.hasText(id) ? id : DEFAULT_CONVERTER_BEAN_NAME;
+ boolean autoIndexCreationEnabled = isAutoIndexCreationEnabled(element);
+
parserContext.pushContainingComponent(new CompositeComponentDefinition("Mapping Mongo Converter", element));
BeanDefinition conversionsDefinition = getCustomConversions(element, parserContext);
- String ctxRef = potentiallyCreateMappingContext(element, parserContext, conversionsDefinition, id);
-
- createIsNewStrategyFactoryBeanDefinition(ctxRef, parserContext, element);
+ String ctxRef = potentiallyCreateMappingContext(element, parserContext, conversionsDefinition, id, autoIndexCreationEnabled);
// Need a reference to a Mongo instance
String dbFactoryRef = element.getAttribute("db-factory-ref");
@@ -120,27 +122,34 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
converterBuilder.addPropertyValue("customConversions", conversionsDefinition);
}
- try {
- registry.getBeanDefinition(INDEX_HELPER_BEAN_NAME);
- } catch (NoSuchBeanDefinitionException ignored) {
- if (!StringUtils.hasText(dbFactoryRef)) {
- dbFactoryRef = DB_FACTORY_BEAN_NAME;
- }
+ if (!registry.containsBeanDefinition("indexOperationsProvider")) {
+
+ BeanDefinitionBuilder indexOperationsProviderBuilder = BeanDefinitionBuilder
+ .genericBeanDefinition("org.springframework.data.mongodb.core.DefaultIndexOperationsProvider");
+ indexOperationsProviderBuilder.addConstructorArgReference(dbFactoryRef);
+ indexOperationsProviderBuilder.addConstructorArgValue(BeanDefinitionBuilder
+ .genericBeanDefinition(QueryMapper.class).addConstructorArgReference(id).getBeanDefinition());
+ parserContext.registerBeanComponent(
+ new BeanComponentDefinition(indexOperationsProviderBuilder.getBeanDefinition(), "indexOperationsProvider"));
+ }
+
+ if (!registry.containsBeanDefinition(INDEX_HELPER_BEAN_NAME)) {
+
BeanDefinitionBuilder indexHelperBuilder = BeanDefinitionBuilder
.genericBeanDefinition(MongoPersistentEntityIndexCreator.class);
indexHelperBuilder.addConstructorArgReference(ctxRef);
- indexHelperBuilder.addConstructorArgReference(dbFactoryRef);
+ indexHelperBuilder.addConstructorArgReference("indexOperationsProvider");
indexHelperBuilder.addDependsOn(ctxRef);
- parserContext.registerBeanComponent(new BeanComponentDefinition(indexHelperBuilder.getBeanDefinition(),
- INDEX_HELPER_BEAN_NAME));
+ parserContext.registerBeanComponent(
+ new BeanComponentDefinition(indexHelperBuilder.getBeanDefinition(), INDEX_HELPER_BEAN_NAME));
}
BeanDefinition validatingMongoEventListener = potentiallyCreateValidatingMongoEventListener(element, parserContext);
- if (validatingMongoEventListener != null) {
- parserContext.registerBeanComponent(new BeanComponentDefinition(validatingMongoEventListener,
- VALIDATING_EVENT_LISTENER_BEAN_NAME));
+ if (validatingMongoEventListener != null && !registry.containsBeanDefinition(VALIDATING_EVENT_LISTENER_BEAN_NAME)) {
+ parserContext.registerBeanComponent(
+ new BeanComponentDefinition(validatingMongoEventListener, VALIDATING_EVENT_LISTENER_BEAN_NAME));
}
parserContext.registerBeanComponent(new BeanComponentDefinition(converterBuilder.getBeanDefinition(), id));
@@ -148,18 +157,20 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
return null;
}
+ @Nullable
private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element element, ParserContext parserContext) {
String disableValidation = element.getAttribute("disable-validation");
- boolean validationDisabled = StringUtils.hasText(disableValidation) && Boolean.valueOf(disableValidation);
+ boolean validationDisabled = StringUtils.hasText(disableValidation) && Boolean.parseBoolean(disableValidation);
if (!validationDisabled) {
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition();
- RuntimeBeanReference validator = getValidator(builder, parserContext);
+ RuntimeBeanReference validator = getValidator(element, parserContext);
if (validator != null) {
builder.getRawBeanDefinition().setBeanClass(ValidatingMongoEventListener.class);
+ builder.getRawBeanDefinition().setSource(element);
builder.addConstructorArgValue(validator);
return builder.getBeanDefinition();
@@ -169,6 +180,7 @@ private BeanDefinition potentiallyCreateValidatingMongoEventListener(Element ele
return null;
}
+ @Nullable
private RuntimeBeanReference getValidator(Object source, ParserContext parserContext) {
if (!JSR_303_PRESENT) {
@@ -180,13 +192,39 @@ private RuntimeBeanReference getValidator(Object source, ParserContext parserCon
validatorDef.setSource(source);
validatorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
String validatorName = parserContext.getReaderContext().registerWithGeneratedName(validatorDef);
- parserContext.registerBeanComponent(new BeanComponentDefinition(validatorDef, validatorName));
return new RuntimeBeanReference(validatorName);
}
+ private static boolean isAutoIndexCreationEnabled(Element element) {
+
+ String autoIndexCreation = element.getAttribute("auto-index-creation");
+ return StringUtils.hasText(autoIndexCreation) && Boolean.parseBoolean(autoIndexCreation);
+ }
+
+ /**
+ * Create and register the {@link BeanDefinition} for a {@link MongoMappingContext} if not explicitly referenced by a
+ * given {@literal mapping-context-ref} {@link Element#getAttribute(String) attribuite}.
+ *
+ * @return the mapping context bean name.
+ * @deprecated since 4.3. Use
+ * {@link #potentiallyCreateMappingContext(Element, ParserContext, BeanDefinition, String, boolean)}
+ * instead.
+ */
+ @Deprecated(since = "4.3", forRemoval = true)
public static String potentiallyCreateMappingContext(Element element, ParserContext parserContext,
- BeanDefinition conversionsDefinition, String converterId) {
+ @Nullable BeanDefinition conversionsDefinition, @Nullable String converterId) {
+ return potentiallyCreateMappingContext(element, parserContext, conversionsDefinition, converterId, false);
+ }
+
+ /**
+ * Create and register the {@link BeanDefinition} for a {@link MongoMappingContext} if not explicitly referenced by a
+ * given {@literal mapping-context-ref} {@link Element#getAttribute(String) attribuite}.
+ *
+ * @return the mapping context bean name.
+ */
+ public static String potentiallyCreateMappingContext(Element element, ParserContext parserContext,
+ @Nullable BeanDefinition conversionsDefinition, @Nullable String converterId, boolean autoIndexCreation) {
String ctxRef = element.getAttribute("mapping-context-ref");
@@ -200,7 +238,7 @@ public static String potentiallyCreateMappingContext(Element element, ParserCont
BeanDefinitionBuilder mappingContextBuilder = BeanDefinitionBuilder
.genericBeanDefinition(MongoMappingContext.class);
- Set classesToAdd = getInititalEntityClasses(element);
+ Set classesToAdd = getInitialEntityClasses(element);
if (classesToAdd != null) {
mappingContextBuilder.addPropertyValue("initialEntitySet", classesToAdd);
@@ -214,6 +252,8 @@ public static String potentiallyCreateMappingContext(Element element, ParserCont
mappingContextBuilder.addPropertyValue("simpleTypeHolder", simpleTypesDefinition);
}
+ mappingContextBuilder.addPropertyValue("autoIndexCreation", autoIndexCreation);
+
parseFieldNamingStrategy(element, parserContext.getReaderContext(), mappingContextBuilder);
ctxRef = converterId == null || DEFAULT_CONVERTER_BEAN_NAME.equals(converterId) ? MAPPING_CONTEXT_BEAN_NAME
@@ -233,7 +273,7 @@ private static void parseFieldNamingStrategy(Element element, ReaderContext cont
&& Boolean.parseBoolean(abbreviateFieldNames);
if (fieldNamingStrategyReferenced && abbreviationActivated) {
- context.error("Field name abbreviation cannot be activated if a field-naming-strategy-ref is configured!",
+ context.error("Field name abbreviation cannot be activated if a field-naming-strategy-ref is configured",
element);
return;
}
@@ -251,6 +291,7 @@ private static void parseFieldNamingStrategy(Element element, ReaderContext cont
}
}
+ @Nullable
private BeanDefinition getCustomConversions(Element element, ParserContext parserContext) {
List customConvertersElements = DomUtils.getChildElementsByTagName(element, "custom-converters");
@@ -258,10 +299,10 @@ private BeanDefinition getCustomConversions(Element element, ParserContext parse
if (customConvertersElements.size() == 1) {
Element customerConvertersElement = customConvertersElements.get(0);
- ManagedList converterBeans = new ManagedList();
+ ManagedList converterBeans = new ManagedList<>();
List converterElements = DomUtils.getChildElementsByTagName(customerConvertersElement, "converter");
- if (converterElements != null) {
+ if (!ObjectUtils.isEmpty(converterElements)) {
for (Element listenerElement : converterElements) {
converterBeans.add(parseConverter(listenerElement, parserContext));
}
@@ -274,12 +315,10 @@ private BeanDefinition getCustomConversions(Element element, ParserContext parse
provider.addExcludeFilter(new NegatingFilter(new AssignableTypeFilter(Converter.class),
new AssignableTypeFilter(GenericConverter.class)));
- for (BeanDefinition candidate : provider.findCandidateComponents(packageToScan)) {
- converterBeans.add(candidate);
- }
+ converterBeans.addAll(provider.findCandidateComponents(packageToScan));
}
- BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(CustomConversions.class);
+ BeanDefinitionBuilder conversionsBuilder = BeanDefinitionBuilder.rootBeanDefinition(MongoCustomConversions.class);
conversionsBuilder.addConstructorArgValue(converterBeans);
AbstractBeanDefinition conversionsBean = conversionsBuilder.getBeanDefinition();
@@ -293,7 +332,8 @@ private BeanDefinition getCustomConversions(Element element, ParserContext parse
return null;
}
- private static Set getInititalEntityClasses(Element element) {
+ @Nullable
+ private static Set getInitialEntityClasses(Element element) {
String basePackage = element.getAttribute(BASE_PACKAGE);
@@ -306,7 +346,7 @@ private static Set getInititalEntityClasses(Element element) {
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
componentProvider.addIncludeFilter(new AnnotationTypeFilter(Persistent.class));
- Set classes = new ManagedSet();
+ Set classes = new ManagedSet<>();
for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
classes.add(candidate.getBeanClassName());
}
@@ -314,6 +354,7 @@ private static Set getInititalEntityClasses(Element element) {
return classes;
}
+ @Nullable
public BeanMetadataElement parseConverter(Element element, ParserContext parserContext) {
String converterRef = element.getAttribute("ref");
@@ -327,28 +368,14 @@ public BeanMetadataElement parseConverter(Element element, ParserContext parserC
return beanDef;
}
- parserContext.getReaderContext().error(
- "Element must specify 'ref' or contain a bean definition for the converter", element);
+ parserContext.getReaderContext()
+ .error("Element must specify 'ref' or contain a bean definition for the converter", element);
return null;
}
- public static String createIsNewStrategyFactoryBeanDefinition(String mappingContextRef, ParserContext context,
- Element element) {
-
- BeanDefinitionBuilder mappingContextStrategyFactoryBuilder = BeanDefinitionBuilder
- .rootBeanDefinition(MappingContextIsNewStrategyFactory.class);
- mappingContextStrategyFactoryBuilder.addConstructorArgReference(mappingContextRef);
-
- BeanComponentDefinitionBuilder builder = new BeanComponentDefinitionBuilder(element, context);
- context.registerBeanComponent(builder.getComponent(mappingContextStrategyFactoryBuilder,
- IS_NEW_STRATEGY_FACTORY_BEAN_NAME));
-
- return IS_NEW_STRATEGY_FACTORY_BEAN_NAME;
- }
-
/**
* {@link TypeFilter} that returns {@literal false} in case any of the given delegates matches.
- *
+ *
* @author Oliver Gierke
*/
private static class NegatingFilter implements TypeFilter {
@@ -357,19 +384,18 @@ private static class NegatingFilter implements TypeFilter {
/**
* Creates a new {@link NegatingFilter} with the given delegates.
- *
+ *
* @param filters
*/
public NegatingFilter(TypeFilter... filters) {
- Assert.notNull(filters);
- this.delegates = new HashSet(Arrays.asList(filters));
+
+ Assert.notNull(filters, "TypeFilters must not be null");
+
+ this.delegates = Set.of(filters);
}
- /*
- * (non-Javadoc)
- * @see org.springframework.core.type.filter.TypeFilter#match(org.springframework.core.type.classreading.MetadataReader, org.springframework.core.type.classreading.MetadataReaderFactory)
- */
- public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
+ public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
+ throws IOException {
for (TypeFilter delegate : delegates) {
if (delegate.match(metadataReader, metadataReaderFactory)) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
index e4d6aedd78..4e05fe6c39 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingBeanDefinitionParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2012-2014 the original author or authors.
+ * Copyright 2012-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -18,6 +18,7 @@
import static org.springframework.data.config.ParsingUtils.*;
import static org.springframework.data.mongodb.config.BeanNames.*;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -26,40 +27,36 @@
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.data.auditing.config.IsNewAwareAuditingHandlerBeanDefinitionParser;
import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener;
+import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback;
+import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
import org.springframework.util.StringUtils;
+
import org.w3c.dom.Element;
/**
- * {@link BeanDefinitionParser} to register a {@link AuditingEventListener} to transparently set auditing information on
- * an entity.
- *
+ * {@link BeanDefinitionParser} to register a {@link AuditingEntityCallback} to transparently set auditing information
+ * on an entity.
+ *
* @author Oliver Gierke
+ * @author Mark Paluch
*/
public class MongoAuditingBeanDefinitionParser extends AbstractSingleBeanDefinitionParser {
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#getBeanClass(org.w3c.dom.Element)
- */
+ private static boolean PROJECT_REACTOR_AVAILABLE = ClassUtils.isPresent("reactor.core.publisher.Mono",
+ MongoAuditingRegistrar.class.getClassLoader());
+
@Override
protected Class> getBeanClass(Element element) {
- return AuditingEventListener.class;
+ return AuditingEntityCallback.class;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#shouldGenerateId()
- */
@Override
protected boolean shouldGenerateId() {
return true;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractSingleBeanDefinitionParser#doParse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext, org.springframework.beans.factory.support.BeanDefinitionBuilder)
- */
@Override
protected void doParse(Element element, ParserContext parserContext, BeanDefinitionBuilder builder) {
@@ -80,7 +77,24 @@ protected void doParse(Element element, ParserContext parserContext, BeanDefinit
mappingContextRef);
parser.parse(element, parserContext);
- builder.addConstructorArgValue(getObjectFactoryBeanDefinition(parser.getResolvedBeanName(),
- parserContext.extractSource(element)));
+ AbstractBeanDefinition isNewAwareAuditingHandler = getObjectFactoryBeanDefinition(parser.getResolvedBeanName(),
+ parserContext.extractSource(element));
+ builder.addConstructorArgValue(isNewAwareAuditingHandler);
+
+ if (PROJECT_REACTOR_AVAILABLE) {
+ registerReactiveAuditingEntityCallback(parserContext.getRegistry(), isNewAwareAuditingHandler,
+ parserContext.extractSource(element));
+ }
+ }
+
+ private void registerReactiveAuditingEntityCallback(BeanDefinitionRegistry registry,
+ AbstractBeanDefinition isNewAwareAuditingHandler, @Nullable Object source) {
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class);
+
+ builder.addConstructorArgValue(isNewAwareAuditingHandler);
+ builder.getRawBeanDefinition().setSource(source);
+
+ registry.registerBeanDefinition(ReactiveAuditingEntityCallback.class.getName(), builder.getBeanDefinition());
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java
index 5e64972830..37e509a38a 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoAuditingRegistrar.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2013-2014 the original author or authors.
+ * Copyright 2013-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -15,116 +15,74 @@
*/
package org.springframework.data.mongodb.config;
-import static org.springframework.beans.factory.config.BeanDefinition.*;
-import static org.springframework.data.mongodb.config.BeanNames.*;
-
import java.lang.annotation.Annotation;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
-import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
-import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.core.Ordered;
import org.springframework.data.auditing.IsNewAwareAuditingHandler;
import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
import org.springframework.data.auditing.config.AuditingConfiguration;
import org.springframework.data.config.ParsingUtils;
-import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
-import org.springframework.data.mongodb.core.mapping.event.AuditingEventListener;
-import org.springframework.data.support.IsNewStrategyFactory;
+import org.springframework.data.mongodb.core.mapping.event.AuditingEntityCallback;
import org.springframework.util.Assert;
/**
* {@link ImportBeanDefinitionRegistrar} to enable {@link EnableMongoAuditing} annotation.
- *
+ *
* @author Thomas Darimont
* @author Oliver Gierke
+ * @author Mark Paluch
+ * @author Christoph Strobl
*/
-class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
+class MongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport implements Ordered {
- /*
- * (non-Javadoc)
- * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAnnotation()
- */
@Override
protected Class extends Annotation> getAnnotation() {
return EnableMongoAuditing.class;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditingHandlerBeanName()
- */
@Override
protected String getAuditingHandlerBeanName() {
return "mongoAuditingHandler";
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerBeanDefinitions(org.springframework.core.type.AnnotationMetadata, org.springframework.beans.factory.support.BeanDefinitionRegistry)
- */
@Override
- public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
-
- Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null!");
- Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
+ protected void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration,
+ BeanDefinitionRegistry registry) {
- defaultDependenciesIfNecessary(registry, annotationMetadata);
- super.registerBeanDefinitions(annotationMetadata, registry);
+ builder.setFactoryMethod("from").addConstructorArgReference("mongoMappingContext");
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#getAuditHandlerBeanDefinitionBuilder(org.springframework.data.auditing.config.AuditingConfiguration)
- */
@Override
protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) {
- Assert.notNull(configuration, "AuditingConfiguration must not be null!");
+ Assert.notNull(configuration, "AuditingConfiguration must not be null");
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class);
- builder.addConstructorArgReference(MAPPING_CONTEXT_BEAN_NAME);
- return configureDefaultAuditHandlerAttributes(configuration, builder);
+ return configureDefaultAuditHandlerAttributes(configuration,
+ BeanDefinitionBuilder.rootBeanDefinition(IsNewAwareAuditingHandler.class));
}
- /*
- * (non-Javadoc)
- * @see org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport#registerAuditListener(org.springframework.beans.factory.config.BeanDefinition, org.springframework.beans.factory.support.BeanDefinitionRegistry)
- */
@Override
protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition,
BeanDefinitionRegistry registry) {
- Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null!");
- Assert.notNull(registry, "BeanDefinitionRegistry must not be null!");
+ Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null");
+ Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
BeanDefinitionBuilder listenerBeanDefinitionBuilder = BeanDefinitionBuilder
- .rootBeanDefinition(AuditingEventListener.class);
- listenerBeanDefinitionBuilder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(
- getAuditingHandlerBeanName(), registry));
+ .rootBeanDefinition(AuditingEntityCallback.class);
+ listenerBeanDefinitionBuilder
+ .addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
registerInfrastructureBeanWithId(listenerBeanDefinitionBuilder.getBeanDefinition(),
- AuditingEventListener.class.getName(), registry);
+ AuditingEntityCallback.class.getName(), registry);
}
- /**
- * Register default bean definitions for a {@link MongoMappingContext} and an {@link IsNewStrategyFactory} in case we
- * don't find beans with the assumed names in the registry.
- *
- * @param registry the {@link BeanDefinitionRegistry} to use to register the components into.
- * @param source the source which the registered components shall be registered with
- */
- private void defaultDependenciesIfNecessary(BeanDefinitionRegistry registry, Object source) {
-
- if (!registry.containsBeanDefinition(MAPPING_CONTEXT_BEAN_NAME)) {
-
- RootBeanDefinition definition = new RootBeanDefinition(MongoMappingContext.class);
- definition.setRole(ROLE_INFRASTRUCTURE);
- definition.setSource(source);
-
- registry.registerBeanDefinition(MAPPING_CONTEXT_BEAN_NAME, definition);
- }
+ @Override
+ public int getOrder() {
+ return Ordered.LOWEST_PRECEDENCE;
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoClientParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoClientParser.java
index 5bceb62b50..501c00b9d6 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoClientParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoClientParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -29,16 +29,12 @@
/**
* Parser for {@code mongo-client} definitions.
- *
+ *
* @author Christoph Strobl
* @since 1.7
*/
public class MongoClientParser implements BeanDefinitionParser {
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.BeanDefinitionParser#parse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
- */
public BeanDefinition parse(Element element, ParserContext parserContext) {
Object source = parserContext.extractSource(element);
@@ -50,10 +46,11 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
ParsingUtils.setPropertyValue(builder, element, "port", "port");
ParsingUtils.setPropertyValue(builder, element, "host", "host");
- ParsingUtils.setPropertyValue(builder, element, "credentials", "credentials");
+ ParsingUtils.setPropertyValue(builder, element, "credential", "credential");
+ ParsingUtils.setPropertyValue(builder, element, "replica-set", "replicaSet");
+ ParsingUtils.setPropertyValue(builder, element, "connection-string", "connectionString");
- MongoParsingUtils.parseMongoClientOptions(element, builder);
- MongoParsingUtils.parseReplicaSet(element, builder);
+ MongoParsingUtils.parseMongoClientSettings(element, builder);
String defaultedId = StringUtils.hasText(id) ? id : BeanNames.MONGO_BEAN_NAME;
@@ -62,22 +59,34 @@ public BeanDefinition parse(Element element, ParserContext parserContext) {
BeanComponentDefinition mongoComponent = helper.getComponent(builder, defaultedId);
parserContext.registerBeanComponent(mongoComponent);
- BeanComponentDefinition serverAddressPropertyEditor = helper.getComponent(MongoParsingUtils
- .getServerAddressPropertyEditorBuilder());
+ BeanComponentDefinition connectionStringPropertyEditor = helper
+ .getComponent(MongoParsingUtils.getConnectionStringPropertyEditorBuilder());
+ parserContext.registerBeanComponent(connectionStringPropertyEditor);
+
+ BeanComponentDefinition serverAddressPropertyEditor = helper
+ .getComponent(MongoParsingUtils.getServerAddressPropertyEditorBuilder());
parserContext.registerBeanComponent(serverAddressPropertyEditor);
- BeanComponentDefinition writeConcernEditor = helper.getComponent(MongoParsingUtils
- .getWriteConcernPropertyEditorBuilder());
+ BeanComponentDefinition writeConcernEditor = helper
+ .getComponent(MongoParsingUtils.getWriteConcernPropertyEditorBuilder());
parserContext.registerBeanComponent(writeConcernEditor);
- BeanComponentDefinition readPreferenceEditor = helper.getComponent(MongoParsingUtils
- .getReadPreferencePropertyEditorBuilder());
+ BeanComponentDefinition readConcernEditor = helper
+ .getComponent(MongoParsingUtils.getReadConcernPropertyEditorBuilder());
+ parserContext.registerBeanComponent(readConcernEditor);
+
+ BeanComponentDefinition readPreferenceEditor = helper
+ .getComponent(MongoParsingUtils.getReadPreferencePropertyEditorBuilder());
parserContext.registerBeanComponent(readPreferenceEditor);
- BeanComponentDefinition credentialsEditor = helper.getComponent(MongoParsingUtils
- .getMongoCredentialPropertyEditor());
+ BeanComponentDefinition credentialsEditor = helper
+ .getComponent(MongoParsingUtils.getMongoCredentialPropertyEditor());
parserContext.registerBeanComponent(credentialsEditor);
+ BeanComponentDefinition uuidRepresentationEditor = helper
+ .getComponent(MongoParsingUtils.getUUidRepresentationEditorBuilder());
+ parserContext.registerBeanComponent(uuidRepresentationEditor);
+
parserContext.popAndRegisterContainingComponent();
return mongoComponent.getBeanDefinition();
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java
new file mode 100644
index 0000000000..0594f6176c
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoConfigurationSupport.java
@@ -0,0 +1,240 @@
+/*
+ * Copyright 2016-2025 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.config;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.bson.UuidRepresentation;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
+import org.springframework.core.convert.converter.Converter;
+import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.data.convert.CustomConversions;
+import org.springframework.data.mapping.model.CamelCaseAbbreviatingFieldNamingStrategy;
+import org.springframework.data.mapping.model.FieldNamingStrategy;
+import org.springframework.data.mapping.model.PropertyNameFieldNamingStrategy;
+import org.springframework.data.mongodb.MongoManagedTypes;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions;
+import org.springframework.data.mongodb.core.convert.MongoCustomConversions.MongoConverterConfigurationAdapter;
+import org.springframework.data.mongodb.core.mapping.Document;
+import org.springframework.data.mongodb.core.mapping.MongoMappingContext;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.MongoClientSettings;
+import com.mongodb.MongoClientSettings.Builder;
+
+/**
+ * Base class for Spring Data MongoDB to be extended for JavaConfiguration usage.
+ *
+ * @author Mark Paluch
+ * @since 2.0
+ */
+public abstract class MongoConfigurationSupport {
+
+ /**
+ * Return the name of the database to connect to.
+ *
+ * @return must not be {@literal null}.
+ */
+ protected abstract String getDatabaseName();
+
+ /**
+ * Returns the base packages to scan for MongoDB mapped entities at startup. Will return the package name of the
+ * configuration class' (the concrete class, not this one here) by default. So if you have a
+ * {@code com.acme.AppConfig} extending {@link MongoConfigurationSupport} the base package will be considered
+ * {@code com.acme} unless the method is overridden to implement alternate behavior.
+ *
+ * @return the base packages to scan for mapped {@link Document} classes or an empty collection to not enable scanning
+ * for entities.
+ * @since 1.10
+ */
+ protected Collection getMappingBasePackages() {
+
+ Package mappingBasePackage = getClass().getPackage();
+ return Collections.singleton(mappingBasePackage == null ? null : mappingBasePackage.getName());
+ }
+
+ /**
+ * Creates a {@link MongoMappingContext} equipped with entity classes scanned from the mapping base package.
+ *
+ * @see #getMappingBasePackages()
+ * @return
+ */
+ @Bean
+ public MongoMappingContext mongoMappingContext(MongoCustomConversions customConversions,
+ MongoManagedTypes mongoManagedTypes) {
+
+ MongoMappingContext mappingContext = new MongoMappingContext();
+ mappingContext.setManagedTypes(mongoManagedTypes);
+ mappingContext.setSimpleTypeHolder(customConversions.getSimpleTypeHolder());
+ mappingContext.setFieldNamingStrategy(fieldNamingStrategy());
+ mappingContext.setAutoIndexCreation(autoIndexCreation());
+
+ return mappingContext;
+ }
+
+ /**
+ * @return new instance of {@link MongoManagedTypes}.
+ * @throws ClassNotFoundException
+ * @since 4.0
+ */
+ @Bean
+ public MongoManagedTypes mongoManagedTypes() throws ClassNotFoundException {
+ return MongoManagedTypes.fromIterable(getInitialEntitySet());
+ }
+
+ /**
+ * Register custom {@link Converter}s in a {@link CustomConversions} object if required. These
+ * {@link CustomConversions} will be registered with the
+ * {@link org.springframework.data.mongodb.core.convert.MappingMongoConverter} and {@link MongoMappingContext}.
+ * Returns an empty {@link MongoCustomConversions} instance by default.
+ *
+ * NOTE: Use {@link #configureConverters(MongoConverterConfigurationAdapter)} to configure MongoDB
+ * native simple types and register custom {@link Converter converters}.
+ *
+ * @return must not be {@literal null}.
+ */
+ @Bean
+ public MongoCustomConversions customConversions() {
+ return MongoCustomConversions.create(this::configureConverters);
+ }
+
+ /**
+ * Configuration hook for {@link MongoCustomConversions} creation.
+ *
+ * @param converterConfigurationAdapter never {@literal null}.
+ * @since 2.3
+ * @see MongoConverterConfigurationAdapter#useNativeDriverJavaTimeCodecs()
+ * @see MongoConverterConfigurationAdapter#useSpringDataJavaTimeCodecs()
+ */
+ protected void configureConverters(MongoConverterConfigurationAdapter converterConfigurationAdapter) {
+
+ }
+
+ /**
+ * Scans the mapping base package for classes annotated with {@link Document}. By default, it scans for entities in
+ * all packages returned by {@link #getMappingBasePackages()}.
+ *
+ * @see #getMappingBasePackages()
+ * @return
+ * @throws ClassNotFoundException
+ */
+ protected Set> getInitialEntitySet() throws ClassNotFoundException {
+
+ Set> initialEntitySet = new HashSet>();
+
+ for (String basePackage : getMappingBasePackages()) {
+ initialEntitySet.addAll(scanForEntities(basePackage));
+ }
+
+ return initialEntitySet;
+ }
+
+ /**
+ * Scans the given base package for entities, i.e. MongoDB specific types annotated with {@link Document}.
+ *
+ * @param basePackage must not be {@literal null}.
+ * @return
+ * @throws ClassNotFoundException
+ * @since 1.10
+ */
+ protected Set> scanForEntities(String basePackage) throws ClassNotFoundException {
+
+ if (!StringUtils.hasText(basePackage)) {
+ return Collections.emptySet();
+ }
+
+ Set> initialEntitySet = new HashSet>();
+
+ if (StringUtils.hasText(basePackage)) {
+
+ ClassPathScanningCandidateComponentProvider componentProvider = new ClassPathScanningCandidateComponentProvider(
+ false);
+ componentProvider.addIncludeFilter(new AnnotationTypeFilter(Document.class));
+
+ for (BeanDefinition candidate : componentProvider.findCandidateComponents(basePackage)) {
+
+ initialEntitySet
+ .add(ClassUtils.forName(candidate.getBeanClassName(), MongoConfigurationSupport.class.getClassLoader()));
+ }
+ }
+
+ return initialEntitySet;
+ }
+
+ /**
+ * Configures whether to abbreviate field names for domain objects by configuring a
+ * {@link CamelCaseAbbreviatingFieldNamingStrategy} on the {@link MongoMappingContext} instance created.
+ *
+ * @return
+ */
+ protected boolean abbreviateFieldNames() {
+ return false;
+ }
+
+ /**
+ * Configures a {@link FieldNamingStrategy} on the {@link MongoMappingContext} instance created.
+ *
+ * @return
+ * @since 1.5
+ */
+ protected FieldNamingStrategy fieldNamingStrategy() {
+ return abbreviateFieldNames() ? new CamelCaseAbbreviatingFieldNamingStrategy()
+ : PropertyNameFieldNamingStrategy.INSTANCE;
+ }
+
+ /**
+ * Configure whether to automatically create indices for domain types by deriving the
+ * {@link org.springframework.data.mongodb.core.index.IndexDefinition} from the entity or not.
+ *
+ * @return {@literal false} by default.
+ * INFO: As of 3.x the default is set to {@literal false}; In 2.x it was {@literal true}.
+ * @since 2.2
+ */
+ protected boolean autoIndexCreation() {
+ return false;
+ }
+
+ /**
+ * Return the {@link MongoClientSettings} used to create the actual {@literal MongoClient}.
+ * Override either this method, or use {@link #configureClientSettings(Builder)} to alter the setup.
+ *
+ * @return never {@literal null}.
+ * @since 3.0
+ */
+ protected MongoClientSettings mongoClientSettings() {
+
+ MongoClientSettings.Builder builder = MongoClientSettings.builder();
+ builder.uuidRepresentation(UuidRepresentation.JAVA_LEGACY);
+ configureClientSettings(builder);
+ return builder.build();
+ }
+
+ /**
+ * Configure {@link MongoClientSettings} via its {@link Builder} API.
+ *
+ * @param builder never {@literal null}.
+ * @since 3.0
+ */
+ protected void configureClientSettings(MongoClientSettings.Builder builder) {
+ // customization hook
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
index a008d1a6b0..b8f23a35af 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoCredentialPropertyEditor.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -16,6 +16,9 @@
package org.springframework.data.mongodb.config;
import java.beans.PropertyEditorSupport;
+import java.lang.reflect.Method;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -23,15 +26,19 @@
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;
import com.mongodb.MongoCredential;
/**
* Parse a {@link String} to a Collection of {@link MongoCredential}.
- *
+ *
* @author Christoph Strobl
* @author Oliver Gierke
+ * @author Stephen Tyler Conrad
+ * @author Mark Paluch
* @since 1.7
*/
public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
@@ -39,23 +46,19 @@ public class MongoCredentialPropertyEditor extends PropertyEditorSupport {
private static final Pattern GROUP_PATTERN = Pattern.compile("(\\\\?')(.*?)\\1");
private static final String AUTH_MECHANISM_KEY = "uri.authMechanism";
- private static final String USERNAME_PASSWORD_DELIMINATOR = ":";
- private static final String DATABASE_DELIMINATOR = "@";
- private static final String OPTIONS_DELIMINATOR = "?";
- private static final String OPTION_VALUE_DELIMINATOR = "&";
-
- /*
- * (non-Javadoc)
- * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
- */
+ private static final String USERNAME_PASSWORD_DELIMITER = ":";
+ private static final String DATABASE_DELIMITER = "@";
+ private static final String OPTIONS_DELIMITER = "?";
+ private static final String OPTION_VALUE_DELIMITER = "&";
+
@Override
- public void setAsText(String text) throws IllegalArgumentException {
+ public void setAsText(@Nullable String text) throws IllegalArgumentException {
if (!StringUtils.hasText(text)) {
return;
}
- List credentials = new ArrayList();
+ List credentials = new ArrayList<>();
for (String credentialString : extractCredentialsString(text)) {
@@ -73,12 +76,23 @@ public void setAsText(String text) throws IllegalArgumentException {
verifyUserNamePresent(userNameAndPassword);
credentials.add(MongoCredential.createGSSAPICredential(userNameAndPassword[0]));
- } else if (MongoCredential.MONGODB_CR_MECHANISM.equals(authMechanism)) {
+ } else if ("MONGODB-CR".equals(authMechanism)) {
verifyUsernameAndPasswordPresent(userNameAndPassword);
verifyDatabasePresent(database);
- credentials.add(MongoCredential.createMongoCRCredential(userNameAndPassword[0], database,
- userNameAndPassword[1].toCharArray()));
+
+ Method createCRCredentialMethod = ReflectionUtils.findMethod(MongoCredential.class,
+ "createMongoCRCredential", String.class, String.class, char[].class);
+
+ if (createCRCredentialMethod == null) {
+ throw new IllegalArgumentException("MONGODB-CR is no longer supported.");
+ }
+
+ MongoCredential credential = MongoCredential.class
+ .cast(ReflectionUtils.invokeMethod(createCRCredentialMethod, null, userNameAndPassword[0], database,
+ userNameAndPassword[1].toCharArray()));
+ credentials.add(credential);
+
} else if (MongoCredential.MONGODB_X509_MECHANISM.equals(authMechanism)) {
verifyUserNamePresent(userNameAndPassword);
@@ -95,9 +109,15 @@ public void setAsText(String text) throws IllegalArgumentException {
verifyDatabasePresent(database);
credentials.add(MongoCredential.createScramSha1Credential(userNameAndPassword[0], database,
userNameAndPassword[1].toCharArray()));
+ } else if (MongoCredential.SCRAM_SHA_256_MECHANISM.equals(authMechanism)) {
+
+ verifyUsernameAndPasswordPresent(userNameAndPassword);
+ verifyDatabasePresent(database);
+ credentials.add(MongoCredential.createScramSha256Credential(userNameAndPassword[0], database,
+ userNameAndPassword[1].toCharArray()));
} else {
throw new IllegalArgumentException(
- String.format("Cannot create MongoCredentials for unknown auth mechanism '%s'!", authMechanism));
+ String.format("Cannot create MongoCredentials for unknown auth mechanism '%s'", authMechanism));
}
}
} else {
@@ -115,7 +135,7 @@ public void setAsText(String text) throws IllegalArgumentException {
private List extractCredentialsString(String source) {
Matcher matcher = GROUP_PATTERN.matcher(source);
- List list = new ArrayList();
+ List list = new ArrayList<>();
while (matcher.find()) {
@@ -132,40 +152,51 @@ private List extractCredentialsString(String source) {
private static String[] extractUserNameAndPassword(String text) {
- int index = text.lastIndexOf(DATABASE_DELIMINATOR);
+ int index = text.lastIndexOf(DATABASE_DELIMITER);
+
+ index = index != -1 ? index : text.lastIndexOf(OPTIONS_DELIMITER);
- index = index != -1 ? index : text.lastIndexOf(OPTIONS_DELIMINATOR);
+ if (index == -1) {
+ return new String[] {};
+ }
- return index == -1 ? new String[] {} : text.substring(0, index).split(USERNAME_PASSWORD_DELIMINATOR);
+ return Arrays.stream(text.substring(0, index).split(USERNAME_PASSWORD_DELIMITER))
+ .map(MongoCredentialPropertyEditor::decodeParameter).toArray(String[]::new);
}
private static String extractDB(String text) {
- int dbSeperationIndex = text.lastIndexOf(DATABASE_DELIMINATOR);
+ int dbSeparationIndex = text.lastIndexOf(DATABASE_DELIMITER);
- if (dbSeperationIndex == -1) {
+ if (dbSeparationIndex == -1) {
return "";
}
- String tmp = text.substring(dbSeperationIndex + 1);
- int optionsSeperationIndex = tmp.lastIndexOf(OPTIONS_DELIMINATOR);
+ String tmp = text.substring(dbSeparationIndex + 1);
+ int optionsSeparationIndex = tmp.lastIndexOf(OPTIONS_DELIMITER);
- return optionsSeperationIndex > -1 ? tmp.substring(0, optionsSeperationIndex) : tmp;
+ return optionsSeparationIndex > -1 ? tmp.substring(0, optionsSeparationIndex) : tmp;
}
private static Properties extractOptions(String text) {
- int optionsSeperationIndex = text.lastIndexOf(OPTIONS_DELIMINATOR);
- int dbSeperationIndex = text.lastIndexOf(OPTIONS_DELIMINATOR);
+ int optionsSeparationIndex = text.lastIndexOf(OPTIONS_DELIMITER);
+ int dbSeparationIndex = text.lastIndexOf(DATABASE_DELIMITER);
- if (optionsSeperationIndex == -1 || dbSeperationIndex > optionsSeperationIndex) {
+ if (optionsSeparationIndex == -1 || dbSeparationIndex > optionsSeparationIndex) {
return new Properties();
}
Properties properties = new Properties();
- for (String option : text.substring(optionsSeperationIndex + 1).split(OPTION_VALUE_DELIMINATOR)) {
+ for (String option : text.substring(optionsSeparationIndex + 1).split(OPTION_VALUE_DELIMITER)) {
+
String[] optionArgs = option.split("=");
+
+ if (optionArgs.length == 1) {
+ throw new IllegalArgumentException(String.format("Query parameter '%s' has no value", optionArgs[0]));
+ }
+
properties.put(optionArgs[0], optionArgs[1]);
}
@@ -178,21 +209,25 @@ private static void verifyUsernameAndPasswordPresent(String[] source) {
if (source.length != 2) {
throw new IllegalArgumentException(
- "Credentials need to specify username and password like in 'username:password@database'!");
+ "Credentials need to specify username and password like in 'username:password@database'");
}
}
private static void verifyDatabasePresent(String source) {
if (!StringUtils.hasText(source)) {
- throw new IllegalArgumentException("Credentials need to specify database like in 'username:password@database'!");
+ throw new IllegalArgumentException("Credentials need to specify database like in 'username:password@database'");
}
}
private static void verifyUserNamePresent(String[] source) {
if (source.length == 0 || !StringUtils.hasText(source[0])) {
- throw new IllegalArgumentException("Credentials need to specify username!");
+ throw new IllegalArgumentException("Credentials need to specify username");
}
}
+
+ private static String decodeParameter(String it) {
+ return URLDecoder.decode(it, StandardCharsets.UTF_8);
+ }
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
index 8e1245984f..2e733cc79f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoDbFactoryParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2015 by the original author(s).
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -18,8 +18,6 @@
import static org.springframework.data.config.ParsingUtils.*;
import static org.springframework.data.mongodb.config.MongoParsingUtils.*;
-import java.util.Collections;
-import java.util.HashSet;
import java.util.Set;
import org.springframework.beans.factory.BeanDefinitionStoreException;
@@ -30,43 +28,29 @@
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.BeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
-import org.springframework.data.authentication.UserCredentials;
import org.springframework.data.config.BeanComponentDefinitionBuilder;
-import org.springframework.data.mongodb.core.MongoFactoryBean;
-import org.springframework.data.mongodb.core.SimpleMongoDbFactory;
+import org.springframework.data.mongodb.core.MongoClientFactoryBean;
+import org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory;
+import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
import org.w3c.dom.Element;
-import com.mongodb.Mongo;
-import com.mongodb.MongoClientURI;
-import com.mongodb.MongoURI;
+import com.mongodb.ConnectionString;
/**
* {@link BeanDefinitionParser} to parse {@code db-factory} elements into {@link BeanDefinition}s.
- *
+ *
* @author Jon Brisbin
* @author Oliver Gierke
* @author Thomas Darimont
* @author Christoph Strobl
* @author Viktor Khoroshko
+ * @author Mark Paluch
*/
public class MongoDbFactoryParser extends AbstractBeanDefinitionParser {
- private static final Set MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES;
-
- static {
-
- Set mongoUriAllowedAdditionalAttributes = new HashSet();
- mongoUriAllowedAdditionalAttributes.add("id");
- mongoUriAllowedAdditionalAttributes.add("write-concern");
+ private static final Set MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES = Set.of("id", "write-concern");
- MONGO_URI_ALLOWED_ADDITIONAL_ATTRIBUTES = Collections.unmodifiableSet(mongoUriAllowedAdditionalAttributes);
- }
-
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
@@ -75,18 +59,15 @@ protected String resolveId(Element element, AbstractBeanDefinition definition, P
return StringUtils.hasText(id) ? id : BeanNames.DB_FACTORY_BEAN_NAME;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parseInternal(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
// Common setup
- BeanDefinitionBuilder dbFactoryBuilder = BeanDefinitionBuilder.genericBeanDefinition(SimpleMongoDbFactory.class);
+ BeanDefinitionBuilder dbFactoryBuilder = BeanDefinitionBuilder
+ .genericBeanDefinition(SimpleMongoClientDatabaseFactory.class);
setPropertyValue(dbFactoryBuilder, element, "write-concern", "writeConcern");
- BeanDefinition mongoUri = getMongoUri(element, parserContext);
+ BeanDefinition mongoUri = getConnectionString(element, parserContext);
if (mongoUri != null) {
@@ -96,10 +77,9 @@ protected AbstractBeanDefinition parseInternal(Element element, ParserContext pa
BeanComponentDefinitionBuilder helper = new BeanComponentDefinitionBuilder(element, parserContext);
- String mongoRef = element.getAttribute("mongo-ref");
- String dbname = element.getAttribute("dbname");
+ String mongoRef = element.getAttribute("mongo-client-ref");
- BeanDefinition userCredentials = getUserCredentialsBeanDefinition(element, parserContext);
+ String dbname = element.getAttribute("dbname");
// Defaulting
if (StringUtils.hasText(mongoRef)) {
@@ -109,8 +89,6 @@ protected AbstractBeanDefinition parseInternal(Element element, ParserContext pa
}
dbFactoryBuilder.addConstructorArgValue(StringUtils.hasText(dbname) ? dbname : "db");
- dbFactoryBuilder.addConstructorArgValue(userCredentials);
- dbFactoryBuilder.addConstructorArgValue(element.getAttribute("authentication-dbname"));
BeanDefinitionBuilder writeConcernPropertyEditorBuilder = getWriteConcernPropertyEditorBuilder();
@@ -122,16 +100,16 @@ protected AbstractBeanDefinition parseInternal(Element element, ParserContext pa
}
/**
- * Registers a default {@link BeanDefinition} of a {@link Mongo} instance and returns the name under which the
- * {@link Mongo} instance was registered under.
- *
+ * Registers a default {@link BeanDefinition} of a {@link com.mongodb.client.MongoClient} instance and returns the
+ * name under which the {@link com.mongodb.client.MongoClient} instance was registered under.
+ *
* @param element must not be {@literal null}.
* @param parserContext must not be {@literal null}.
* @return
*/
private BeanDefinition registerMongoBeanDefinition(Element element, ParserContext parserContext) {
- BeanDefinitionBuilder mongoBuilder = BeanDefinitionBuilder.genericBeanDefinition(MongoFactoryBean.class);
+ BeanDefinitionBuilder mongoBuilder = BeanDefinitionBuilder.genericBeanDefinition(MongoClientFactoryBean.class);
setPropertyValue(mongoBuilder, element, "host");
setPropertyValue(mongoBuilder, element, "port");
@@ -139,42 +117,28 @@ private BeanDefinition registerMongoBeanDefinition(Element element, ParserContex
}
/**
- * Returns a {@link BeanDefinition} for a {@link UserCredentials} object.
- *
- * @param element
- * @return the {@link BeanDefinition} or {@literal null} if neither username nor password given.
- */
- private BeanDefinition getUserCredentialsBeanDefinition(Element element, ParserContext context) {
-
- String username = element.getAttribute("username");
- String password = element.getAttribute("password");
-
- if (!StringUtils.hasText(username) && !StringUtils.hasText(password)) {
- return null;
- }
-
- BeanDefinitionBuilder userCredentialsBuilder = BeanDefinitionBuilder.genericBeanDefinition(UserCredentials.class);
- userCredentialsBuilder.addConstructorArgValue(StringUtils.hasText(username) ? username : null);
- userCredentialsBuilder.addConstructorArgValue(StringUtils.hasText(password) ? password : null);
-
- return getSourceBeanDefinition(userCredentialsBuilder, context, element);
- }
-
- /**
- * Creates a {@link BeanDefinition} for a {@link MongoURI} or {@link MongoClientURI} depending on configured
- * attributes.
+ * Creates a {@link BeanDefinition} for a {@link ConnectionString} depending on configured attributes.
* Errors when configured element contains {@literal uri} or {@literal client-uri} along with other attributes except
* {@literal write-concern} and/or {@literal id}.
- *
+ *
* @param element must not be {@literal null}.
* @param parserContext
* @return {@literal null} in case no client-/uri defined.
*/
- private BeanDefinition getMongoUri(Element element, ParserContext parserContext) {
+ @Nullable
+ private BeanDefinition getConnectionString(Element element, ParserContext parserContext) {
- boolean hasClientUri = element.hasAttribute("client-uri");
+ String type = null;
- if (!hasClientUri && !element.hasAttribute("uri")) {
+ if (element.hasAttribute("client-uri")) {
+ type = "client-uri";
+ } else if (element.hasAttribute("connection-string")) {
+ type = "connection-string";
+ } else if (element.hasAttribute("uri")) {
+ type = "uri";
+ }
+
+ if (!StringUtils.hasText(type)) {
return null;
}
@@ -188,16 +152,12 @@ private BeanDefinition getMongoUri(Element element, ParserContext parserContext)
if (element.getAttributes().getLength() > allowedAttributesCount) {
- parserContext.getReaderContext().error(
- "Configure either " + (hasClientUri ? "Mongo Client URI" : "Mongo URI") + " or details individually!",
+ parserContext.getReaderContext().error("Configure either MongoDB " + type + " or details individually",
parserContext.extractSource(element));
}
- Class> type = hasClientUri ? MongoClientURI.class : MongoURI.class;
- String uri = hasClientUri ? element.getAttribute("client-uri") : element.getAttribute("uri");
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(type);
- builder.addConstructorArgValue(uri);
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(ConnectionString.class);
+ builder.addConstructorArgValue(element.getAttribute(type));
return builder.getBeanDefinition();
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java
index f32a16d542..af1ffbbb02 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoJmxParser.java
@@ -1,69 +1,78 @@
-/*
- * Copyright 2011 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
- *
- * http://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.config;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.parsing.BeanComponentDefinition;
-import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
-import org.springframework.beans.factory.support.BeanDefinitionBuilder;
-import org.springframework.beans.factory.xml.BeanDefinitionParser;
-import org.springframework.beans.factory.xml.ParserContext;
-import org.springframework.data.mongodb.core.MongoAdmin;
-import org.springframework.data.mongodb.monitor.*;
-import org.springframework.util.StringUtils;
-import org.w3c.dom.Element;
-
-public class MongoJmxParser implements BeanDefinitionParser {
-
- public BeanDefinition parse(Element element, ParserContext parserContext) {
- String name = element.getAttribute("mongo-ref");
- if (!StringUtils.hasText(name)) {
- name = "mongo";
- }
- registerJmxComponents(name, element, parserContext);
- return null;
- }
-
- protected void registerJmxComponents(String mongoRefName, Element element, ParserContext parserContext) {
- Object eleSource = parserContext.extractSource(element);
-
- CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
-
- createBeanDefEntry(AssertMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(BackgroundFlushingMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(BtreeIndexCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(ConnectionMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(GlobalLockMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(MemoryMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(OperationCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(ServerInfo.class, compositeDef, mongoRefName, eleSource, parserContext);
- createBeanDefEntry(MongoAdmin.class, compositeDef, mongoRefName, eleSource, parserContext);
-
- parserContext.registerComponent(compositeDef);
-
- }
-
- protected void createBeanDefEntry(Class> clazz, CompositeComponentDefinition compositeDef, String mongoRefName,
- Object eleSource, ParserContext parserContext) {
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
- builder.getRawBeanDefinition().setSource(eleSource);
- builder.addConstructorArgReference(mongoRefName);
- BeanDefinition assertDef = builder.getBeanDefinition();
- String assertName = parserContext.getReaderContext().registerWithGeneratedName(assertDef);
- compositeDef.addNestedComponent(new BeanComponentDefinition(assertDef, assertName));
- }
-
-}
+/*
+ * Copyright 2011-2025 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.config;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.parsing.BeanComponentDefinition;
+import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.beans.factory.xml.ParserContext;
+import org.springframework.data.mongodb.core.MongoAdmin;
+import org.springframework.data.mongodb.monitor.*;
+import org.springframework.util.StringUtils;
+import org.w3c.dom.Element;
+
+/**
+ * @author Mark Pollack
+ * @author Thomas Risberg
+ * @author John Brisbin
+ * @author Oliver Gierke
+ * @author Christoph Strobl
+ * @deprecated since 4.5
+ */
+@Deprecated(since = "4.5", forRemoval = true)
+public class MongoJmxParser implements BeanDefinitionParser {
+
+ public BeanDefinition parse(Element element, ParserContext parserContext) {
+ String name = element.getAttribute("mongo-ref");
+ if (!StringUtils.hasText(name)) {
+ name = BeanNames.MONGO_BEAN_NAME;
+ }
+ registerJmxComponents(name, element, parserContext);
+ return null;
+ }
+
+ protected void registerJmxComponents(String mongoRefName, Element element, ParserContext parserContext) {
+ Object eleSource = parserContext.extractSource(element);
+
+ CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), eleSource);
+
+ createBeanDefEntry(AssertMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(BackgroundFlushingMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(BtreeIndexCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(ConnectionMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(GlobalLockMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(MemoryMetrics.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(OperationCounters.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(ServerInfo.class, compositeDef, mongoRefName, eleSource, parserContext);
+ createBeanDefEntry(MongoAdmin.class, compositeDef, mongoRefName, eleSource, parserContext);
+
+ parserContext.registerComponent(compositeDef);
+
+ }
+
+ protected void createBeanDefEntry(Class> clazz, CompositeComponentDefinition compositeDef, String mongoRefName,
+ Object eleSource, ParserContext parserContext) {
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(clazz);
+ builder.getRawBeanDefinition().setSource(eleSource);
+ builder.addConstructorArgReference(mongoRefName);
+ BeanDefinition assertDef = builder.getBeanDefinition();
+ String assertName = parserContext.getReaderContext().registerWithGeneratedName(assertDef);
+ compositeDef.addNestedComponent(new BeanComponentDefinition(assertDef, assertName));
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
index 377eedb446..47519ca615 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoNamespaceHandler.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2015 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -19,21 +19,16 @@
/**
* {@link org.springframework.beans.factory.xml.NamespaceHandler} for Mongo DB configuration.
- *
+ *
* @author Oliver Gierke
* @author Martin Baumgartner
* @author Christoph Strobl
*/
public class MongoNamespaceHandler extends NamespaceHandlerSupport {
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.NamespaceHandler#init()
- */
public void init() {
registerBeanDefinitionParser("mapping-converter", new MappingMongoConverterParser());
- registerBeanDefinitionParser("mongo", new MongoParser());
registerBeanDefinitionParser("mongo-client", new MongoClientParser());
registerBeanDefinitionParser("db-factory", new MongoDbFactoryParser());
registerBeanDefinitionParser("jmx", new MongoJmxParser());
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParser.java
deleted file mode 100644
index 604214f261..0000000000
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParser.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * Copyright 2011-2015 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
- *
- * http://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.config;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.parsing.BeanComponentDefinition;
-import org.springframework.beans.factory.parsing.CompositeComponentDefinition;
-import org.springframework.beans.factory.support.BeanDefinitionBuilder;
-import org.springframework.beans.factory.xml.BeanDefinitionParser;
-import org.springframework.beans.factory.xml.ParserContext;
-import org.springframework.data.config.BeanComponentDefinitionBuilder;
-import org.springframework.data.config.ParsingUtils;
-import org.springframework.data.mongodb.core.MongoFactoryBean;
-import org.springframework.util.StringUtils;
-import org.w3c.dom.Element;
-
-/**
- * Parser for <mongo;gt; definitions.
- *
- * @author Mark Pollack
- * @author Oliver Gierke
- * @author Christoph Strobl
- */
-public class MongoParser implements BeanDefinitionParser {
-
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.BeanDefinitionParser#parse(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
- */
- public BeanDefinition parse(Element element, ParserContext parserContext) {
-
- Object source = parserContext.extractSource(element);
- String id = element.getAttribute("id");
-
- BeanComponentDefinitionBuilder helper = new BeanComponentDefinitionBuilder(element, parserContext);
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MongoFactoryBean.class);
- ParsingUtils.setPropertyValue(builder, element, "port", "port");
- ParsingUtils.setPropertyValue(builder, element, "host", "host");
- ParsingUtils.setPropertyValue(builder, element, "write-concern", "writeConcern");
-
- MongoParsingUtils.parseMongoOptions(element, builder);
- MongoParsingUtils.parseReplicaSet(element, builder);
-
- String defaultedId = StringUtils.hasText(id) ? id : BeanNames.MONGO_BEAN_NAME;
-
- parserContext.pushContainingComponent(new CompositeComponentDefinition("Mongo", source));
-
- BeanComponentDefinition mongoComponent = helper.getComponent(builder, defaultedId);
- parserContext.registerBeanComponent(mongoComponent);
- BeanComponentDefinition serverAddressPropertyEditor = helper.getComponent(MongoParsingUtils
- .getServerAddressPropertyEditorBuilder());
- parserContext.registerBeanComponent(serverAddressPropertyEditor);
- BeanComponentDefinition writeConcernPropertyEditor = helper.getComponent(MongoParsingUtils
- .getWriteConcernPropertyEditorBuilder());
- parserContext.registerBeanComponent(writeConcernPropertyEditor);
-
- parserContext.popAndRegisterContainingComponent();
-
- return mongoComponent.getBeanDefinition();
- }
-
-}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
index 29c92097a7..95b56b58f3 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoParsingUtils.java
@@ -1,206 +1,261 @@
-/*
- * Copyright 2011-2015 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
- *
- * http://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.config;
-
-import static org.springframework.data.config.ParsingUtils.*;
-
-import java.util.Map;
-
-import org.springframework.beans.factory.config.BeanDefinition;
-import org.springframework.beans.factory.config.CustomEditorConfigurer;
-import org.springframework.beans.factory.support.BeanDefinitionBuilder;
-import org.springframework.beans.factory.support.ManagedMap;
-import org.springframework.beans.factory.xml.BeanDefinitionParser;
-import org.springframework.data.mongodb.core.MongoClientOptionsFactoryBean;
-import org.springframework.data.mongodb.core.MongoOptionsFactoryBean;
-import org.springframework.util.xml.DomUtils;
-import org.w3c.dom.Element;
-
-/**
- * Utility methods for {@link BeanDefinitionParser} implementations for MongoDB.
- *
- * @author Mark Pollack
- * @author Oliver Gierke
- * @author Thomas Darimont
- * @author Christoph Strobl
- */
-@SuppressWarnings("deprecation")
-abstract class MongoParsingUtils {
-
- private MongoParsingUtils() {}
-
- /**
- * Parses the mongo replica-set element.
- *
- * @param parserContext the parser context
- * @param element the mongo element
- * @param mongoBuilder the bean definition builder to populate
- * @return
- */
- static void parseReplicaSet(Element element, BeanDefinitionBuilder mongoBuilder) {
- setPropertyValue(mongoBuilder, element, "replica-set", "replicaSetSeeds");
- }
-
- /**
- * Parses the {@code mongo:options} sub-element. Populates the given attribute factory with the proper attributes.
- *
- * @return true if parsing actually occured, {@literal false} otherwise
- */
- static boolean parseMongoOptions(Element element, BeanDefinitionBuilder mongoBuilder) {
-
- Element optionsElement = DomUtils.getChildElementByTagName(element, "options");
-
- if (optionsElement == null) {
- return false;
- }
-
- BeanDefinitionBuilder optionsDefBuilder = BeanDefinitionBuilder
- .genericBeanDefinition(MongoOptionsFactoryBean.class);
-
- setPropertyValue(optionsDefBuilder, optionsElement, "connections-per-host", "connectionsPerHost");
- setPropertyValue(optionsDefBuilder, optionsElement, "threads-allowed-to-block-for-connection-multiplier",
- "threadsAllowedToBlockForConnectionMultiplier");
- setPropertyValue(optionsDefBuilder, optionsElement, "max-wait-time", "maxWaitTime");
- setPropertyValue(optionsDefBuilder, optionsElement, "connect-timeout", "connectTimeout");
- setPropertyValue(optionsDefBuilder, optionsElement, "socket-timeout", "socketTimeout");
- setPropertyValue(optionsDefBuilder, optionsElement, "socket-keep-alive", "socketKeepAlive");
- setPropertyValue(optionsDefBuilder, optionsElement, "auto-connect-retry", "autoConnectRetry");
- setPropertyValue(optionsDefBuilder, optionsElement, "max-auto-connect-retry-time", "maxAutoConnectRetryTime");
- setPropertyValue(optionsDefBuilder, optionsElement, "write-number", "writeNumber");
- setPropertyValue(optionsDefBuilder, optionsElement, "write-timeout", "writeTimeout");
- setPropertyValue(optionsDefBuilder, optionsElement, "write-fsync", "writeFsync");
- setPropertyValue(optionsDefBuilder, optionsElement, "slave-ok", "slaveOk");
- setPropertyValue(optionsDefBuilder, optionsElement, "ssl", "ssl");
- setPropertyReference(optionsDefBuilder, optionsElement, "ssl-socket-factory-ref", "sslSocketFactory");
-
- mongoBuilder.addPropertyValue("mongoOptions", optionsDefBuilder.getBeanDefinition());
- return true;
- }
-
- /**
- * Parses the {@code mongo:client-options} sub-element. Populates the given attribute factory with the proper
- * attributes.
- *
- * @param element must not be {@literal null}.
- * @param mongoClientBuilder must not be {@literal null}.
- * @return
- * @since 1.7
- */
- public static boolean parseMongoClientOptions(Element element, BeanDefinitionBuilder mongoClientBuilder) {
-
- Element optionsElement = DomUtils.getChildElementByTagName(element, "client-options");
-
- if (optionsElement == null) {
- return false;
- }
-
- BeanDefinitionBuilder clientOptionsDefBuilder = BeanDefinitionBuilder
- .genericBeanDefinition(MongoClientOptionsFactoryBean.class);
-
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "description", "description");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "min-connections-per-host", "minConnectionsPerHost");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "connections-per-host", "connectionsPerHost");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "threads-allowed-to-block-for-connection-multiplier",
- "threadsAllowedToBlockForConnectionMultiplier");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "max-wait-time", "maxWaitTime");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "max-connection-idle-time", "maxConnectionIdleTime");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "max-connection-life-time", "maxConnectionLifeTime");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "connect-timeout", "connectTimeout");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "socket-timeout", "socketTimeout");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "socket-keep-alive", "socketKeepAlive");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "read-preference", "readPreference");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "write-concern", "writeConcern");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "heartbeat-frequency", "heartbeatFrequency");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "min-heartbeat-frequency", "minHeartbeatFrequency");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "heartbeat-connect-timeout", "heartbeatConnectTimeout");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "heartbeat-socket-timeout", "heartbeatSocketTimeout");
- setPropertyValue(clientOptionsDefBuilder, optionsElement, "ssl", "ssl");
- setPropertyReference(clientOptionsDefBuilder, optionsElement, "ssl-socket-factory-ref", "sslSocketFactory");
-
- mongoClientBuilder.addPropertyValue("mongoClientOptions", clientOptionsDefBuilder.getBeanDefinition());
-
- return true;
- }
-
- /**
- * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
- * {@link WriteConcernPropertyEditor}.
- *
- * @return
- */
- static BeanDefinitionBuilder getWriteConcernPropertyEditorBuilder() {
-
- Map> customEditors = new ManagedMap>();
- customEditors.put("com.mongodb.WriteConcern", WriteConcernPropertyEditor.class);
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
- builder.addPropertyValue("customEditors", customEditors);
-
- return builder;
- }
-
- /**
- * One should only register one bean definition but want to have the convenience of using
- * AbstractSingleBeanDefinitionParser but have the side effect of registering a 'default' property editor with the
- * container.
- */
- static BeanDefinitionBuilder getServerAddressPropertyEditorBuilder() {
-
- Map customEditors = new ManagedMap();
- customEditors.put("com.mongodb.ServerAddress[]",
- "org.springframework.data.mongodb.config.ServerAddressPropertyEditor");
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
- builder.addPropertyValue("customEditors", customEditors);
- return builder;
- }
-
- /**
- * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
- * {@link ReadPreferencePropertyEditor}.
- *
- * @return
- * @since 1.7
- */
- static BeanDefinitionBuilder getReadPreferencePropertyEditorBuilder() {
-
- Map> customEditors = new ManagedMap>();
- customEditors.put("com.mongodb.ReadPreference", ReadPreferencePropertyEditor.class);
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
- builder.addPropertyValue("customEditors", customEditors);
-
- return builder;
- }
-
- /**
- * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
- * {@link MongoCredentialPropertyEditor}.
- *
- * @return
- * @since 1.7
- */
- static BeanDefinitionBuilder getMongoCredentialPropertyEditor() {
-
- Map> customEditors = new ManagedMap>();
- customEditors.put("com.mongodb.MongoCredential[]", MongoCredentialPropertyEditor.class);
-
- BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
- builder.addPropertyValue("customEditors", customEditors);
-
- return builder;
- }
-}
+/*
+ * Copyright 2011-2025 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.config;
+
+import static org.springframework.data.config.ParsingUtils.*;
+
+import java.util.Map;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.CustomEditorConfigurer;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionValidationException;
+import org.springframework.beans.factory.support.ManagedMap;
+import org.springframework.beans.factory.xml.BeanDefinitionParser;
+import org.springframework.data.mongodb.core.MongoClientSettingsFactoryBean;
+import org.springframework.data.mongodb.core.MongoServerApiFactoryBean;
+import org.springframework.util.StringUtils;
+import org.springframework.util.xml.DomUtils;
+import org.w3c.dom.Element;
+
+/**
+ * Utility methods for {@link BeanDefinitionParser} implementations for MongoDB.
+ *
+ * @author Mark Pollack
+ * @author Oliver Gierke
+ * @author Thomas Darimont
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ */
+abstract class MongoParsingUtils {
+
+ private MongoParsingUtils() {}
+
+ /**
+ * Parses the {@code mongo:client-settings} sub-element. Populates the given attribute factory with the proper
+ * attributes.
+ *
+ * @param element
+ * @param mongoClientBuilder
+ * @return
+ * @since 3.0
+ */
+ public static boolean parseMongoClientSettings(Element element, BeanDefinitionBuilder mongoClientBuilder) {
+
+ Element settingsElement = DomUtils.getChildElementByTagName(element, "client-settings");
+ if (settingsElement == null) {
+ return false;
+ }
+
+ BeanDefinitionBuilder clientOptionsDefBuilder = BeanDefinitionBuilder
+ .genericBeanDefinition(MongoClientSettingsFactoryBean.class);
+
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "application-name", "applicationName");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "read-preference", "readPreference");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "read-concern", "readConcern");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "write-concern", "writeConcern");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "retry-reads", "retryReads");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "retry-writes", "retryWrites");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "uuid-representation", "uUidRepresentation");
+
+ // SocketSettings
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "socket-connect-timeout", "socketConnectTimeoutMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "socket-read-timeout", "socketReadTimeoutMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "socket-receive-buffer-size", "socketReceiveBufferSize");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "socket-send-buffer-size", "socketSendBufferSize");
+
+ // Server Settings
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "server-heartbeat-frequency",
+ "serverHeartbeatFrequencyMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "server-min-heartbeat-frequency",
+ "serverMinHeartbeatFrequencyMS");
+
+ // Cluster Settings
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-srv-host", "clusterSrvHost");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-hosts", "clusterHosts");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-connection-mode", "clusterConnectionMode");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-type", "custerRequiredClusterType");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-local-threshold", "clusterLocalThresholdMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "cluster-server-selection-timeout",
+ "clusterServerSelectionTimeoutMS");
+
+ // Connection Pool Settings
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-max-size", "poolMaxSize");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-min-size", "poolMinSize");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-max-wait-time", "poolMaxWaitTimeMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-max-connection-life-time",
+ "poolMaxConnectionLifeTimeMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-max-connection-idle-time",
+ "poolMaxConnectionIdleTimeMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-maintenance-initial-delay",
+ "poolMaintenanceInitialDelayMS");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "connection-pool-maintenance-frequency",
+ "poolMaintenanceFrequencyMS");
+
+ // SSL Settings
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "ssl-enabled", "sslEnabled");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "ssl-invalid-host-name-allowed",
+ "sslInvalidHostNameAllowed");
+ setPropertyValue(clientOptionsDefBuilder, settingsElement, "ssl-provider", "sslProvider");
+
+ // Field level encryption
+ setPropertyReference(clientOptionsDefBuilder, settingsElement, "encryption-settings-ref", "autoEncryptionSettings");
+
+ // ServerAPI
+ if (StringUtils.hasText(settingsElement.getAttribute("server-api-version"))) {
+
+ MongoServerApiFactoryBean serverApiFactoryBean = new MongoServerApiFactoryBean();
+ serverApiFactoryBean.setVersion(settingsElement.getAttribute("server-api-version"));
+ try {
+ clientOptionsDefBuilder.addPropertyValue("serverApi", serverApiFactoryBean.getObject());
+ } catch (Exception exception) {
+ throw new BeanDefinitionValidationException("Non parsable server-api.", exception);
+ }
+ } else {
+ setPropertyReference(clientOptionsDefBuilder, settingsElement, "server-api-ref", "serverApi");
+ }
+
+ // and the rest
+
+ mongoClientBuilder.addPropertyValue("mongoClientSettings", clientOptionsDefBuilder.getBeanDefinition());
+
+ return true;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link WriteConcernPropertyEditor}.
+ *
+ * @return
+ */
+ static BeanDefinitionBuilder getWriteConcernPropertyEditorBuilder() {
+
+ Map> customEditors = new ManagedMap>();
+ customEditors.put("com.mongodb.WriteConcern", WriteConcernPropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link ReadConcernPropertyEditor}.
+ *
+ * @return
+ * @since 3.0
+ */
+ static BeanDefinitionBuilder getReadConcernPropertyEditorBuilder() {
+
+ Map> customEditors = new ManagedMap<>();
+ customEditors.put("com.mongodb.ReadConcern", ReadConcernPropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+ /**
+ * One should only register one bean definition but want to have the convenience of using
+ * AbstractSingleBeanDefinitionParser but have the side effect of registering a 'default' property editor with the
+ * container.
+ */
+ static BeanDefinitionBuilder getServerAddressPropertyEditorBuilder() {
+
+ Map customEditors = new ManagedMap<>();
+ customEditors.put("com.mongodb.ServerAddress[]",
+ "org.springframework.data.mongodb.config.ServerAddressPropertyEditor");
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+ return builder;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link ReadPreferencePropertyEditor}.
+ *
+ * @return
+ * @since 1.7
+ */
+ static BeanDefinitionBuilder getReadPreferencePropertyEditorBuilder() {
+
+ Map> customEditors = new ManagedMap<>();
+ customEditors.put("com.mongodb.ReadPreference", ReadPreferencePropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link MongoCredentialPropertyEditor}.
+ *
+ * @return
+ * @since 1.7
+ */
+ static BeanDefinitionBuilder getMongoCredentialPropertyEditor() {
+
+ Map> customEditors = new ManagedMap>();
+ customEditors.put("com.mongodb.MongoCredential[]", MongoCredentialPropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link ConnectionStringPropertyEditor}.
+ *
+ * @return
+ * @since 3.0
+ */
+ static BeanDefinitionBuilder getConnectionStringPropertyEditorBuilder() {
+
+ Map> customEditors = new ManagedMap<>();
+ customEditors.put("com.mongodb.ConnectionString", ConnectionStringPropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+ /**
+ * Returns the {@link BeanDefinitionBuilder} to build a {@link BeanDefinition} for a
+ * {@link ConnectionStringPropertyEditor}.
+ *
+ * @return
+ * @since 3.0
+ */
+ static BeanDefinitionBuilder getUUidRepresentationEditorBuilder() {
+
+ Map> customEditors = new ManagedMap<>();
+ customEditors.put("org.bson.UuidRepresentation", UUidRepresentationPropertyEditor.class);
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CustomEditorConfigurer.class);
+ builder.addPropertyValue("customEditors", customEditors);
+
+ return builder;
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
index cfc5fdabca..1e1b11356f 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/MongoTemplateParser.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2014 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -33,16 +33,12 @@
/**
* {@link BeanDefinitionParser} to parse {@code template} elements into {@link BeanDefinition}s.
- *
+ *
* @author Martin Baumgartner
* @author Oliver Gierke
*/
class MongoTemplateParser extends AbstractBeanDefinitionParser {
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#resolveId(org.w3c.dom.Element, org.springframework.beans.factory.support.AbstractBeanDefinition, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected String resolveId(Element element, AbstractBeanDefinition definition, ParserContext parserContext)
throws BeanDefinitionStoreException {
@@ -51,10 +47,6 @@ protected String resolveId(Element element, AbstractBeanDefinition definition, P
return StringUtils.hasText(id) ? id : BeanNames.MONGO_TEMPLATE_BEAN_NAME;
}
- /*
- * (non-Javadoc)
- * @see org.springframework.beans.factory.xml.AbstractBeanDefinitionParser#parseInternal(org.w3c.dom.Element, org.springframework.beans.factory.xml.ParserContext)
- */
@Override
protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/PersistentEntitiesFactoryBean.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/PersistentEntitiesFactoryBean.java
new file mode 100644
index 0000000000..e46701a7f3
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/PersistentEntitiesFactoryBean.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright 2020-2025 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.config;
+
+import org.springframework.beans.factory.FactoryBean;
+import org.springframework.data.mapping.context.PersistentEntities;
+import org.springframework.data.mongodb.core.convert.MappingMongoConverter;
+
+/**
+ * Simple helper to be able to wire the {@link PersistentEntities} from a {@link MappingMongoConverter} bean available
+ * in the application context.
+ *
+ * @author Oliver Gierke
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 3.1
+ */
+public class PersistentEntitiesFactoryBean implements FactoryBean {
+
+ private final MappingMongoConverter converter;
+
+ /**
+ * Creates a new {@link PersistentEntitiesFactoryBean} for the given {@link MappingMongoConverter}.
+ *
+ * @param converter must not be {@literal null}.
+ */
+ public PersistentEntitiesFactoryBean(MappingMongoConverter converter) {
+ this.converter = converter;
+ }
+
+ @Override
+ public PersistentEntities getObject() {
+ return PersistentEntities.of(converter.getMappingContext());
+ }
+
+ @Override
+ public Class> getObjectType() {
+ return PersistentEntities.class;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReactiveMongoAuditingRegistrar.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReactiveMongoAuditingRegistrar.java
new file mode 100644
index 0000000000..80cf404434
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReactiveMongoAuditingRegistrar.java
@@ -0,0 +1,81 @@
+/*
+ * Copyright 2020-2025 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.config;
+
+import java.lang.annotation.Annotation;
+
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.BeanDefinitionBuilder;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
+import org.springframework.data.auditing.ReactiveIsNewAwareAuditingHandler;
+import org.springframework.data.auditing.config.AuditingBeanDefinitionRegistrarSupport;
+import org.springframework.data.auditing.config.AuditingConfiguration;
+import org.springframework.data.config.ParsingUtils;
+import org.springframework.data.mongodb.core.mapping.event.ReactiveAuditingEntityCallback;
+import org.springframework.util.Assert;
+
+/**
+ * {@link ImportBeanDefinitionRegistrar} to enable {@link EnableReactiveMongoAuditing} annotation.
+ *
+ * @author Mark Paluch
+ * @author Christoph Strobl
+ * @since 3.1
+ */
+class ReactiveMongoAuditingRegistrar extends AuditingBeanDefinitionRegistrarSupport {
+
+ @Override
+ protected Class extends Annotation> getAnnotation() {
+ return EnableReactiveMongoAuditing.class;
+ }
+
+ @Override
+ protected String getAuditingHandlerBeanName() {
+ return "reactiveMongoAuditingHandler";
+ }
+
+ @Override
+ protected void postProcess(BeanDefinitionBuilder builder, AuditingConfiguration configuration,
+ BeanDefinitionRegistry registry) {
+ builder.setFactoryMethod("from").addConstructorArgReference("mongoMappingContext");
+ }
+
+ @Override
+ protected BeanDefinitionBuilder getAuditHandlerBeanDefinitionBuilder(AuditingConfiguration configuration) {
+
+ Assert.notNull(configuration, "AuditingConfiguration must not be null");
+
+ return configureDefaultAuditHandlerAttributes(configuration,
+ BeanDefinitionBuilder.rootBeanDefinition(ReactiveIsNewAwareAuditingHandler.class));
+ }
+
+ @Override
+ protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition,
+ BeanDefinitionRegistry registry) {
+
+ Assert.notNull(auditingHandlerDefinition, "BeanDefinition must not be null");
+ Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
+
+ BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(ReactiveAuditingEntityCallback.class);
+
+ builder.addConstructorArgValue(ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), registry));
+ builder.getRawBeanDefinition().setSource(auditingHandlerDefinition.getSource());
+
+ registerInfrastructureBeanWithId(builder.getBeanDefinition(), ReactiveAuditingEntityCallback.class.getName(),
+ registry);
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java
new file mode 100644
index 0000000000..60bf126ae7
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadConcernPropertyEditor.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2019-2025 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.config;
+
+import java.beans.PropertyEditorSupport;
+
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+import com.mongodb.ReadConcern;
+import com.mongodb.ReadConcernLevel;
+
+/**
+ * Parse a {@link String} to a {@link ReadConcern}. If it is a well know {@link String} as identified by the
+ * {@link ReadConcernLevel#fromString(String)}.
+ *
+ * @author Christoph Strobl
+ * @since 3.0
+ */
+public class ReadConcernPropertyEditor extends PropertyEditorSupport {
+
+ @Override
+ public void setAsText(@Nullable String readConcernString) {
+
+ if (!StringUtils.hasText(readConcernString)) {
+ return;
+ }
+
+ setValue(new ReadConcern(ReadConcernLevel.fromString(readConcernString)));
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
index e5509e0cec..5ed9b66619 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ReadPreferencePropertyEditor.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2015 the original author or authors.
+ * Copyright 2015-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -17,22 +17,20 @@
import java.beans.PropertyEditorSupport;
+import org.springframework.lang.Nullable;
+
import com.mongodb.ReadPreference;
/**
* Parse a {@link String} to a {@link ReadPreference}.
- *
+ *
* @author Christoph Strobl
* @since 1.7
*/
public class ReadPreferencePropertyEditor extends PropertyEditorSupport {
- /*
- * (non-Javadoc)
- * @see java.beans.PropertyEditorSupport#setAsText(java.lang.String)
- */
@Override
- public void setAsText(String readPreferenceString) throws IllegalArgumentException {
+ public void setAsText(@Nullable String readPreferenceString) throws IllegalArgumentException {
if (readPreferenceString == null) {
return;
@@ -59,8 +57,8 @@ public void setAsText(String readPreferenceString) throws IllegalArgumentExcepti
} else if ("NEAREST".equalsIgnoreCase(readPreferenceString)) {
setValue(ReadPreference.nearest());
} else {
- throw new IllegalArgumentException(String.format("Cannot find matching ReadPreference for %s",
- readPreferenceString));
+ throw new IllegalArgumentException(
+ String.format("Cannot find matching ReadPreference for %s", readPreferenceString));
}
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
index 32383ba49a..9c51900902 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/ServerAddressPropertyEditor.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011-2013 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -21,8 +21,9 @@
import java.util.HashSet;
import java.util.Set;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
+import org.apache.commons.logging.Log;
+import org.apache.commons.logging.LogFactory;
+import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
@@ -30,10 +31,11 @@
/**
* Parse a {@link String} to a {@link ServerAddress} array. The format is host1:port1,host2:port2,host3:port3.
- *
+ *
* @author Mark Pollack
* @author Oliver Gierke
* @author Thomas Darimont
+ * @author Christoph Strobl
*/
public class ServerAddressPropertyEditor extends PropertyEditorSupport {
@@ -41,15 +43,11 @@ public class ServerAddressPropertyEditor extends PropertyEditorSupport {
* A port is a number without a leading 0 at the end of the address that is proceeded by just a single :.
*/
private static final String HOST_PORT_SPLIT_PATTERN = "(? 2) {
- LOG.warn(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source);
+ if(LOG.isWarnEnabled()) {
+ LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "source", source));
+ }
return null;
}
@@ -102,9 +105,13 @@ private ServerAddress parseServerAddress(String source) {
return port == null ? new ServerAddress(hostAddress) : new ServerAddress(hostAddress, port);
} catch (UnknownHostException e) {
- LOG.warn(COULD_NOT_PARSE_ADDRESS_MESSAGE, "host", hostAndPort[0]);
+ if(LOG.isWarnEnabled()) {
+ LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "host", hostAndPort[0]));
+ }
} catch (NumberFormatException e) {
- LOG.warn(COULD_NOT_PARSE_ADDRESS_MESSAGE, "port", hostAndPort[1]);
+ if(LOG.isWarnEnabled()) {
+ LOG.warn(String.format(COULD_NOT_PARSE_ADDRESS_MESSAGE, "port", hostAndPort[1]));
+ }
}
return null;
@@ -112,13 +119,13 @@ private ServerAddress parseServerAddress(String source) {
/**
* Extract the host and port from the given {@link String}.
- *
+ *
* @param addressAndPortSource must not be {@literal null}.
* @return
*/
private String[] extractHostAddressAndPort(String addressAndPortSource) {
- Assert.notNull(addressAndPortSource, "Address and port source must not be null!");
+ Assert.notNull(addressAndPortSource, "Address and port source must not be null");
String[] hostAndPort = addressAndPortSource.split(HOST_PORT_SPLIT_PATTERN);
String hostAddress = hostAndPort[0];
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/StringToWriteConcernConverter.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/StringToWriteConcernConverter.java
index b0fba90e2e..9f579b8fe9 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/StringToWriteConcernConverter.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/StringToWriteConcernConverter.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2012 the original author or authors.
+ * Copyright 2012-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -21,15 +21,11 @@
/**
* Converter to create {@link WriteConcern} instances from String representations.
- *
+ *
* @author Oliver Gierke
*/
public class StringToWriteConcernConverter implements Converter {
- /*
- * (non-Javadoc)
- * @see org.springframework.core.convert.converter.Converter#convert(java.lang.Object)
- */
public WriteConcern convert(String source) {
WriteConcern writeConcern = WriteConcern.valueOf(source);
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java
new file mode 100644
index 0000000000..b777969967
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/UUidRepresentationPropertyEditor.java
@@ -0,0 +1,41 @@
+/*
+ * Copyright 2020-2025 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.config;
+
+import java.beans.PropertyEditorSupport;
+
+import org.bson.UuidRepresentation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
+/**
+ * Parse a {@link String} to a {@link UuidRepresentation}.
+ *
+ * @author Christoph Strobl
+ * @since 3.0
+ */
+public class UUidRepresentationPropertyEditor extends PropertyEditorSupport {
+
+ @Override
+ public void setAsText(@Nullable String value) {
+
+ if (!StringUtils.hasText(value)) {
+ return;
+ }
+
+ setValue(UuidRepresentation.valueOf(value));
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
index a98d5f6cbe..ee0d09e555 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/WriteConcernPropertyEditor.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2011 the original author or authors.
+ * Copyright 2011-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -17,6 +17,9 @@
import java.beans.PropertyEditorSupport;
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+
import com.mongodb.WriteConcern;
/**
@@ -24,16 +27,21 @@
* {@link WriteConcern#valueOf(String)}, use the well known {@link WriteConcern} value, otherwise pass the string as is
* to the constructor of the write concern. There is no support for other constructor signatures when parsing from a
* string value.
- *
+ *
* @author Mark Pollack
+ * @author Christoph Strobl
*/
public class WriteConcernPropertyEditor extends PropertyEditorSupport {
/**
- * Parse a string to a List
+ * Parse a string to a {@link WriteConcern}.
*/
@Override
- public void setAsText(String writeConcernString) {
+ public void setAsText(@Nullable String writeConcernString) {
+
+ if (!StringUtils.hasText(writeConcernString)) {
+ return;
+ }
WriteConcern writeConcern = WriteConcern.valueOf(writeConcernString);
if (writeConcern != null) {
@@ -43,6 +51,5 @@ public void setAsText(String writeConcernString) {
// pass on the string to the constructor
setValue(new WriteConcern(writeConcernString));
}
-
}
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
index f098200afe..5a1e5b725e 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/config/package-info.java
@@ -1,5 +1,6 @@
/**
* Spring XML namespace configuration for MongoDB specific repositories.
*/
+@org.springframework.lang.NonNullApi
package org.springframework.data.mongodb.config;
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java
new file mode 100644
index 0000000000..a00d95a9ad
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/AggregationUtil.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2018-2025 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.core;
+
+import java.util.List;
+
+import org.bson.Document;
+
+import org.springframework.data.mapping.context.MappingContext;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.AggregationOptions.DomainTypeMapping;
+import org.springframework.data.mongodb.core.aggregation.FieldLookupPolicy;
+import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.TypedAggregation;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentProperty;
+import org.springframework.data.util.Lazy;
+import org.springframework.lang.Nullable;
+
+/**
+ * Utility methods to map {@link org.springframework.data.mongodb.core.aggregation.Aggregation} pipeline definitions and
+ * create type-bound {@link AggregationOperationContext}.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @since 2.1
+ */
+class AggregationUtil {
+
+ final QueryMapper queryMapper;
+ final MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext;
+ final Lazy untypedMappingContext;
+
+ AggregationUtil(QueryMapper queryMapper,
+ MappingContext extends MongoPersistentEntity>, MongoPersistentProperty> mappingContext) {
+
+ this.queryMapper = queryMapper;
+ this.mappingContext = mappingContext;
+ this.untypedMappingContext = Lazy.of(() -> new TypeBasedAggregationOperationContext(Object.class, mappingContext,
+ queryMapper, FieldLookupPolicy.relaxed()));
+ }
+
+ AggregationOperationContext createAggregationContext(Aggregation aggregation, @Nullable Class> inputType) {
+
+ DomainTypeMapping domainTypeMapping = aggregation.getOptions().getDomainTypeMapping();
+
+ if (domainTypeMapping == DomainTypeMapping.NONE) {
+ return Aggregation.DEFAULT_CONTEXT;
+ }
+
+ FieldLookupPolicy lookupPolicy = domainTypeMapping == DomainTypeMapping.STRICT
+ && !aggregation.getPipeline().containsUnionWith() ? FieldLookupPolicy.strict() : FieldLookupPolicy.relaxed();
+
+ if (aggregation instanceof TypedAggregation> ta) {
+ return new TypeBasedAggregationOperationContext(ta.getInputType(), mappingContext, queryMapper, lookupPolicy);
+ }
+
+ if (inputType == null) {
+ return untypedMappingContext.get();
+ }
+
+ return new TypeBasedAggregationOperationContext(inputType, mappingContext, queryMapper, lookupPolicy);
+ }
+
+ /**
+ * Extract and map the aggregation pipeline into a {@link List} of {@link Document}.
+ *
+ * @param aggregation
+ * @param context
+ * @return
+ */
+ List createPipeline(Aggregation aggregation, AggregationOperationContext context) {
+ return aggregation.toPipeline(context);
+ }
+
+ /**
+ * Extract the command and map the aggregation pipeline.
+ *
+ * @param aggregation
+ * @param context
+ * @return
+ */
+ Document createCommand(String collection, Aggregation aggregation, AggregationOperationContext context) {
+ return aggregation.toDocument(collection, context);
+ }
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java
index d4762e738d..4820c2355c 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperations.java
@@ -1,11 +1,11 @@
/*
- * Copyright 2015-2016 the original author or authors.
+ * Copyright 2015-2025 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
*
- * http://www.apache.org/licenses/LICENSE-2.0
+ * 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,
@@ -17,20 +17,36 @@
import java.util.List;
+import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.data.mongodb.core.query.UpdateDefinition;
import org.springframework.data.util.Pair;
-import com.mongodb.BulkWriteResult;
+import com.mongodb.bulk.BulkWriteResult;
/**
- * Bulk operations for insert/update/remove actions on a collection. These bulks operation are available since MongoDB
- * 2.6 and make use of low level bulk commands on the protocol level. This interface defines a fluent API to add
- * multiple single operations or list of similar operations in sequence which can then eventually be executed by calling
+ * Bulk operations for insert/update/remove actions on a collection. Bulk operations are available since MongoDB 2.6 and
+ * make use of low level bulk commands on the protocol level. This interface defines a fluent API to add multiple single
+ * operations or list of similar operations in sequence which can then eventually be executed by calling
* {@link #execute()}.
- *
+ *
+ *
+ * MongoOperations ops = …;
+ *
+ * ops.bulkOps(BulkMode.UNORDERED, Person.class)
+ * .insert(newPerson)
+ * .updateOne(where("firstname").is("Joe"), Update.update("lastname", "Doe"))
+ * .execute();
+ *
+ *
+ * Bulk operations are issued as one batch that pulls together all insert, update, and delete operations. Operations
+ * that require individual operation results such as optimistic locking (using {@code @Version}) are not supported and
+ * the version field remains not populated.
+ *
* @author Tobias Trelle
* @author Oliver Gierke
+ * @author Minsu Kim
* @since 1.9
*/
public interface BulkOperations {
@@ -38,18 +54,18 @@ public interface BulkOperations {
/**
* Mode for bulk operation.
**/
- public enum BulkMode {
+ enum BulkMode {
/** Perform bulk operations in sequence. The first error will cancel processing. */
ORDERED,
/** Perform bulk operations in parallel. Processing will continue on errors. */
UNORDERED
- };
+ }
/**
* Add a single insert to the bulk operation.
- *
+ *
* @param documents the document to insert, must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the insert added, will never be {@literal null}.
*/
@@ -57,7 +73,7 @@ public enum BulkMode {
/**
* Add a list of inserts to the bulk operation.
- *
+ *
* @param documents List of documents to insert, must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the insert added, will never be {@literal null}.
*/
@@ -65,63 +81,99 @@ public enum BulkMode {
/**
* Add a single update to the bulk operation. For the update request, only the first matching document is updated.
- *
- * @param query update criteria, must not be {@literal null}.
+ *
+ * @param query update criteria, must not be {@literal null}. The {@link Query} may define a {@link Query#with(Sort)
+ * sort order} to influence which document to update when potentially matching multiple candidates.
* @param update {@link Update} operation to perform, must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
*/
- BulkOperations updateOne(Query query, Update update);
+ default BulkOperations updateOne(Query query, Update update) {
+ return updateOne(query, (UpdateDefinition) update);
+ }
+
+ /**
+ * Add a single update to the bulk operation. For the update request, only the first matching document is updated.
+ *
+ * @param query update criteria, must not be {@literal null}. The {@link Query} may define a {@link Query#with(Sort)
+ * sort order} to influence which document to update when potentially matching multiple candidates.
+ * @param update {@link Update} operation to perform, must not be {@literal null}.
+ * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
+ * @since 4.1
+ */
+ BulkOperations updateOne(Query query, UpdateDefinition update);
/**
* Add a list of updates to the bulk operation. For each update request, only the first matching document is updated.
- *
+ *
* @param updates Update operations to perform.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
*/
- BulkOperations updateOne(List> updates);
+ BulkOperations updateOne(List> updates);
+
+ /**
+ * Add a single update to the bulk operation. For the update request, all matching documents are updated.
+ *
+ * @param query Update criteria.
+ * @param update Update operation to perform.
+ * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
+ */
+ default BulkOperations updateMulti(Query query, Update update) {
+ return updateMulti(query, (UpdateDefinition) update);
+ }
/**
* Add a single update to the bulk operation. For the update request, all matching documents are updated.
- *
+ *
* @param query Update criteria.
* @param update Update operation to perform.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
+ * @since 4.1
*/
- BulkOperations updateMulti(Query query, Update update);
+ BulkOperations updateMulti(Query query, UpdateDefinition update);
/**
* Add a list of updates to the bulk operation. For each update request, all matching documents are updated.
- *
+ *
* @param updates Update operations to perform.
- * @return The bulk operation.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
*/
- BulkOperations updateMulti(List> updates);
+ BulkOperations updateMulti(List> updates);
/**
* Add a single upsert to the bulk operation. An upsert is an update if the set of matching documents is not empty,
* else an insert.
- *
+ *
* @param query Update criteria.
* @param update Update operation to perform.
- * @return The bulk operation.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
*/
- BulkOperations upsert(Query query, Update update);
+ default BulkOperations upsert(Query query, Update update) {
+ return upsert(query, (UpdateDefinition) update);
+ }
+
+ /**
+ * Add a single upsert to the bulk operation. An upsert is an update if the set of matching documents is not empty,
+ * else an insert.
+ *
+ * @param query Update criteria.
+ * @param update Update operation to perform.
+ * @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
+ * @since 4.1
+ */
+ BulkOperations upsert(Query query, UpdateDefinition update);
/**
* Add a list of upserts to the bulk operation. An upsert is an update if the set of matching documents is not empty,
* else an insert.
- *
+ *
* @param updates Updates/insert operations to perform.
- * @return The bulk operation.
* @return the current {@link BulkOperations} instance with the update added, will never be {@literal null}.
*/
BulkOperations upsert(List> updates);
/**
* Add a single remove operation to the bulk operation.
- *
+ *
* @param remove the {@link Query} to select the documents to be removed, must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the removal added, will never be {@literal null}.
*/
@@ -129,17 +181,42 @@ public enum BulkMode {
/**
* Add a list of remove operations to the bulk operation.
- *
+ *
* @param removes the remove operations to perform, must not be {@literal null}.
* @return the current {@link BulkOperations} instance with the removal added, will never be {@literal null}.
*/
BulkOperations remove(List removes);
+ /**
+ * Add a single replace operation to the bulk operation.
+ *
+ * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence
+ * which document to replace when potentially matching multiple candidates.
+ * @param replacement the replacement document. Must not be {@literal null}.
+ * @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}.
+ * @since 2.2
+ */
+ default BulkOperations replaceOne(Query query, Object replacement) {
+ return replaceOne(query, replacement, FindAndReplaceOptions.empty());
+ }
+
+ /**
+ * Add a single replace operation to the bulk operation.
+ *
+ * @param query Replace criteria. The {@link Query} may define a {@link Query#with(Sort) sort order} to influence
+ * which document to replace when potentially matching multiple candidates.
+ * @param replacement the replacement document. Must not be {@literal null}.
+ * @param options the {@link FindAndModifyOptions} holding additional information. Must not be {@literal null}.
+ * @return the current {@link BulkOperations} instance with the replacement added, will never be {@literal null}.
+ * @since 2.2
+ */
+ BulkOperations replaceOne(Query query, Object replacement, FindAndReplaceOptions options);
+
/**
* Execute all bulk operations using the default write concern.
- *
+ *
* @return Result of the bulk operation providing counters for inserts/updates etc.
- * @throws {@link BulkOperationException} if an error occurred during bulk processing.
+ * @throws org.springframework.data.mongodb.BulkOperationException if an error occurred during bulk processing.
*/
BulkWriteResult execute();
}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java
new file mode 100644
index 0000000000..1f5509cd60
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/BulkOperationsSupport.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2023-2025 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.core;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.bson.Document;
+import org.bson.conversions.Bson;
+import org.springframework.context.ApplicationEvent;
+import org.springframework.data.mapping.PersistentEntity;
+import org.springframework.data.mongodb.core.BulkOperations.BulkMode;
+import org.springframework.data.mongodb.core.aggregation.AggregationOperationContext;
+import org.springframework.data.mongodb.core.aggregation.AggregationUpdate;
+import org.springframework.data.mongodb.core.aggregation.RelaxedTypeBasedAggregationOperationContext;
+import org.springframework.data.mongodb.core.convert.QueryMapper;
+import org.springframework.data.mongodb.core.convert.UpdateMapper;
+import org.springframework.data.mongodb.core.mapping.MongoPersistentEntity;
+import org.springframework.data.mongodb.core.mapping.event.AfterSaveEvent;
+import org.springframework.data.mongodb.core.mapping.event.BeforeSaveEvent;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.query.Query;
+import org.springframework.data.mongodb.core.query.Update;
+import org.springframework.data.mongodb.core.query.UpdateDefinition;
+import org.springframework.data.mongodb.core.query.UpdateDefinition.ArrayFilter;
+import org.springframework.util.Assert;
+
+import com.mongodb.client.model.BulkWriteOptions;
+import com.mongodb.client.model.DeleteManyModel;
+import com.mongodb.client.model.DeleteOneModel;
+import com.mongodb.client.model.InsertOneModel;
+import com.mongodb.client.model.ReplaceOneModel;
+import com.mongodb.client.model.UpdateManyModel;
+import com.mongodb.client.model.UpdateOneModel;
+import com.mongodb.client.model.UpdateOptions;
+import com.mongodb.client.model.WriteModel;
+
+/**
+ * Support class for bulk operations.
+ *
+ * @author Mark Paluch
+ * @since 4.1
+ */
+abstract class BulkOperationsSupport {
+
+ private final String collectionName;
+
+ BulkOperationsSupport(String collectionName) {
+
+ Assert.hasText(collectionName, "CollectionName must not be null nor empty");
+
+ this.collectionName = collectionName;
+ }
+
+ /**
+ * Emit a {@link BeforeSaveEvent}.
+ *
+ * @param holder
+ */
+ void maybeEmitBeforeSaveEvent(SourceAwareWriteModelHolder holder) {
+
+ if (holder.model() instanceof InsertOneModel) {
+
+ Document target = ((InsertOneModel) holder.model()).getDocument();
+ maybeEmitEvent(new BeforeSaveEvent<>(holder.source(), target, collectionName));
+ } else if (holder.model() instanceof ReplaceOneModel) {
+
+ Document target = ((ReplaceOneModel) holder.model()).getReplacement();
+ maybeEmitEvent(new BeforeSaveEvent<>(holder.source(), target, collectionName));
+ }
+ }
+
+ /**
+ * Emit a {@link AfterSaveEvent}.
+ *
+ * @param holder
+ */
+ void maybeEmitAfterSaveEvent(SourceAwareWriteModelHolder holder) {
+
+ if (holder.model() instanceof InsertOneModel) {
+
+ Document target = ((InsertOneModel) holder.model()).getDocument();
+ maybeEmitEvent(new AfterSaveEvent<>(holder.source(), target, collectionName));
+ } else if (holder.model() instanceof ReplaceOneModel) {
+
+ Document target = ((ReplaceOneModel) holder.model()).getReplacement();
+ maybeEmitEvent(new AfterSaveEvent<>(holder.source(), target, collectionName));
+ }
+ }
+
+ WriteModel mapWriteModel(Object source, WriteModel writeModel) {
+
+ if (writeModel instanceof UpdateOneModel model) {
+
+ Bson sort = model.getOptions().getSort();
+ if (sort instanceof Document sortDocument) {
+ model.getOptions().sort(updateMapper().getMappedSort(sortDocument, entity().orElse(null)));
+ }
+
+ if (source instanceof AggregationUpdate aggregationUpdate) {
+
+ List pipeline = mapUpdatePipeline(aggregationUpdate);
+ return new UpdateOneModel<>(getMappedQuery(model.getFilter()), pipeline, model.getOptions());
+ }
+
+ return new UpdateOneModel<>(getMappedQuery(model.getFilter()), getMappedUpdate(model.getUpdate()),
+ model.getOptions());
+ }
+
+ if (writeModel instanceof UpdateManyModel model) {
+
+ if (source instanceof AggregationUpdate aggregationUpdate) {
+
+ List pipeline = mapUpdatePipeline(aggregationUpdate);
+ return new UpdateManyModel<>(getMappedQuery(model.getFilter()), pipeline, model.getOptions());
+ }
+
+ return new UpdateManyModel<>(getMappedQuery(model.getFilter()), getMappedUpdate(model.getUpdate()),
+ model.getOptions());
+ }
+
+ if (writeModel instanceof DeleteOneModel model) {
+ return new DeleteOneModel<>(getMappedQuery(model.getFilter()), model.getOptions());
+ }
+
+ if (writeModel instanceof DeleteManyModel model) {
+ return new DeleteManyModel<>(getMappedQuery(model.getFilter()), model.getOptions());
+ }
+
+ if (writeModel instanceof ReplaceOneModel model) {
+
+ Bson sort = model.getReplaceOptions().getSort();
+
+ if (sort instanceof Document sortDocument) {
+ model.getReplaceOptions().sort(updateMapper().getMappedSort(sortDocument, entity().orElse(null)));
+ }
+ return new ReplaceOneModel<>(getMappedQuery(model.getFilter()), model.getReplacement(),
+ model.getReplaceOptions());
+ }
+
+ return writeModel;
+ }
+
+ private List mapUpdatePipeline(AggregationUpdate source) {
+
+ Class> type = entity().isPresent() ? entity().map(PersistentEntity::getType).get() : Object.class;
+ AggregationOperationContext context = new RelaxedTypeBasedAggregationOperationContext(type,
+ updateMapper().getMappingContext(), queryMapper());
+
+ return new AggregationUtil(queryMapper(), queryMapper().getMappingContext()).createPipeline(source, context);
+ }
+
+ /**
+ * Emit a {@link ApplicationEvent} if event multicasting is enabled.
+ *
+ * @param event
+ */
+ protected abstract void maybeEmitEvent(ApplicationEvent event);
+
+ /**
+ * @return the {@link UpdateMapper} to use.
+ */
+ protected abstract UpdateMapper updateMapper();
+
+ /**
+ * @return the {@link QueryMapper} to use.
+ */
+ protected abstract QueryMapper queryMapper();
+
+ /**
+ * @return the associated {@link PersistentEntity}. Can be {@link Optional#empty()}.
+ */
+ protected abstract Optional extends MongoPersistentEntity>> entity();
+
+ protected Bson getMappedUpdate(Bson update) {
+ return updateMapper().getMappedObject(update, entity());
+ }
+
+ protected Bson getMappedQuery(Bson query) {
+ return queryMapper().getMappedObject(query, entity());
+ }
+
+ protected static BulkWriteOptions getBulkWriteOptions(BulkMode bulkMode) {
+
+ BulkWriteOptions options = new BulkWriteOptions();
+
+ return switch (bulkMode) {
+ case ORDERED -> options.ordered(true);
+ case UNORDERED -> options.ordered(false);
+ };
+ }
+
+ /**
+ * @param filterQuery The {@link Query} to read a potential {@link Collation} from. Must not be {@literal null}.
+ * @param update The {@link Update} to apply
+ * @param upsert flag to indicate if document should be upserted.
+ * @param multi flag to indicate if update might affect multiple documents.
+ * @return new instance of {@link UpdateOptions}.
+ */
+ protected UpdateOptions computeUpdateOptions(Query filterQuery, UpdateDefinition update, boolean upsert,
+ boolean multi) {
+
+ UpdateOptions options = new UpdateOptions();
+ options.upsert(upsert);
+
+ if (update.hasArrayFilters()) {
+ List list = new ArrayList<>(update.getArrayFilters().size());
+ for (ArrayFilter arrayFilter : update.getArrayFilters()) {
+ list.add(arrayFilter.asDocument());
+ }
+ options.arrayFilters(list);
+ }
+
+ if (!multi && filterQuery.isSorted()) {
+ options.sort(filterQuery.getSortObject());
+ }
+
+ filterQuery.getCollation().map(Collation::toMongoCollation).ifPresent(options::collation);
+ return options;
+ }
+
+ /**
+ * Value object chaining together an actual source with its {@link WriteModel} representation.
+ *
+ * @author Christoph Strobl
+ */
+ record SourceAwareWriteModelHolder(Object source, WriteModel model) {
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
new file mode 100644
index 0000000000..17b8835b7e
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamEvent.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2018-2025 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.core;
+
+import java.time.Instant;
+import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
+
+import org.bson.BsonTimestamp;
+import org.bson.BsonValue;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.convert.MongoConverter;
+import org.springframework.data.mongodb.core.messaging.Message;
+import org.springframework.lang.Nullable;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+import com.mongodb.client.model.changestream.ChangeStreamDocument;
+import com.mongodb.client.model.changestream.OperationType;
+
+/**
+ * {@link Message} implementation specific to MongoDB Change
+ * Streams .
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Myroslav Kosinskyi
+ * @since 2.1
+ */
+public class ChangeStreamEvent {
+
+ @SuppressWarnings("rawtypes") //
+ private static final AtomicReferenceFieldUpdater CONVERTED_FULL_DOCUMENT_UPDATER = AtomicReferenceFieldUpdater
+ .newUpdater(ChangeStreamEvent.class, Object.class, "convertedFullDocument");
+
+ @SuppressWarnings("rawtypes") //
+ private static final AtomicReferenceFieldUpdater CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER = AtomicReferenceFieldUpdater
+ .newUpdater(ChangeStreamEvent.class, Object.class, "convertedFullDocumentBeforeChange");
+
+ private final @Nullable ChangeStreamDocument raw;
+
+ private final Class targetType;
+ private final MongoConverter converter;
+
+ // accessed through CONVERTED_FULL_DOCUMENT_UPDATER.
+ private volatile @Nullable T convertedFullDocument;
+
+ // accessed through CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER.
+ private volatile @Nullable T convertedFullDocumentBeforeChange;
+
+ /**
+ * @param raw can be {@literal null}.
+ * @param targetType must not be {@literal null}.
+ * @param converter must not be {@literal null}.
+ */
+ public ChangeStreamEvent(@Nullable ChangeStreamDocument raw, Class targetType,
+ MongoConverter converter) {
+
+ this.raw = raw;
+ this.targetType = targetType;
+ this.converter = converter;
+ }
+
+ /**
+ * Get the raw {@link ChangeStreamDocument} as emitted by the driver.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public ChangeStreamDocument getRaw() {
+ return raw;
+ }
+
+ /**
+ * Get the {@link ChangeStreamDocument#getClusterTime() cluster time} as {@link Instant} the event was emitted at.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public Instant getTimestamp() {
+
+ return getBsonTimestamp() != null ? converter.getConversionService().convert(raw.getClusterTime(), Instant.class)
+ : null;
+ }
+
+ /**
+ * Get the {@link ChangeStreamDocument#getClusterTime() cluster time}.
+ *
+ * @return can be {@literal null}.
+ * @since 2.2
+ */
+ @Nullable
+ public BsonTimestamp getBsonTimestamp() {
+ return raw != null ? raw.getClusterTime() : null;
+ }
+
+ /**
+ * Get the {@link ChangeStreamDocument#getResumeToken() resume token} for this event.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public BsonValue getResumeToken() {
+ return raw != null ? raw.getResumeToken() : null;
+ }
+
+ /**
+ * Get the {@link ChangeStreamDocument#getOperationType() operation type} for this event.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public OperationType getOperationType() {
+ return raw != null ? raw.getOperationType() : null;
+ }
+
+ /**
+ * Get the database name the event was originated at.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public String getDatabaseName() {
+ return raw != null ? raw.getNamespace().getDatabaseName() : null;
+ }
+
+ /**
+ * Get the collection name the event was originated at.
+ *
+ * @return can be {@literal null}.
+ */
+ @Nullable
+ public String getCollectionName() {
+ return raw != null ? raw.getNamespace().getCollectionName() : null;
+ }
+
+ /**
+ * Get the potentially converted {@link ChangeStreamDocument#getFullDocument()}.
+ *
+ * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocument()} is
+ * {@literal null}.
+ */
+ @Nullable
+ public T getBody() {
+
+ if (raw == null || raw.getFullDocument() == null) {
+ return null;
+ }
+
+ return getConvertedFullDocument(raw.getFullDocument());
+ }
+
+ /**
+ * Get the potentially converted {@link ChangeStreamDocument#getFullDocumentBeforeChange() document} before being changed.
+ *
+ * @return {@literal null} when {@link #getRaw()} or {@link ChangeStreamDocument#getFullDocumentBeforeChange()} is
+ * {@literal null}.
+ * @since 4.0
+ */
+ @Nullable
+ public T getBodyBeforeChange() {
+
+ if (raw == null || raw.getFullDocumentBeforeChange() == null) {
+ return null;
+ }
+
+ return getConvertedFullDocumentBeforeChange(raw.getFullDocumentBeforeChange());
+ }
+
+ @SuppressWarnings("unchecked")
+ private T getConvertedFullDocumentBeforeChange(Document fullDocument) {
+ return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_BEFORE_CHANGE_UPDATER);
+ }
+
+ @SuppressWarnings("unchecked")
+ private T getConvertedFullDocument(Document fullDocument) {
+ return (T) doGetConverted(fullDocument, CONVERTED_FULL_DOCUMENT_UPDATER);
+ }
+
+ private Object doGetConverted(Document fullDocument, AtomicReferenceFieldUpdater updater) {
+
+ Object result = updater.get(this);
+
+ if (result != null) {
+ return result;
+ }
+
+ if (ClassUtils.isAssignable(Document.class, fullDocument.getClass())) {
+
+ result = converter.read(targetType, fullDocument);
+ return updater.compareAndSet(this, null, result) ? result : updater.get(this);
+ }
+
+ if (converter.getConversionService().canConvert(fullDocument.getClass(), targetType)) {
+
+ result = converter.getConversionService().convert(fullDocument, targetType);
+ return updater.compareAndSet(this, null, result) ? result : updater.get(this);
+ }
+
+ throw new IllegalArgumentException(
+ String.format("No converter found capable of converting %s to %s", fullDocument.getClass(), targetType));
+ }
+
+ @Override
+ public String toString() {
+ return "ChangeStreamEvent {" + "raw=" + raw + ", targetType=" + targetType + '}';
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ ChangeStreamEvent> that = (ChangeStreamEvent>) o;
+
+ if (!ObjectUtils.nullSafeEquals(this.raw, that.raw)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(this.targetType, that.targetType);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = raw != null ? raw.hashCode() : 0;
+ result = 31 * result + ObjectUtils.nullSafeHashCode(targetType);
+ return result;
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java
new file mode 100644
index 0000000000..aaee3b76af
--- /dev/null
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ChangeStreamOptions.java
@@ -0,0 +1,444 @@
+/*
+ * Copyright 2018-2025 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.core;
+
+import java.time.Instant;
+import java.util.Arrays;
+import java.util.Optional;
+
+import org.bson.BsonDocument;
+import org.bson.BsonTimestamp;
+import org.bson.BsonValue;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.aggregation.Aggregation;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+import org.springframework.util.ObjectUtils;
+
+import com.mongodb.client.model.changestream.ChangeStreamDocument;
+import com.mongodb.client.model.changestream.FullDocument;
+import com.mongodb.client.model.changestream.FullDocumentBeforeChange;
+
+/**
+ * Options applicable to MongoDB Change Streams . Intended
+ * to be used along with {@link org.springframework.data.mongodb.core.messaging.ChangeStreamRequest} in a sync world as
+ * well {@link ReactiveMongoOperations} if you prefer it that way.
+ *
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Myroslav Kosinskyi
+ * @since 2.1
+ */
+public class ChangeStreamOptions {
+
+ private @Nullable Object filter;
+ private @Nullable BsonValue resumeToken;
+ private @Nullable FullDocument fullDocumentLookup;
+ private @Nullable FullDocumentBeforeChange fullDocumentBeforeChangeLookup;
+ private @Nullable Collation collation;
+ private @Nullable Object resumeTimestamp;
+ private Resume resume = Resume.UNDEFINED;
+
+ protected ChangeStreamOptions() {}
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getFilter() {
+ return Optional.ofNullable(filter);
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getResumeToken() {
+ return Optional.ofNullable(resumeToken);
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getFullDocumentLookup() {
+ return Optional.ofNullable(fullDocumentLookup);
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ * @since 4.0
+ */
+ public Optional getFullDocumentBeforeChangeLookup() {
+ return Optional.ofNullable(fullDocumentBeforeChangeLookup);
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getCollation() {
+ return Optional.ofNullable(collation);
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getResumeTimestamp() {
+ return Optional.ofNullable(resumeTimestamp).map(timestamp -> asTimestampOfType(timestamp, Instant.class));
+ }
+
+ /**
+ * @return {@link Optional#empty()} if not set.
+ * @since 2.2
+ */
+ public Optional getResumeBsonTimestamp() {
+ return Optional.ofNullable(resumeTimestamp).map(timestamp -> asTimestampOfType(timestamp, BsonTimestamp.class));
+ }
+
+ /**
+ * @return {@literal true} if the change stream should be started after the {@link #getResumeToken() token}.
+ * @since 2.2
+ */
+ public boolean isStartAfter() {
+ return Resume.START_AFTER.equals(resume);
+ }
+
+ /**
+ * @return {@literal true} if the change stream should be resumed after the {@link #getResumeToken() token}.
+ * @since 2.2
+ */
+ public boolean isResumeAfter() {
+ return Resume.RESUME_AFTER.equals(resume);
+ }
+
+ /**
+ * @return empty {@link ChangeStreamOptions}.
+ */
+ public static ChangeStreamOptions empty() {
+ return ChangeStreamOptions.builder().build();
+ }
+
+ /**
+ * Obtain a shiny new {@link ChangeStreamOptionsBuilder} and start defining options in this fancy fluent way. Just
+ * don't forget to call {@link ChangeStreamOptionsBuilder#build() build()} when done.
+ *
+ * @return new instance of {@link ChangeStreamOptionsBuilder}.
+ */
+ public static ChangeStreamOptionsBuilder builder() {
+ return new ChangeStreamOptionsBuilder();
+ }
+
+ private static T asTimestampOfType(Object timestamp, Class targetType) {
+ return targetType.cast(doGetTimestamp(timestamp, targetType));
+ }
+
+ private static Object doGetTimestamp(Object timestamp, Class targetType) {
+
+ if (ClassUtils.isAssignableValue(targetType, timestamp)) {
+ return timestamp;
+ }
+
+ if (timestamp instanceof Instant instant) {
+ return new BsonTimestamp((int) instant.getEpochSecond(), 0);
+ }
+
+ if (timestamp instanceof BsonTimestamp bsonTimestamp) {
+ return Instant.ofEpochSecond(bsonTimestamp.getTime());
+ }
+
+ throw new IllegalArgumentException(
+ "o_O that should actually not happen; The timestamp should be an Instant or a BsonTimestamp but was "
+ + ObjectUtils.nullSafeClassName(timestamp));
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o)
+ return true;
+ if (o == null || getClass() != o.getClass())
+ return false;
+
+ ChangeStreamOptions that = (ChangeStreamOptions) o;
+
+ if (!ObjectUtils.nullSafeEquals(this.filter, that.filter)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(this.resumeToken, that.resumeToken)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(this.fullDocumentLookup, that.fullDocumentLookup)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(this.fullDocumentBeforeChangeLookup, that.fullDocumentBeforeChangeLookup)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(this.collation, that.collation)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(this.resumeTimestamp, that.resumeTimestamp)) {
+ return false;
+ }
+ return resume == that.resume;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(filter);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(resumeToken);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(fullDocumentLookup);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(fullDocumentBeforeChangeLookup);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(collation);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(resumeTimestamp);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(resume);
+ return result;
+ }
+
+ /**
+ * @author Christoph Strobl
+ * @since 2.2
+ */
+ enum Resume {
+
+ UNDEFINED,
+
+ /**
+ * @see com.mongodb.client.ChangeStreamIterable#startAfter(BsonDocument)
+ */
+ START_AFTER,
+
+ /**
+ * @see com.mongodb.client.ChangeStreamIterable#resumeAfter(BsonDocument)
+ */
+ RESUME_AFTER
+ }
+
+ /**
+ * Builder for creating {@link ChangeStreamOptions}.
+ *
+ * @author Christoph Strobl
+ * @since 2.1
+ */
+ public static class ChangeStreamOptionsBuilder {
+
+ private @Nullable Object filter;
+ private @Nullable BsonValue resumeToken;
+ private @Nullable FullDocument fullDocumentLookup;
+ private @Nullable FullDocumentBeforeChange fullDocumentBeforeChangeLookup;
+ private @Nullable Collation collation;
+ private @Nullable Object resumeTimestamp;
+ private Resume resume = Resume.UNDEFINED;
+
+ private ChangeStreamOptionsBuilder() {}
+
+ /**
+ * Set the collation to use.
+ *
+ * @param collation must not be {@literal null} nor {@literal empty}.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder collation(Collation collation) {
+
+ Assert.notNull(collation, "Collation must not be null nor empty");
+
+ this.collation = collation;
+ return this;
+ }
+
+ /**
+ * Set the filter to apply.
+ *
+ * Fields on aggregation expression root level are prefixed to map to fields contained in
+ * {@link ChangeStreamDocument#getFullDocument() fullDocument}. However {@literal operationType}, {@literal ns},
+ * {@literal documentKey} and {@literal fullDocument} are reserved words that will be omitted, and therefore taken
+ * as given, during the mapping procedure. You may want to have a look at the
+ * structure of Change Events .
+ *
+ * Use {@link org.springframework.data.mongodb.core.aggregation.TypedAggregation} to ensure filter expressions are
+ * mapped to domain type fields.
+ *
+ * @param filter the {@link Aggregation Aggregation pipeline} to apply for filtering events. Must not be
+ * {@literal null}.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder filter(Aggregation filter) {
+
+ Assert.notNull(filter, "Filter must not be null");
+
+ this.filter = filter;
+ return this;
+ }
+
+ /**
+ * Set the plain filter chain to apply.
+ *
+ * @param filter must not be {@literal null} nor contain {@literal null} values.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder filter(Document... filter) {
+
+ Assert.noNullElements(filter, "Filter must not contain null values");
+
+ this.filter = Arrays.asList(filter);
+ return this;
+ }
+
+ /**
+ * Set the resume token (typically a {@link org.bson.BsonDocument} containing a {@link org.bson.BsonBinary binary
+ * token}) after which to start with listening.
+ *
+ * @param resumeToken must not be {@literal null}.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder resumeToken(BsonValue resumeToken) {
+
+ Assert.notNull(resumeToken, "ResumeToken must not be null");
+
+ this.resumeToken = resumeToken;
+
+ if (this.resume == Resume.UNDEFINED) {
+ this.resume = Resume.RESUME_AFTER;
+ }
+
+ return this;
+ }
+
+ /**
+ * Set the {@link FullDocument} lookup to {@link FullDocument#UPDATE_LOOKUP}.
+ *
+ * @return this.
+ * @see #fullDocumentLookup(FullDocument)
+ */
+ public ChangeStreamOptionsBuilder returnFullDocumentOnUpdate() {
+ return fullDocumentLookup(FullDocument.UPDATE_LOOKUP);
+ }
+
+ /**
+ * Set the {@link FullDocument} lookup to use.
+ *
+ * @param lookup must not be {@literal null}.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder fullDocumentLookup(FullDocument lookup) {
+
+ Assert.notNull(lookup, "Lookup must not be null");
+
+ this.fullDocumentLookup = lookup;
+ return this;
+ }
+
+ /**
+ * Set the {@link FullDocumentBeforeChange} lookup to use.
+ *
+ * @param lookup must not be {@literal null}.
+ * @return this.
+ * @since 4.0
+ */
+ public ChangeStreamOptionsBuilder fullDocumentBeforeChangeLookup(FullDocumentBeforeChange lookup) {
+
+ Assert.notNull(lookup, "Lookup must not be null");
+
+ this.fullDocumentBeforeChangeLookup = lookup;
+ return this;
+ }
+
+ /**
+ * Return the full document before being changed if it is available.
+ *
+ * @return this.
+ * @since 4.0
+ * @see #fullDocumentBeforeChangeLookup(FullDocumentBeforeChange)
+ */
+ public ChangeStreamOptionsBuilder returnFullDocumentBeforeChange() {
+ return fullDocumentBeforeChangeLookup(FullDocumentBeforeChange.WHEN_AVAILABLE);
+ }
+
+ /**
+ * Set the cluster time to resume from.
+ *
+ * @param resumeTimestamp must not be {@literal null}.
+ * @return this.
+ */
+ public ChangeStreamOptionsBuilder resumeAt(Instant resumeTimestamp) {
+
+ Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null");
+
+ this.resumeTimestamp = resumeTimestamp;
+ return this;
+ }
+
+ /**
+ * Set the cluster time to resume from.
+ *
+ * @param resumeTimestamp must not be {@literal null}.
+ * @return this.
+ * @since 2.2
+ */
+ public ChangeStreamOptionsBuilder resumeAt(BsonTimestamp resumeTimestamp) {
+
+ Assert.notNull(resumeTimestamp, "ResumeTimestamp must not be null");
+
+ this.resumeTimestamp = resumeTimestamp;
+ return this;
+ }
+
+ /**
+ * Set the resume token after which to continue emitting notifications.
+ *
+ * @param resumeToken must not be {@literal null}.
+ * @return this.
+ * @since 2.2
+ */
+ public ChangeStreamOptionsBuilder resumeAfter(BsonValue resumeToken) {
+
+ resumeToken(resumeToken);
+ this.resume = Resume.RESUME_AFTER;
+
+ return this;
+ }
+
+ /**
+ * Set the resume token after which to start emitting notifications.
+ *
+ * @param resumeToken must not be {@literal null}.
+ * @return this.
+ * @since 2.2
+ */
+ public ChangeStreamOptionsBuilder startAfter(BsonValue resumeToken) {
+
+ resumeToken(resumeToken);
+ this.resume = Resume.START_AFTER;
+
+ return this;
+ }
+
+ /**
+ * @return the built {@link ChangeStreamOptions}
+ */
+ public ChangeStreamOptions build() {
+
+ ChangeStreamOptions options = new ChangeStreamOptions();
+
+ options.filter = this.filter;
+ options.resumeToken = this.resumeToken;
+ options.fullDocumentLookup = this.fullDocumentLookup;
+ options.fullDocumentBeforeChangeLookup = this.fullDocumentBeforeChangeLookup;
+ options.collation = this.collation;
+ options.resumeTimestamp = this.resumeTimestamp;
+ options.resume = this.resume;
+
+ return options;
+ }
+ }
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
index 97b32f6119..c142aca173 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionCallback.java
@@ -1,26 +1,46 @@
-/*
- * Copyright 2010-2011 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
- *
- * http://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.core;
-
-import com.mongodb.DBCollection;
-import com.mongodb.MongoException;
-import org.springframework.dao.DataAccessException;
-
-public interface CollectionCallback {
-
- T doInCollection(DBCollection collection) throws MongoException, DataAccessException;
-
-}
+/*
+ * Copyright 2010-2025 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.core;
+
+import org.bson.Document;
+import org.springframework.dao.DataAccessException;
+import org.springframework.lang.Nullable;
+
+import com.mongodb.MongoException;
+import com.mongodb.client.MongoCollection;
+
+/**
+ * Callback interface for executing actions against a {@link MongoCollection}.
+ *
+ * @author Mark Pollak
+ * @author Grame Rocher
+ * @author Oliver Gierke
+ * @author John Brisbin
+ * @author Christoph Strobl
+ * @since 1.0
+ */
+public interface CollectionCallback {
+
+ /**
+ * @param collection never {@literal null}.
+ * @return can be {@literal null}.
+ * @throws MongoException
+ * @throws DataAccessException
+ */
+ @Nullable
+ T doInCollection(MongoCollection collection) throws MongoException, DataAccessException;
+
+}
diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
index 756e2863e4..5df30e0b92 100644
--- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
+++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/CollectionOptions.java
@@ -1,70 +1,1048 @@
-/*
- * Copyright 2010-2011 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
- *
- * http://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.core;
-
-/**
- * Provides a simple wrapper to encapsulate the variety of settings you can use when creating a collection.
- *
- * @author Thomas Risberg
- */
-public class CollectionOptions {
-
- private Integer maxDocuments;
-
- private Integer size;
-
- private Boolean capped;
-
- /**
- * Constructs a new CollectionOptions
instance.
- *
- * @param size the collection size in bytes, this data space is preallocated
- * @param maxDocuments the maximum number of documents in the collection.
- * @param capped true to created a "capped" collection (fixed size with auto-FIFO behavior based on insertion order),
- * false otherwise.
- */
- public CollectionOptions(Integer size, Integer maxDocuments, Boolean capped) {
- super();
- this.maxDocuments = maxDocuments;
- this.size = size;
- this.capped = capped;
- }
-
- public Integer getMaxDocuments() {
- return maxDocuments;
- }
-
- public void setMaxDocuments(Integer maxDocuments) {
- this.maxDocuments = maxDocuments;
- }
-
- public Integer getSize() {
- return size;
- }
-
- public void setSize(Integer size) {
- this.size = size;
- }
-
- public Boolean getCapped() {
- return capped;
- }
-
- public void setCapped(Boolean capped) {
- this.capped = capped;
- }
-
-}
+/*
+ * Copyright 2010-2025 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.core;
+
+import java.nio.charset.StandardCharsets;
+import java.time.Duration;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.StreamSupport;
+
+import org.bson.BsonBinary;
+import org.bson.BsonBinarySubType;
+import org.bson.BsonNull;
+import org.bson.Document;
+import org.springframework.data.mongodb.core.mapping.Field;
+import org.springframework.data.mongodb.core.query.Collation;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.QueryableJsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.JsonSchemaProperty;
+import org.springframework.data.mongodb.core.schema.MongoJsonSchema;
+import org.springframework.data.mongodb.core.schema.QueryCharacteristic;
+import org.springframework.data.mongodb.core.timeseries.Granularity;
+import org.springframework.data.mongodb.core.timeseries.GranularityDefinition;
+import org.springframework.data.mongodb.core.validation.Validator;
+import org.springframework.data.util.Optionals;
+import org.springframework.lang.CheckReturnValue;
+import org.springframework.lang.Contract;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ObjectUtils;
+
+import com.mongodb.client.model.ValidationAction;
+import com.mongodb.client.model.ValidationLevel;
+
+/**
+ * Provides a simple wrapper to encapsulate the variety of settings you can use when creating a collection.
+ *
+ * @author Thomas Risberg
+ * @author Christoph Strobl
+ * @author Mark Paluch
+ * @author Andreas Zink
+ * @author Ben Foster
+ * @author Ross Lawley
+ */
+public class CollectionOptions {
+
+ private @Nullable Long maxDocuments;
+ private @Nullable Long size;
+ private @Nullable Boolean capped;
+ private @Nullable Collation collation;
+ private ValidationOptions validationOptions;
+ private @Nullable TimeSeriesOptions timeSeriesOptions;
+ private @Nullable CollectionChangeStreamOptions changeStreamOptions;
+ private @Nullable EncryptedFieldsOptions encryptedFieldsOptions;
+
+ private CollectionOptions(@Nullable Long size, @Nullable Long maxDocuments, @Nullable Boolean capped,
+ @Nullable Collation collation, ValidationOptions validationOptions, @Nullable TimeSeriesOptions timeSeriesOptions,
+ @Nullable CollectionChangeStreamOptions changeStreamOptions,
+ @Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
+
+ this.maxDocuments = maxDocuments;
+ this.size = size;
+ this.capped = capped;
+ this.collation = collation;
+ this.validationOptions = validationOptions;
+ this.timeSeriesOptions = timeSeriesOptions;
+ this.changeStreamOptions = changeStreamOptions;
+ this.encryptedFieldsOptions = encryptedFieldsOptions;
+ }
+
+ /**
+ * Create new {@link CollectionOptions} by just providing the {@link Collation} to use.
+ *
+ * @param collation must not be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public static CollectionOptions just(Collation collation) {
+
+ Assert.notNull(collation, "Collation must not be null");
+
+ return new CollectionOptions(null, null, null, collation, ValidationOptions.none(), null, null, null);
+ }
+
+ /**
+ * Create new empty {@link CollectionOptions}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public static CollectionOptions empty() {
+ return new CollectionOptions(null, null, null, null, ValidationOptions.none(), null, null, null);
+ }
+
+ /**
+ * Quick way to set up {@link CollectionOptions} for a Time Series collection. For more advanced settings use
+ * {@link #timeSeries(String, Function)}.
+ *
+ * @param timeField The name of the property which contains the date in each time series document. Must not be
+ * {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ * @see #timeSeries(TimeSeriesOptions)
+ * @since 3.3
+ */
+ public static CollectionOptions timeSeries(String timeField) {
+ return timeSeries(timeField, it -> it);
+ }
+
+ /**
+ * Set up {@link CollectionOptions} for a Time Series collection.
+ *
+ * @param timeField the name of the field that contains the date in each time series document.
+ * @param options a function to apply additional settings to {@link TimeSeriesOptions}.
+ * @return new instance of {@link CollectionOptions}.
+ * @since 4.4
+ */
+ public static CollectionOptions timeSeries(String timeField, Function options) {
+ return empty().timeSeries(options.apply(TimeSeriesOptions.timeSeries(timeField)));
+ }
+
+ /**
+ * Quick way to set up {@link CollectionOptions} for emitting (pre & post) change events.
+ *
+ * @return new instance of {@link CollectionOptions}.
+ * @see #changeStream(CollectionChangeStreamOptions)
+ * @see CollectionChangeStreamOptions#preAndPostImages(boolean)
+ * @since 4.0
+ */
+ public static CollectionOptions emitChangedRevisions() {
+ return empty().changeStream(CollectionChangeStreamOptions.preAndPostImages(true));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with the given {@code encryptedFields}.
+ *
+ * @param encryptedFieldsOptions can be null
+ * @return new instance of {@link CollectionOptions}.
+ * @since 4.5.0
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ public static CollectionOptions encryptedCollection(@Nullable EncryptedFieldsOptions encryptedFieldsOptions) {
+ return new CollectionOptions(null, null, null, null, ValidationOptions.NONE, null, null, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} reading encryption options from the given {@link MongoJsonSchema}.
+ *
+ * @param schema must not be {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ * @since 4.5.0
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ public static CollectionOptions encryptedCollection(MongoJsonSchema schema) {
+ return encryptedCollection(EncryptedFieldsOptions.fromSchema(schema));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} building encryption options in a fluent style.
+ *
+ * @param optionsFunction must not be {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ * @since 4.5.0
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ public static CollectionOptions encryptedCollection(
+ Function optionsFunction) {
+ return encryptedCollection(optionsFunction.apply(new EncryptedFieldsOptions()));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and capped set to {@literal true}.
+ * NOTE: Using capped collections requires defining {@link #size(long)}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public CollectionOptions capped() {
+ return new CollectionOptions(size, maxDocuments, true, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code maxDocuments} set to given value.
+ *
+ * @param maxDocuments can be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public CollectionOptions maxDocuments(long maxDocuments) {
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code size} set to given value.
+ *
+ * @param size can be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public CollectionOptions size(long size) {
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code collation} set to given value.
+ *
+ * @param collation can be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.0
+ */
+ public CollectionOptions collation(@Nullable Collation collation) {
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
+ * {@link MongoJsonSchema}.
+ *
+ * @param schema must not be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions schema(MongoJsonSchema schema) {
+ return validator(Validator.schema(schema));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationOptions} set to given
+ * {@link Validator}.
+ *
+ * @param validator can be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions validator(@Nullable Validator validator) {
+ return validation(validationOptions.validator(validator));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
+ * {@link ValidationLevel#OFF}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions disableValidation() {
+ return schemaValidationLevel(ValidationLevel.OFF);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
+ * {@link ValidationLevel#STRICT}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions strictValidation() {
+ return schemaValidationLevel(ValidationLevel.STRICT);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set to
+ * {@link ValidationLevel#MODERATE}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions moderateValidation() {
+ return schemaValidationLevel(ValidationLevel.MODERATE);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to
+ * {@link ValidationAction#WARN}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions warnOnValidationError() {
+ return schemaValidationAction(ValidationAction.WARN);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set to
+ * {@link ValidationAction#ERROR}.
+ *
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions failOnValidationError() {
+ return schemaValidationAction(ValidationAction.ERROR);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationLevel} set given
+ * {@link ValidationLevel}.
+ *
+ * @param validationLevel must not be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions schemaValidationLevel(ValidationLevel validationLevel) {
+
+ Assert.notNull(validationLevel, "ValidationLevel must not be null");
+ return validation(validationOptions.validationLevel(validationLevel));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with already given settings and {@code validationAction} set given
+ * {@link ValidationAction}.
+ *
+ * @param validationAction must not be {@literal null}.
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions schemaValidationAction(ValidationAction validationAction) {
+
+ Assert.notNull(validationAction, "ValidationAction must not be null");
+ return validation(validationOptions.validationAction(validationAction));
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with the given {@link ValidationOptions}.
+ *
+ * @param validationOptions must not be {@literal null}. Use {@link ValidationOptions#none()} to remove validation.
+ * @return new {@link CollectionOptions}.
+ * @since 2.1
+ */
+ public CollectionOptions validation(ValidationOptions validationOptions) {
+
+ Assert.notNull(validationOptions, "ValidationOptions must not be null");
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
+ *
+ * @param timeSeriesOptions must not be {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ * @since 3.3
+ */
+ public CollectionOptions timeSeries(TimeSeriesOptions timeSeriesOptions) {
+
+ Assert.notNull(timeSeriesOptions, "TimeSeriesOptions must not be null");
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Create new {@link CollectionOptions} with the given {@link TimeSeriesOptions}.
+ *
+ * @param changeStreamOptions must not be {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ * @since 3.3
+ */
+ public CollectionOptions changeStream(CollectionChangeStreamOptions changeStreamOptions) {
+
+ Assert.notNull(changeStreamOptions, "ChangeStreamOptions must not be null");
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Set the {@link EncryptedFieldsOptions} for collections using queryable encryption.
+ *
+ * @param encryptedFieldsOptions must not be {@literal null}.
+ * @return new instance of {@link CollectionOptions}.
+ */
+ @Contract("_ -> new")
+ @CheckReturnValue
+ public CollectionOptions encrypted(EncryptedFieldsOptions encryptedFieldsOptions) {
+
+ Assert.notNull(encryptedFieldsOptions, "EncryptedCollectionOptions must not be null");
+ return new CollectionOptions(size, maxDocuments, capped, collation, validationOptions, timeSeriesOptions,
+ changeStreamOptions, encryptedFieldsOptions);
+ }
+
+ /**
+ * Get the max number of documents the collection should be limited to.
+ *
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getMaxDocuments() {
+ return Optional.ofNullable(maxDocuments);
+ }
+
+ /**
+ * Get the {@literal size} in bytes the collection should be limited to.
+ *
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getSize() {
+ return Optional.ofNullable(size);
+ }
+
+ /**
+ * Get if the collection should be capped.
+ *
+ * @return {@link Optional#empty()} if not set.
+ * @since 2.0
+ */
+ public Optional getCapped() {
+ return Optional.ofNullable(capped);
+ }
+
+ /**
+ * Get the {@link Collation} settings.
+ *
+ * @return {@link Optional#empty()} if not set.
+ * @since 2.0
+ */
+ public Optional getCollation() {
+ return Optional.ofNullable(collation);
+ }
+
+ /**
+ * Get the {@link MongoJsonSchema} for the collection.
+ *
+ * @return {@link Optional#empty()} if not set.
+ * @since 2.1
+ */
+ public Optional getValidationOptions() {
+ return validationOptions.isEmpty() ? Optional.empty() : Optional.of(validationOptions);
+ }
+
+ /**
+ * Get the {@link TimeSeriesOptions} if available.
+ *
+ * @return {@link Optional#empty()} if not specified.
+ * @since 3.3
+ */
+ public Optional getTimeSeriesOptions() {
+ return Optional.ofNullable(timeSeriesOptions);
+ }
+
+ /**
+ * Get the {@link CollectionChangeStreamOptions} if available.
+ *
+ * @return {@link Optional#empty()} if not specified.
+ * @since 4.0
+ */
+ public Optional getChangeStreamOptions() {
+ return Optional.ofNullable(changeStreamOptions);
+ }
+
+ /**
+ * Get the {@code encryptedFields} if available.
+ *
+ * @return {@link Optional#empty()} if not specified.
+ * @since 4.5
+ */
+ public Optional getEncryptedFieldsOptions() {
+ return Optional.ofNullable(encryptedFieldsOptions);
+ }
+
+ @Override
+ public String toString() {
+ return "CollectionOptions{" + "maxDocuments=" + maxDocuments + ", size=" + size + ", capped=" + capped
+ + ", collation=" + collation + ", validationOptions=" + validationOptions + ", timeSeriesOptions="
+ + timeSeriesOptions + ", changeStreamOptions=" + changeStreamOptions + ", encryptedCollectionOptions="
+ + encryptedFieldsOptions + ", disableValidation=" + disableValidation() + ", strictValidation="
+ + strictValidation() + ", moderateValidation=" + moderateValidation() + ", warnOnValidationError="
+ + warnOnValidationError() + ", failOnValidationError=" + failOnValidationError() + '}';
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ CollectionOptions that = (CollectionOptions) o;
+
+ if (!ObjectUtils.nullSafeEquals(maxDocuments, that.maxDocuments)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(size, that.size)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(capped, that.capped)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(collation, that.collation)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(validationOptions, that.validationOptions)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(timeSeriesOptions, that.timeSeriesOptions)) {
+ return false;
+ }
+ if (!ObjectUtils.nullSafeEquals(changeStreamOptions, that.changeStreamOptions)) {
+ return false;
+ }
+ return ObjectUtils.nullSafeEquals(encryptedFieldsOptions, that.encryptedFieldsOptions);
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(maxDocuments);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(size);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(capped);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(collation);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(validationOptions);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(timeSeriesOptions);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(changeStreamOptions);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(encryptedFieldsOptions);
+ return result;
+ }
+
+ /**
+ * Encapsulation of ValidationOptions options.
+ *
+ * @author Christoph Strobl
+ * @author Andreas Zink
+ * @since 2.1
+ */
+ public static class ValidationOptions {
+
+ private static final ValidationOptions NONE = new ValidationOptions(null, null, null);
+
+ private final @Nullable Validator validator;
+ private final @Nullable ValidationLevel validationLevel;
+ private final @Nullable ValidationAction validationAction;
+
+ public ValidationOptions(@Nullable Validator validator, @Nullable ValidationLevel validationLevel,
+ @Nullable ValidationAction validationAction) {
+
+ this.validator = validator;
+ this.validationLevel = validationLevel;
+ this.validationAction = validationAction;
+ }
+
+ /**
+ * Create an empty {@link ValidationOptions}.
+ *
+ * @return never {@literal null}.
+ */
+ public static ValidationOptions none() {
+ return NONE;
+ }
+
+ /**
+ * Define the {@link Validator} to be used for document validation.
+ *
+ * @param validator can be {@literal null}.
+ * @return new instance of {@link ValidationOptions}.
+ */
+ public ValidationOptions validator(@Nullable Validator validator) {
+ return new ValidationOptions(validator, validationLevel, validationAction);
+ }
+
+ /**
+ * Define the validation level to apply.
+ *
+ * @param validationLevel can be {@literal null}.
+ * @return new instance of {@link ValidationOptions}.
+ */
+ public ValidationOptions validationLevel(ValidationLevel validationLevel) {
+ return new ValidationOptions(validator, validationLevel, validationAction);
+ }
+
+ /**
+ * Define the validation action to take.
+ *
+ * @param validationAction can be {@literal null}.
+ * @return new instance of {@link ValidationOptions}.
+ */
+ public ValidationOptions validationAction(ValidationAction validationAction) {
+ return new ValidationOptions(validator, validationLevel, validationAction);
+ }
+
+ /**
+ * Get the {@link Validator} to use.
+ *
+ * @return never {@literal null}.
+ */
+ public Optional getValidator() {
+ return Optional.ofNullable(validator);
+ }
+
+ /**
+ * Get the {@code validationLevel} to apply.
+ *
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getValidationLevel() {
+ return Optional.ofNullable(validationLevel);
+ }
+
+ /**
+ * Get the {@code validationAction} to perform.
+ *
+ * @return {@link Optional#empty()} if not set.
+ */
+ public Optional getValidationAction() {
+ return Optional.ofNullable(validationAction);
+ }
+
+ /**
+ * @return {@literal true} if no arguments set.
+ */
+ boolean isEmpty() {
+ return !Optionals.isAnyPresent(getValidator(), getValidationAction(), getValidationLevel());
+ }
+
+ @Override
+ public String toString() {
+
+ return "ValidationOptions{" + "validator=" + validator + ", validationLevel=" + validationLevel
+ + ", validationAction=" + validationAction + '}';
+ }
+
+ @Override
+ public boolean equals(@Nullable Object o) {
+ if (this == o) {
+ return true;
+ }
+ if (o == null || getClass() != o.getClass()) {
+ return false;
+ }
+
+ ValidationOptions that = (ValidationOptions) o;
+
+ if (!ObjectUtils.nullSafeEquals(validator, that.validator)) {
+ return false;
+ }
+ if (validationLevel != that.validationLevel)
+ return false;
+ return validationAction == that.validationAction;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = ObjectUtils.nullSafeHashCode(validator);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(validationLevel);
+ result = 31 * result + ObjectUtils.nullSafeHashCode(validationAction);
+ return result;
+ }
+ }
+
+ /**
+ * Encapsulation of Encryption options for collections.
+ *
+ * @author Christoph Strobl
+ * @since 4.5
+ */
+ public static class EncryptedFieldsOptions {
+
+ private static final EncryptedFieldsOptions NONE = new EncryptedFieldsOptions();
+
+ private final @Nullable MongoJsonSchema schema;
+ private final List queryableProperties;
+
+ EncryptedFieldsOptions() {
+ this(null, List.of());
+ }
+
+ private EncryptedFieldsOptions(@Nullable MongoJsonSchema schema,
+ List queryableProperties) {
+
+ this.schema = schema;
+ this.queryableProperties = queryableProperties;
+ }
+
+ /**
+ * @return {@link EncryptedFieldsOptions#NONE}
+ */
+ public static EncryptedFieldsOptions none() {
+ return NONE;
+ }
+
+ /**
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ public static EncryptedFieldsOptions fromSchema(MongoJsonSchema schema) {
+ return new EncryptedFieldsOptions(schema, List.of());
+ }
+
+ /**
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ public static EncryptedFieldsOptions fromProperties(List properties) {
+ return new EncryptedFieldsOptions(null, List.copyOf(properties));
+ }
+
+ /**
+ * Add a new {@link QueryableJsonSchemaProperty queryable property} for the given source property.
+ *
+ * Please note that, a given {@link JsonSchemaProperty} may override options from a given {@link MongoJsonSchema} if
+ * set.
+ *
+ * @param property the queryable source - typically
+ * {@link org.springframework.data.mongodb.core.schema.IdentifiableJsonSchemaProperty.EncryptedJsonSchemaProperty
+ * encrypted}.
+ * @param characteristics the query options to set.
+ * @return new instance of {@link EncryptedFieldsOptions}.
+ */
+ @Contract("_, _ -> new")
+ @CheckReturnValue
+ public EncryptedFieldsOptions queryable(JsonSchemaProperty property, QueryCharacteristic... characteristics) {
+
+ List targetPropertyList = new ArrayList<>(queryableProperties.size() + 1);
+ targetPropertyList.addAll(queryableProperties);
+ targetPropertyList.add(JsonSchemaProperty.queryable(property, List.of(characteristics)));
+
+ return new EncryptedFieldsOptions(schema, targetPropertyList);
+ }
+
+ public Document toDocument() {
+ return new Document("fields", selectPaths());
+ }
+
+ private List selectPaths() {
+
+ Map