diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java index 1f51240f4..1469dbf64 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/MongoDbFactory.java @@ -95,7 +95,7 @@ public interface MongoDbFactory extends CodecRegistryProvider { * Obtain a {@link ClientSession} bound instance of {@link MongoDbFactory} returning {@link MongoDatabase} instances * that are aware and bound to the given session. * - * @param options must not be {@literal null}. + * @param session must not be {@literal null}. * @return never {@literal null}. * @since 2.1 */ 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 index 484b1d934..889fae8ba 100644 --- 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 @@ -83,10 +83,9 @@ public interface ReactiveMongoDatabaseFactory extends CodecRegistryProvider { * Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoDatabaseFactory} returning * {@link MongoDatabase} instances that are aware and bound to the given session. * - * @param options must not be {@literal null}. + * @param session must not be {@literal null}. * @return never {@literal null}. * @since 2.1 */ ReactiveMongoDatabaseFactory withSession(ClientSession session); - } 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 index 5925115e0..03123ee2e 100644 --- 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 @@ -24,9 +24,9 @@ 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.ObjectUtils; import org.springframework.util.ReflectionUtils; import com.mongodb.WriteConcern; @@ -40,6 +40,10 @@ import com.mongodb.session.ClientSession; * 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 { @@ -47,8 +51,8 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { private static final MethodCache METHOD_CACHE = new MethodCache(); private final ClientSession session; - private final BiFunction collectionDecorator; - private final BiFunction databaseDecorator; + private final ClientSessionOperator collectionDecorator; + private final ClientSessionOperator databaseDecorator; private final Object target; private final Class targetType; private final Class collectionType; @@ -56,20 +60,27 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { /** * 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 BiFunction} used to create the proxy for an imperative / reactive + * @param databaseDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive * {@code MongoDatabase}. * @param collectionType the MongoDB collection type. - * @param collectionCallback a {@link BiFunction} used to create the proxy for an imperative / reactive + * @param collectionDecorator a {@link ClientSessionOperator} used to create the proxy for an imperative / reactive * {@code MongoCollection}. - * @param + * @param target object type. */ public SessionAwareMethodInterceptor(ClientSession session, T target, Class databaseType, - BiFunction databaseDecorator, Class collectionType, - BiFunction collectionDecorator) { + ClientSessionOperator databaseDecorator, Class collectionType, + ClientSessionOperator collectionDecorator) { + + Assert.notNull(session, "ClientSession must not be null!"); + Assert.notNull(target, "Target 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; @@ -85,10 +96,11 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { * (non-Javadoc) * @see org.aopalliance.intercept.MethodInterceptor(org.aopalliance.intercept.MethodInvocation) */ + @Nullable @Override public Object invoke(MethodInvocation methodInvocation) throws Throwable { - if (requiresDecoration(methodInvocation)) { + if (requiresDecoration(methodInvocation.getMethod())) { Object target = methodInvocation.proceed(); if (target instanceof Proxy) { @@ -98,43 +110,47 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { return decorate(target); } - if (!requiresSession(methodInvocation)) { + if (!requiresSession(methodInvocation.getMethod())) { return methodInvocation.proceed(); } Optional targetMethod = METHOD_CACHE.lookup(methodInvocation.getMethod(), targetType); return !targetMethod.isPresent() ? methodInvocation.proceed() - : ReflectionUtils.invokeMethod(targetMethod.get(), target, prependSessionToArguments(methodInvocation)); + : ReflectionUtils.invokeMethod(targetMethod.get(), target, + prependSessionToArguments(session, methodInvocation)); } - private boolean requiresDecoration(MethodInvocation methodInvocation) { + private boolean requiresDecoration(Method method) { - return ClassUtils.isAssignable(databaseType, methodInvocation.getMethod().getReturnType()) - || ClassUtils.isAssignable(collectionType, methodInvocation.getMethod().getReturnType()); + 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 boolean requiresSession(MethodInvocation methodInvocation) { + private static boolean requiresSession(Method method) { - if (ObjectUtils.isEmpty(methodInvocation.getMethod().getParameterTypes()) - || !ClassUtils.isAssignable(ClientSession.class, methodInvocation.getMethod().getParameterTypes()[0])) { + if (method.getParameterCount() == 0 + || !ClassUtils.isAssignable(ClientSession.class, method.getParameterTypes()[0])) { return true; } return false; } - private Object[] prependSessionToArguments(MethodInvocation invocation) { + 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; } @@ -148,6 +164,13 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { private final ConcurrentReferenceHashMap> cache = new ConcurrentReferenceHashMap<>(); + /** + * Lookup the target {@link Method}. + * + * @param method + * @param targetClass + * @return + */ Optional lookup(Method method, Class targetClass) { return cache.computeIfAbsent(new MethodClassKey(method, targetClass), @@ -165,9 +188,24 @@ public class SessionAwareMethodInterceptor implements MethodInterceptor { 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/core/DefaultIndexOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java index 3ec168b72..0574f5711 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/DefaultIndexOperations.java @@ -97,12 +97,15 @@ public class DefaultIndexOperations implements IndexOperations { * Creates a new {@link DefaultIndexOperations}. * * @param mongoOperations must not be {@literal null}. - * @param collectionName must not be {@literal null}. + * @param collectionName must not be {@literal null} or empty. * @param type can be {@literal null}. * @since 2.1 */ public DefaultIndexOperations(MongoOperations mongoOperations, String collectionName, @Nullable Class type) { + Assert.notNull(mongoOperations, "MongoOperations must not be null!"); + Assert.hasText(collectionName, "Collection name must not be null or empty!"); + this.mongoOperations = mongoOperations; this.mapper = new QueryMapper(mongoOperations.getConverter()); this.collectionName = collectionName; diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java index bc679de64..4e68d52a2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoExceptionTranslator.java @@ -132,6 +132,8 @@ public class MongoExceptionTranslator implements PersistenceExceptionTranslator return new UncategorizedMongoDbException(ex.getMessage(), ex); } + // may interfere with OmitStackTraceInFastThrow (enabled by default). + // see https://jira.spring.io/browse/DATAMONGO-1905 if (ex instanceof IllegalStateException) { for (StackTraceElement elm : ex.getStackTrace()) { if (elm.getClassName().contains("ClientSession")) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java index 2896cb40e..b5f5a9a2e 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoOperations.java @@ -41,6 +41,7 @@ import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.data.util.CloseableIterator; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; import com.mongodb.ClientSessionOptions; import com.mongodb.Cursor; @@ -156,8 +157,8 @@ public interface MongoOperations extends FluentMongoOperations { T execute(String collectionName, CollectionCallback action); /** - * Obtain a session bound instance of {@link SessionScoped} binding a new {@link ClientSession} with given - * {@literal sessionOptions} to each and every command issued against MongoDB. + * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession} + * with given {@literal sessionOptions} to each and every command issued against MongoDB. * * @param sessionOptions must not be {@literal null}. * @return new instance of {@link SessionScoped}. Never {@literal null}. @@ -166,17 +167,19 @@ public interface MongoOperations extends FluentMongoOperations { SessionScoped withSession(ClientSessionOptions sessionOptions); /** - * Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given - * {@link Supplier} to each and every command issued against MongoDB. + * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession} + * provided by the given {@link Supplier} to each and every command issued against MongoDB. *

- * Note: It is up to the caller to manage the {@link ClientSession} lifecycle. + * Note: It is up to the caller to manage the {@link ClientSession} lifecycle. Use the + * {@link SessionScoped#execute(SessionCallback, Consumer)} hook to potentially close the {@link ClientSession}. * * @param sessionProvider must not be {@literal null}. - * @param onComplete a simple hook called when done .Must not be {@literal null}. * @since 2.1 */ default SessionScoped withSession(Supplier sessionProvider) { + Assert.notNull(sessionProvider, "SessionProvider must not be null!"); + return new SessionScoped() { private final Object lock = new Object(); @@ -201,7 +204,7 @@ public interface MongoOperations extends FluentMongoOperations { } /** - * Obtain a {@link ClientSession} bound instance of MongoOperations. + * Obtain a {@link ClientSession} bound instance of {@link MongoOperations}. *

* Note: It is up to the caller to manage the {@link ClientSession} lifecycle. * diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java index 68d281b5b..733ca07ee 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/MongoTemplate.java @@ -71,16 +71,7 @@ import org.springframework.data.mongodb.core.aggregation.AggregationResults; import org.springframework.data.mongodb.core.aggregation.Fields; import org.springframework.data.mongodb.core.aggregation.TypeBasedAggregationOperationContext; import org.springframework.data.mongodb.core.aggregation.TypedAggregation; -import org.springframework.data.mongodb.core.convert.DbRefResolver; -import org.springframework.data.mongodb.core.convert.DefaultDbRefResolver; -import org.springframework.data.mongodb.core.convert.JsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MappingMongoConverter; -import org.springframework.data.mongodb.core.convert.MongoConverter; -import org.springframework.data.mongodb.core.convert.MongoCustomConversions; -import org.springframework.data.mongodb.core.convert.MongoJsonSchemaMapper; -import org.springframework.data.mongodb.core.convert.MongoWriter; -import org.springframework.data.mongodb.core.convert.QueryMapper; -import org.springframework.data.mongodb.core.convert.UpdateMapper; +import org.springframework.data.mongodb.core.convert.*; import org.springframework.data.mongodb.core.index.IndexOperations; import org.springframework.data.mongodb.core.index.IndexOperationsProvider; import org.springframework.data.mongodb.core.index.MongoMappingEventPublisher; @@ -141,16 +132,7 @@ import com.mongodb.client.MongoCollection; import com.mongodb.client.MongoCursor; import com.mongodb.client.MongoDatabase; import com.mongodb.client.MongoIterable; -import com.mongodb.client.model.CountOptions; -import com.mongodb.client.model.CreateCollectionOptions; -import com.mongodb.client.model.DeleteOptions; -import com.mongodb.client.model.Filters; -import com.mongodb.client.model.FindOneAndDeleteOptions; -import com.mongodb.client.model.FindOneAndUpdateOptions; -import com.mongodb.client.model.ReturnDocument; -import com.mongodb.client.model.UpdateOptions; -import com.mongodb.client.model.ValidationAction; -import com.mongodb.client.model.ValidationLevel; +import com.mongodb.client.model.*; import com.mongodb.client.result.DeleteResult; import com.mongodb.client.result.UpdateResult; import com.mongodb.session.ClientSession; @@ -273,7 +255,6 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, this.updateMapper = that.updateMapper; this.schemaMapper = that.schemaMapper; this.projectionFactory = that.projectionFactory; - this.mappingContext = that.mappingContext; } @@ -522,10 +503,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ public T execute(DbCallback action) { - Assert.notNull(action, "DbCallbackmust not be null!"); + Assert.notNull(action, "DbCallback must not be null!"); try { - MongoDatabase db = prepareDatabase(this.getDbInternal()); + MongoDatabase db = prepareDatabase(this.doGetDatabase()); return action.doInDB(db); } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -552,7 +533,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, Assert.notNull(callback, "CollectionCallback must not be null!"); try { - MongoCollection collection = getAndPrepareCollection(getDbInternal(), collectionName); + MongoCollection collection = getAndPrepareCollection(doGetDatabase(), collectionName); return callback.doInCollection(collection); } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -565,6 +546,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public SessionScoped withSession(ClientSessionOptions options) { + + Assert.notNull(options, "ClientSessionOptions must not be null!"); + return withSession(() -> mongoDbFactory.getSession(options)); } @@ -574,6 +558,9 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, */ @Override public MongoTemplate withSession(ClientSession session) { + + Assert.notNull(session, "ClientSession must not be null!"); + return new SessionBoundMongoTemplate(session, MongoTemplate.this); } @@ -1819,7 +1806,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, String mapFunc = replaceWithResourceIfNecessary(mapFunction); String reduceFunc = replaceWithResourceIfNecessary(reduceFunction); - MongoCollection inputCollection = getAndPrepareCollection(getDbInternal(), inputCollectionName); + MongoCollection inputCollection = getAndPrepareCollection(doGetDatabase(), inputCollectionName); // MapReduceOp MapReduceIterable result = inputCollection.mapReduce(mapFunc, reduceFunc, Document.class); @@ -2252,10 +2239,10 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, } public MongoDatabase getDb() { - return getDbInternal(); + return doGetDatabase(); } - protected MongoDatabase getDbInternal() { + protected MongoDatabase doGetDatabase() { return mongoDbFactory.getDb(); } @@ -2605,7 +2592,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, try { T result = objectCallback - .doWith(collectionCallback.doInCollection(getAndPrepareCollection(getDbInternal(), collectionName))); + .doWith(collectionCallback.doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName))); return result; } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -2640,7 +2627,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, try { FindIterable iterable = collectionCallback - .doInCollection(getAndPrepareCollection(getDbInternal(), collectionName)); + .doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)); if (preparer != null) { iterable = preparer.prepare(iterable); @@ -2676,7 +2663,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, try { FindIterable iterable = collectionCallback - .doInCollection(getAndPrepareCollection(getDbInternal(), collectionName)); + .doInCollection(getAndPrepareCollection(doGetDatabase(), collectionName)); if (preparer != null) { iterable = preparer.prepare(iterable); @@ -3437,8 +3424,7 @@ public class MongoTemplate implements MongoOperations, ApplicationContextAware, /** * @param session must not be {@literal null}. - * @param mongoDbFactory must not be {@literal null}. - * @param mongoConverter must not be {@literal null}. + * @param that must not be {@literal null}. */ SessionBoundMongoTemplate(ClientSession session, MongoTemplate that) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java index 99aee5584..6747519b4 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoOperations.java @@ -147,11 +147,12 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { Flux execute(String collectionName, ReactiveCollectionCallback action); /** - * Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given - * {@link Supplier} to each and every command issued against MongoDB. + * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession} + * provided by the given {@link Supplier} to each and every command issued against MongoDB. *

* Note: It is up to the caller to manage the {@link ClientSession} lifecycle. Use - * {@link #withSession(Supplier, Consumer)} to provide a hook for processing the {@link ClientSession} when done. + * {@link ReactiveSessionScoped#execute(ReactiveSessionCallback, Consumer)} to provide a hook for processing the + * {@link ClientSession} when done. * * @param sessionProvider must not be {@literal null}. * @return new instance of {@link SessionScoped}. Never {@literal null}. @@ -165,8 +166,8 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { } /** - * Obtain a session bound instance of {@link SessionScoped} binding a new {@link ClientSession} with given - * {@literal sessionOptions} to each and every command issued against MongoDB. + * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding a new {@link ClientSession} + * with given {@literal sessionOptions} to each and every command issued against MongoDB. * * @param sessionOptions must not be {@literal null}. * @return new instance of {@link SessionScoped}. Never {@literal null}. @@ -175,11 +176,11 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { ReactiveSessionScoped withSession(ClientSessionOptions sessionOptions); /** - * Obtain a session bound instance of {@link SessionScoped} binding the {@link ClientSession} provided by the given - * {@link Supplier} to each and every command issued against MongoDB. + * Obtain a {@link ClientSession session} bound instance of {@link SessionScoped} binding the {@link ClientSession} + * provided by the given {@link Supplier} to each and every command issued against MongoDB. *

* Note: It is up to the caller to manage the {@link ClientSession} lifecycle. Use the - * {@litera onComplete} hook to potentially close the {@link ClientSession}. + * {@literal onComplete} hook to potentially close the {@link ClientSession}. * * @param sessionProvider must not be {@literal null}. * @return new instance of {@link SessionScoped}. Never {@literal null}. @@ -202,6 +203,15 @@ public interface ReactiveMongoOperations extends ReactiveFluentMongoOperations { }; } + /** + * Obtain a {@link ClientSession} bound instance of {@link ReactiveMongoOperations}. + *

+ * Note: It is up to the caller to manage the {@link ClientSession} lifecycle. + * + * @param session must not be {@literal null}. + * @return {@link ClientSession} bound instance of {@link ReactiveMongoOperations}. + * @since 2.1 + */ ReactiveMongoOperations withSession(ClientSession session); /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java index 9fb8e80cf..b73fbdce7 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveMongoTemplate.java @@ -105,16 +105,7 @@ import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import com.mongodb.BasicDBObject; -import com.mongodb.ClientSessionOptions; -import com.mongodb.CursorType; -import com.mongodb.DBCollection; -import com.mongodb.DBCursor; -import com.mongodb.DBRef; -import com.mongodb.Mongo; -import com.mongodb.MongoException; -import com.mongodb.ReadPreference; -import com.mongodb.WriteConcern; +import com.mongodb.*; import com.mongodb.client.model.CountOptions; import com.mongodb.client.model.CreateCollectionOptions; import com.mongodb.client.model.DeleteOptions; @@ -430,9 +421,9 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati @Override public ReactiveSessionScoped withSession(Publisher sessionProvider) { - return new ReactiveSessionScoped() { + Mono cachedSession = Mono.from(sessionProvider).cache(); - private final Mono cachedSession = Mono.from(sessionProvider).cache(); + return new ReactiveSessionScoped() { @Override public Flux execute(ReactiveSessionCallback action, Consumer doFinally) { @@ -474,7 +465,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!"); - return Flux.defer(() -> callback.doInDB(prepareDatabase(getDbInternal()))).onErrorMap(translateException()); + return Flux.defer(() -> callback.doInDB(prepareDatabase(doGetDatabase()))).onErrorMap(translateException()); } /** @@ -488,7 +479,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!"); - return Mono.defer(() -> Mono.from(callback.doInDB(prepareDatabase(getDbInternal())))) + return Mono.defer(() -> Mono.from(callback.doInDB(prepareDatabase(doGetDatabase())))) .onErrorMap(translateException()); } @@ -505,7 +496,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(callback, "ReactiveDatabaseCallback must not be null!"); Mono> collectionPublisher = Mono - .fromCallable(() -> getAndPrepareCollection(getDbInternal(), collectionName)); + .fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName)); return collectionPublisher.flatMapMany(callback::doInCollection).onErrorMap(translateException()); } @@ -524,7 +515,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(callback, "ReactiveCollectionCallback must not be null!"); Mono> collectionPublisher = Mono - .fromCallable(() -> getAndPrepareCollection(getDbInternal(), collectionName)); + .fromCallable(() -> getAndPrepareCollection(doGetDatabase(), collectionName)); return collectionPublisher.flatMap(collection -> Mono.from(callback.doInCollection(collection))) .onErrorMap(translateException()); @@ -622,10 +613,10 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati } public MongoDatabase getMongoDatabase() { - return getDbInternal(); + return doGetDatabase(); } - protected MongoDatabase getDbInternal() { + protected MongoDatabase doGetDatabase() { return mongoDatabaseFactory.getMongoDatabase(); } @@ -2400,7 +2391,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati Assert.notNull(action, "MongoDatabaseCallback must not be null!"); try { - MongoDatabase db = this.getDbInternal(); + MongoDatabase db = this.doGetDatabase(); return action.doInDatabase(db); } catch (RuntimeException e) { throw potentiallyConvertRuntimeException(e, exceptionTranslator); @@ -3003,8 +2994,7 @@ public class ReactiveMongoTemplate implements ReactiveMongoOperations, Applicati /** * @param session must not be {@literal null}. - * @param mongoDbFactory must not be {@literal null}. - * @param mongoConverter must not be {@literal null}. + * @param that must not be {@literal null}. */ ReactiveSessionBoundMongoTemplate(ClientSession session, ReactiveMongoTemplate that) { diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionCallback.java index 2fd58c862..a6f46b033 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionCallback.java @@ -19,10 +19,12 @@ import org.reactivestreams.Publisher; import org.springframework.data.mongodb.core.query.Query; /** - * Callback interface for executing operations within a {@link com.mongodb.session.ClientSession} reactively. + * Callback interface for executing operations within a {@link com.mongodb.session.ClientSession} using reactive + * infrastructure. * * @author Christoph Strobl * @since 2.1 + * @see com.mongodb.session.ClientSession */ @FunctionalInterface public interface ReactiveSessionCallback { @@ -31,15 +33,15 @@ public interface ReactiveSessionCallback { * Execute operations against a MongoDB instance via session bound {@link ReactiveMongoOperations}. The session is * inferred directly into the operation so that no further interaction is necessary. *

- * Please note that only Spring Data specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and + * Please note that only Spring Data-specific abstractions like {@link ReactiveMongoOperations#find(Query, Class)} and * others are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway * objects like {@link com.mongodb.reactivestreams.client.MongoCollection} or - * {@link om.mongodb.reactivestreams.client.MongoDatabase} via eg. + * {@link com.mongodb.reactivestreams.client.MongoDatabase} via eg. * {@link ReactiveMongoOperations#getCollection(String)} we leave responsibility for * {@link com.mongodb.session.ClientSession} again up to the caller. * * @param operations will never be {@literal null}. - * @return can be {@literal null}. + * @return never {@literal null}. */ Publisher doInSession(ReactiveMongoOperations operations); } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionScoped.java index d5cc3d04f..047dbe6ef 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionScoped.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/ReactiveSessionScoped.java @@ -26,12 +26,13 @@ import com.mongodb.session.ClientSession; * {@link ReactiveSessionCallback}. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ public interface ReactiveSessionScoped { /** - * Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession} + * Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}. *

* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close() * closed} when done. @@ -45,13 +46,15 @@ public interface ReactiveSessionScoped { } /** - * Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession} + * Executes the given {@link ReactiveSessionCallback} within the {@link com.mongodb.session.ClientSession}. *

* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close() * closed} when done. * * @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}. - * @param doFinally + * @param doFinally callback object that accepts {@link ClientSession} after invoking {@link ReactiveSessionCallback}. + * This {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of + * {@link ReactiveSessionCallback}). * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java index af1963353..dcc65f7f2 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionCallback.java @@ -23,6 +23,7 @@ import org.springframework.lang.Nullable; * * @author Christoph Strobl * @since 2.1 + * @see com.mongodb.session.ClientSession */ public interface SessionCallback { @@ -30,7 +31,7 @@ public interface SessionCallback { * Execute operations against a MongoDB instance via session bound {@link MongoOperations}. The session is inferred * directly into the operation so that no further interaction is necessary. *

- * Please note that only Spring Data specific abstractions like {@link MongoOperations#find(Query, Class)} and others + * Please note that only Spring Data-specific abstractions like {@link MongoOperations#find(Query, Class)} and others * are enhanced with the {@link com.mongodb.session.ClientSession}. When obtaining plain MongoDB gateway objects like * {@link com.mongodb.client.MongoCollection} or {@link com.mongodb.client.MongoDatabase} via eg. * {@link MongoOperations#getCollection(String)} we leave responsibility for {@link com.mongodb.session.ClientSession} diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java index ecc6893c4..939dbf678 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SessionScoped.java @@ -27,12 +27,13 @@ import com.mongodb.session.ClientSession; * The very same bound {@link ClientSession} is used for all invocations of {@code execute} on the instance. * * @author Christoph Strobl + * @author Mark Paluch * @since 2.1 */ public interface SessionScoped { /** - * Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession} + * Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}. *

* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close() * closed} when done. @@ -47,13 +48,15 @@ public interface SessionScoped { } /** - * Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession} + * Executes the given {@link SessionCallback} within the {@link com.mongodb.session.ClientSession}. *

* It is up to the caller to make sure the {@link com.mongodb.session.ClientSession} is {@link ClientSession#close() * closed} when done. * * @param action callback object that specifies the MongoDB action the callback action. Must not be {@literal null}. - * @param doFinally + * @param doFinally callback object that accepts {@link ClientSession} after invoking {@link SessionCallback}. This + * {@link Consumer} is guaranteed to be notified in any case (successful and exceptional outcome of + * {@link SessionCallback}). * @param return type. * @return a result object returned by the action. Can be {@literal null}. */ diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java index f1ccf6f20..c57c72a43 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleMongoDbFactory.java @@ -15,6 +15,8 @@ */ package org.springframework.data.mongodb.core; +import lombok.Value; + import java.net.UnknownHostException; import org.springframework.aop.framework.ProxyFactory; @@ -174,20 +176,15 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { /** * {@link ClientSession} bound {@link MongoDbFactory} decorating the database with a * {@link SessionAwareMethodInterceptor}. - * + * * @author Christoph Strobl * @since 2.1 */ + @Value static class ClientSessionBoundMongoDbFactory implements MongoDbFactory { - private final ClientSession session; - private final MongoDbFactory delegate; - - ClientSessionBoundMongoDbFactory(ClientSession session, MongoDbFactory delegate) { - - this.session = session; - this.delegate = delegate; - } + ClientSession session; + MongoDbFactory delegate; /* * (non-Javadoc) @@ -265,7 +262,7 @@ public class SimpleMongoDbFactory implements DisposableBean, MongoDbFactory { factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, MongoCollection.class, this::proxyCollection)); - return (T) factory.getProxy(); + return targetType.cast(factory.getProxy()); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java index 7207c620e..a57b0e8cf 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactory.java @@ -15,6 +15,7 @@ */ package org.springframework.data.mongodb.core; +import lombok.Value; import reactor.core.publisher.Mono; import java.net.UnknownHostException; @@ -162,16 +163,11 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React * @author Christoph Strobl * @since 2.1 */ + @Value static class ClientSessionBoundMongoDbFactory implements ReactiveMongoDatabaseFactory { - private final ClientSession session; - private final ReactiveMongoDatabaseFactory delegate; - - ClientSessionBoundMongoDbFactory(ClientSession session, ReactiveMongoDatabaseFactory delegate) { - - this.session = session; - this.delegate = delegate; - } + ClientSession session; + ReactiveMongoDatabaseFactory delegate; /* * (non-Javadoc) @@ -240,7 +236,7 @@ public class SimpleReactiveMongoDatabaseFactory implements DisposableBean, React factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, MongoCollection.class, this::proxyCollection)); - return (T) factory.getProxy(); + return targetType.cast(factory.getProxy()); } } } diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java index bf65557e4..1bc09c5d1 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DbRefResolver.java @@ -47,7 +47,7 @@ public interface DbRefResolver { * @return */ @Nullable - Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback, + Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler proxyHandler); /** @@ -59,7 +59,8 @@ public interface DbRefResolver { * @param id will never be {@literal null}. * @return */ - DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation, MongoPersistentEntity entity, + DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation, + MongoPersistentEntity entity, Object id); /** diff --git a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java index cea30568d..7b7988119 100644 --- a/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java +++ b/spring-data-mongodb/src/main/java/org/springframework/data/mongodb/core/convert/DefaultDbRefResolver.java @@ -85,24 +85,12 @@ public class DefaultDbRefResolver implements DbRefResolver { this.objenesis = new ObjenesisStd(true); } - /** - * Creates a new {@link DefaultDbRefResolver} with the given {@link MongoDbFactory}. - * - * @param mongoDbFactory must not be {@literal null}. - */ - private DefaultDbRefResolver(DefaultDbRefResolver delegate) { - - this.mongoDbFactory = delegate.mongoDbFactory; - this.exceptionTranslator = delegate.exceptionTranslator; - this.objenesis = delegate.objenesis; - } - /* * (non-Javadoc) * @see org.springframework.data.mongodb.core.convert.DbRefResolver#resolveDbRef(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.convert.DbRefResolverCallback) */ @Override - public Object resolveDbRef(MongoPersistentProperty property, DBRef dbref, DbRefResolverCallback callback, + public Object resolveDbRef(MongoPersistentProperty property, @Nullable DBRef dbref, DbRefResolverCallback callback, DbRefProxyHandler handler) { Assert.notNull(property, "Property must not be null!"); @@ -121,7 +109,7 @@ public class DefaultDbRefResolver implements DbRefResolver { * @see org.springframework.data.mongodb.core.convert.DbRefResolver#created(org.springframework.data.mongodb.core.mapping.MongoPersistentProperty, org.springframework.data.mongodb.core.mapping.MongoPersistentEntity, java.lang.Object) */ @Override - public DBRef createDbRef(org.springframework.data.mongodb.core.mapping.DBRef annotation, + public DBRef createDbRef(@Nullable org.springframework.data.mongodb.core.mapping.DBRef annotation, MongoPersistentEntity entity, Object id) { if (annotation != null && StringUtils.hasText(annotation.db())) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java index f2b2c75f2..923035fcc 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/SessionAwareMethodInterceptorUnitTests.java @@ -40,6 +40,8 @@ import com.mongodb.client.MongoDatabase; import com.mongodb.session.ClientSession; /** + * Unit tests for {@link SessionAwareMethodInterceptor}. + * * @author Christoph Strobl */ @RunWith(MockitoJUnitRunner.class) @@ -131,7 +133,7 @@ public class SessionAwareMethodInterceptorUnitTests { } @Test // DATAMONGO-1880 - public void proxiesNewDbInstanceReturnedByMethdod() { + public void proxiesNewDbInstanceReturnedByMethod() { MongoDatabase otherDb = mock(MongoDatabase.class); when(targetDatabase.withCodecRegistry(any())).thenReturn(otherDb); @@ -145,7 +147,7 @@ public class SessionAwareMethodInterceptorUnitTests { } @Test // DATAMONGO-1880 - public void proxiesNewCollectionInstanceReturnedByMethdod() { + public void proxiesNewCollectionInstanceReturnedByMethod() { MongoCollection otherCollection = mock(MongoCollection.class); when(targetCollection.withCodecRegistry(any())).thenReturn(otherCollection); @@ -176,7 +178,7 @@ public class SessionAwareMethodInterceptorUnitTests { factory.addAdvice(new SessionAwareMethodInterceptor<>(session, target, MongoDatabase.class, this::proxyDatabase, MongoCollection.class, this::proxyCollection)); - return (T) factory.getProxy(); + return targetType.cast(factory.getProxy()); } } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java index 0eeea77d6..cf207b219 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ClientSessionTests.java @@ -21,8 +21,10 @@ import org.bson.Document; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.TestRule; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.mongodb.test.util.ReplicaSet; import org.springframework.data.util.Version; import com.mongodb.ClientSessionOptions; @@ -31,10 +33,12 @@ import com.mongodb.session.ClientSession; /** * @author Christoph Strobl + * @author Mark Paluch */ public class ClientSessionTests { public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0")); + public static @ClassRule TestRule replSet = ReplicaSet.required(); MongoTemplate template; MongoClient client; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveClientSessionTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveClientSessionTests.java index 95d7fe4f2..d409c07b2 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveClientSessionTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveClientSessionTests.java @@ -27,8 +27,10 @@ import org.bson.Document; import org.junit.Before; import org.junit.ClassRule; import org.junit.Test; +import org.junit.rules.TestRule; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.test.util.MongoVersionRule; +import org.springframework.data.mongodb.test.util.ReplicaSet; import org.springframework.data.util.Version; import com.mongodb.ClientSessionOptions; @@ -38,10 +40,12 @@ import com.mongodb.session.ClientSession; /** * @author Christoph Strobl + * @author Mark Paluch */ public class ReactiveClientSessionTests { public static @ClassRule MongoVersionRule REQUIRES_AT_LEAST_3_6_0 = MongoVersionRule.atLeast(Version.parse("3.6.0")); + public static @ClassRule TestRule replSet = ReplicaSet.required(); MongoClient client; ReactiveMongoTemplate template; diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java index 6a0b244f2..412eec50a 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/ReactiveSessionBoundMongoTemplateUnitTests.java @@ -26,7 +26,6 @@ import static org.mockito.Mockito.anyString; import java.lang.reflect.Proxy; -import com.mongodb.reactivestreams.client.MongoClient; import org.bson.Document; import org.bson.codecs.BsonValueCodec; import org.bson.codecs.configuration.CodecRegistry; @@ -42,7 +41,6 @@ import org.springframework.data.geo.Point; import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; import org.springframework.data.mongodb.core.ReactiveMongoTemplate.NoOpDbRefResolver; import org.springframework.data.mongodb.core.ReactiveMongoTemplate.ReactiveSessionBoundMongoTemplate; -import org.springframework.data.mongodb.core.SimpleReactiveMongoDatabaseFactory.ClientSessionBoundMongoDbFactory; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.convert.MappingMongoConverter; import org.springframework.data.mongodb.core.mapping.MongoMappingContext; @@ -57,13 +55,17 @@ import com.mongodb.client.model.UpdateOptions; import com.mongodb.reactivestreams.client.AggregatePublisher; import com.mongodb.reactivestreams.client.DistinctPublisher; import com.mongodb.reactivestreams.client.FindPublisher; +import com.mongodb.reactivestreams.client.MongoClient; import com.mongodb.reactivestreams.client.MongoCollection; import com.mongodb.reactivestreams.client.MongoDatabase; import com.mongodb.session.ClientSession; /** + * Unit tests for {@link ReactiveSessionBoundMongoTemplate}. + * * @author Christoph Strobl */ +@SuppressWarnings("unchecked") @RunWith(MockitoJUnitRunner.Silent.class) public class ReactiveSessionBoundMongoTemplateUnitTests { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java index a9ff4353c..3d1a369ee 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateTests.java @@ -108,6 +108,14 @@ public class SessionBoundMongoTemplateTests { @Override protected MongoCollection prepareCollection(MongoCollection collection) { + injectCollectionSpy(collection); + + return super.prepareCollection(collection); + } + + @SuppressWarnings({ "ConstantConditions", "unchecked" }) + private void injectCollectionSpy(MongoCollection collection) { + InvocationHandler handler = Proxy.getInvocationHandler(collection); Advised advised = (Advised) ReflectionTestUtils.getField(handler, "advised"); @@ -123,8 +131,6 @@ public class SessionBoundMongoTemplateTests { ReflectionTestUtils.setField(advice, "target", spiedCollection); } } - - return super.prepareCollection(collection); } }; } @@ -294,5 +300,4 @@ public class SessionBoundMongoTemplateTests { return converter; } - } diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java index aeced5321..c510a002b 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SessionBoundMongoTemplateUnitTests.java @@ -67,6 +67,7 @@ import com.mongodb.session.ClientSession; * * @author Christoph Strobl */ +@SuppressWarnings("unchecked") @RunWith(MockitoJUnitRunner.Silent.class) public class SessionBoundMongoTemplateUnitTests { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java index a882e8f8b..fbbbfa3eb 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleMongoDbFactoryUnitTests.java @@ -20,6 +20,8 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; import static org.springframework.test.util.ReflectionTestUtils.*; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; import java.net.UnknownHostException; import org.junit.Rule; @@ -28,26 +30,29 @@ import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.data.authentication.UserCredentials; +import org.springframework.aop.framework.AopProxyUtils; import org.springframework.data.mongodb.MongoDbFactory; +import org.springframework.test.util.ReflectionTestUtils; -import com.mongodb.Mongo; import com.mongodb.MongoClient; import com.mongodb.MongoClientURI; -import com.mongodb.MongoURI; +import com.mongodb.client.MongoDatabase; +import com.mongodb.session.ClientSession; /** * Unit tests for {@link SimpleMongoDbFactory}. * * @author Oliver Gierke * @author Christoph Strobl + * @author Mark Paluch */ @RunWith(MockitoJUnitRunner.class) public class SimpleMongoDbFactoryUnitTests { public @Rule ExpectedException expectedException = ExpectedException.none(); @Mock MongoClient mongo; + @Mock ClientSession clientSession; + @Mock MongoDatabase database; @Test // DATADOC-254 public void rejectsIllegalDatabaseNames() { @@ -82,6 +87,22 @@ public class SimpleMongoDbFactoryUnitTests { assertThat(getField(factory, "databaseName").toString(), is("myDataBase")); } + @Test // DATAMONGO-1880 + public void cascadedWithSessionUsesRootFactory() { + + when(mongo.getDatabase("foo")).thenReturn(database); + + MongoDbFactory factory = new SimpleMongoDbFactory(mongo, "foo"); + MongoDbFactory wrapped = factory.withSession(clientSession).withSession(clientSession); + + InvocationHandler invocationHandler = Proxy.getInvocationHandler(wrapped.getDb()); + + Object singletonTarget = AopProxyUtils + .getSingletonTarget(ReflectionTestUtils.getField(invocationHandler, "advised")); + + assertThat(singletonTarget, is(sameInstance(database))); + } + @SuppressWarnings("deprecation") private void rejectsDatabaseName(String databaseName) { diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactoryUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactoryUnitTests.java new file mode 100644 index 000000000..38eda9a01 --- /dev/null +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/SimpleReactiveMongoDatabaseFactoryUnitTests.java @@ -0,0 +1,64 @@ +/* + * Copyright 2018 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 static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Proxy; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.data.mongodb.ReactiveMongoDatabaseFactory; +import org.springframework.test.util.ReflectionTestUtils; + +import com.mongodb.reactivestreams.client.MongoClient; +import com.mongodb.reactivestreams.client.MongoDatabase; +import com.mongodb.session.ClientSession; + +/** + * Unit tests for {@link SimpleReactiveMongoDatabaseFactory}. + * + * @author Mark Paluch + */ +@RunWith(MockitoJUnitRunner.class) +public class SimpleReactiveMongoDatabaseFactoryUnitTests { + + @Mock MongoClient mongoClient; + @Mock ClientSession clientSession; + @Mock MongoDatabase database; + + @Test // DATAMONGO-1880 + public void cascadedWithSessionUsesRootFactory() { + + when(mongoClient.getDatabase("foo")).thenReturn(database); + + ReactiveMongoDatabaseFactory factory = new SimpleReactiveMongoDatabaseFactory(mongoClient, "foo"); + ReactiveMongoDatabaseFactory wrapped = factory.withSession(clientSession).withSession(clientSession); + + InvocationHandler invocationHandler = Proxy.getInvocationHandler(wrapped.getMongoDatabase()); + + Object singletonTarget = AopProxyUtils + .getSingletonTarget(ReflectionTestUtils.getField(invocationHandler, "advised")); + + assertThat(singletonTarget, is(sameInstance(database))); + } +} diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java index b64729597..f71ef6c77 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/core/index/MongoPersistentEntityIndexCreatorUnitTests.java @@ -15,30 +15,23 @@ */ package org.springframework.data.mongodb.core.index; -import static org.hamcrest.Matchers.*; -import static org.junit.Assert.*; -import static org.mockito.Matchers.*; +import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; import java.util.Collections; import java.util.Date; import java.util.concurrent.TimeUnit; -import org.hamcrest.core.IsEqual; -import org.hamcrest.number.IsCloseTo; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.junit.MockitoJUnitRunner; -import org.springframework.context.ApplicationContext; import org.springframework.dao.DataAccessException; import org.springframework.data.geo.Point; import org.springframework.data.mapping.context.MappingContextEvent; import org.springframework.data.mongodb.MongoDbFactory; -import org.springframework.data.mongodb.core.DefaultIndexOperations; import org.springframework.data.mongodb.core.MongoExceptionTranslator; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.mapping.Document; @@ -83,7 +76,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { when(factory.getDb()).thenReturn(db); when(factory.getExceptionTranslator()).thenReturn(new MongoExceptionTranslator()); - when(db.getCollection(collectionCaptor.capture(), Mockito.eq(org.bson.Document.class))).thenReturn((MongoCollection) collection); + when(db.getCollection(collectionCaptor.capture(), eq(org.bson.Document.class))) + .thenReturn((MongoCollection) collection); mongoTemplate = new MongoTemplate(factory); @@ -97,11 +91,10 @@ public class MongoPersistentEntityIndexCreatorUnitTests { new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(keysCaptor.getValue(), is(notNullValue())); - assertThat(keysCaptor.getValue().keySet(), hasItem("fieldname")); - assertThat(optionsCaptor.getValue().getName(), is("indexName")); - assertThat(optionsCaptor.getValue().isBackground(), is(false)); - assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), nullValue()); + assertThat(keysCaptor.getValue()).isNotNull().containsKey("fieldname"); + assertThat(optionsCaptor.getValue().getName()).isEqualTo("indexName"); + assertThat(optionsCaptor.getValue().isBackground()).isFalse(); + assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull(); } @Test @@ -128,8 +121,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { mappingContext.initialize(); MongoPersistentEntityIndexCreator creator = new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(creator.isIndexCreatorFor(mappingContext), is(true)); - assertThat(creator.isIndexCreatorFor(new MongoMappingContext()), is(false)); + assertThat(creator.isIndexCreatorFor(mappingContext)).isTrue(); + assertThat(creator.isIndexCreatorFor(new MongoMappingContext())).isFalse(); } @Test // DATAMONGO-554 @@ -138,11 +131,10 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = prepareMappingContext(AnotherPerson.class); new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(keysCaptor.getValue(), is(notNullValue())); - assertThat(keysCaptor.getValue().keySet(), hasItem("lastname")); - assertThat(optionsCaptor.getValue().getName(), is("lastname")); - assertThat(optionsCaptor.getValue().isBackground(), IsEqual. equalTo(true)); - assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), nullValue()); + assertThat(keysCaptor.getValue()).isNotNull().containsKey("lastname"); + assertThat(optionsCaptor.getValue().getName()).isEqualTo("lastname"); + assertThat(optionsCaptor.getValue().isBackground()).isTrue(); + assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isNull(); } @Test // DATAMONGO-544 @@ -151,9 +143,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = prepareMappingContext(Milk.class); new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(keysCaptor.getValue(), is(notNullValue())); - assertThat(keysCaptor.getValue().keySet(), hasItem("expiry")); - assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS), IsEqual. equalTo(60L)); + assertThat(keysCaptor.getValue()).isNotNull().containsKey("expiry"); + assertThat(optionsCaptor.getValue().getExpireAfter(TimeUnit.SECONDS)).isEqualTo(60); } @Test // DATAMONGO-899 @@ -162,13 +153,13 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = prepareMappingContext(Wrapper.class); new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(keysCaptor.getValue(), equalTo(new org.bson.Document().append("company.address.location", "2d"))); + assertThat(keysCaptor.getValue()).isEqualTo(new org.bson.Document("company.address.location", "2d")); IndexOptions opts = optionsCaptor.getValue(); - assertThat(opts.getName(), is(equalTo("company.address.location"))); - assertThat(opts.getMin(), IsCloseTo.closeTo(-180, 0)); - assertThat(opts.getMax(), IsCloseTo.closeTo(180, 0)); - assertThat(opts.getBits(), is(26)); + assertThat(opts.getName()).isEqualTo("company.address.location"); + assertThat(opts.getMin()).isCloseTo(-180d, offset(0d)); + assertThat(opts.getMax()).isCloseTo(180d, offset(0d)); + assertThat(opts.getBits()).isEqualTo(26); } @Test // DATAMONGO-827 @@ -177,10 +168,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { MongoMappingContext mappingContext = prepareMappingContext(EntityWithGeneratedIndexName.class); new MongoPersistentEntityIndexCreator(mappingContext, mongoTemplate); - assertThat(keysCaptor.getValue().containsKey("name"), is(false)); - assertThat(keysCaptor.getValue().keySet(), hasItem("lastname")); - - assertThat(optionsCaptor.getValue().getName(), nullValue()); + assertThat(keysCaptor.getValue()).doesNotContainKey("name").containsKey("lastname"); + assertThat(optionsCaptor.getValue().getName()).isNull(); } @Test // DATAMONGO-367 @@ -191,8 +180,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { ArgumentCaptor collectionNameCapturer = ArgumentCaptor.forClass(String.class); - verify(db, times(1)).getCollection(collectionNameCapturer.capture(), Mockito.any()); - assertThat(collectionNameCapturer.getValue(), equalTo("wrapper")); + verify(db, times(1)).getCollection(collectionNameCapturer.capture(), any()); + assertThat(collectionNameCapturer.getValue()).isEqualTo("wrapper"); } @Test // DATAMONGO-367 @@ -203,15 +192,15 @@ public class MongoPersistentEntityIndexCreatorUnitTests { ArgumentCaptor collectionNameCapturer = ArgumentCaptor.forClass(String.class); - verify(db, times(1)).getCollection(collectionNameCapturer.capture(), Mockito.any()); - assertThat(collectionNameCapturer.getValue(), equalTo("indexedDocumentWrapper")); + verify(db, times(1)).getCollection(collectionNameCapturer.capture(), any()); + assertThat(collectionNameCapturer.getValue()).isEqualTo("indexedDocumentWrapper"); } @Test(expected = DataAccessException.class) // DATAMONGO-1125 public void createIndexShouldUsePersistenceExceptionTranslatorForNonDataIntegrityConcerns() { - doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(Mockito.any(org.bson.Document.class), - Mockito.any(IndexOptions.class)); + doThrow(new MongoException(6, "HostUnreachable")).when(collection).createIndex(any(org.bson.Document.class), + any(IndexOptions.class)); MongoMappingContext mappingContext = prepareMappingContext(Person.class); @@ -221,8 +210,8 @@ public class MongoPersistentEntityIndexCreatorUnitTests { @Test(expected = ClassCastException.class) // DATAMONGO-1125 public void createIndexShouldNotConvertUnknownExceptionTypes() { - doThrow(new ClassCastException("o_O")).when(collection).createIndex(Mockito.any(org.bson.Document.class), - Mockito.any(IndexOptions.class)); + doThrow(new ClassCastException("o_O")).when(collection).createIndex(any(org.bson.Document.class), + any(IndexOptions.class)); MongoMappingContext mappingContext = prepareMappingContext(Person.class); diff --git a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ReplicaSet.java b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ReplicaSet.java index 88f69cdfd..b39471007 100644 --- a/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ReplicaSet.java +++ b/spring-data-mongodb/src/test/java/org/springframework/data/mongodb/test/util/ReplicaSet.java @@ -83,7 +83,7 @@ public class ReplicaSet implements TestRule { } if (!runsAsReplicaSet()) { - throw new AssumptionViolatedException("Not runnig in repl set mode"); + throw new AssumptionViolatedException("Not running in repl set mode"); } base.evaluate(); } diff --git a/src/main/asciidoc/reference/client-session.adoc b/src/main/asciidoc/reference/client-session.adoc index 6cf23f921..bbd9ce784 100644 --- a/src/main/asciidoc/reference/client-session.adoc +++ b/src/main/asciidoc/reference/client-session.adoc @@ -5,11 +5,11 @@ As of version 3.6 MongoDB supports a concept of Sessions. The use of sessions en WARNING: Operations within a client session are not isolated from operations outside the session. -Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. Within the callback all operations on `MongoCollection` and `MongoDatabase` are called with the provided session via a `Proxy` without the need to add it manually. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`. +Both `MongoOperations` and `ReactiveMongoOperations` provide gateway methods for tying a `ClientSession` to the operations themselves. `MongoCollection` and `MongoDatabase` use session proxy objects implementing MongoDB's collection and and database interfaces so there's no need to add a session on each call. This means that a potential call to `MongoCollection#find()` is delegated to `MongoCollection#find(ClientSession)`. -NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB java driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` will *NOT* be wrapped by the `Proxy`. So please make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#excute` callbacks on `MongoOperations`. +NOTE: Methods like `(Reactive)MongoOperations#getCollection` returning native MongoDB Java Driver gateway objects, such as `MongoCollection`, that themselves offer dedicated methods for `ClientSession` are *NOT* be session-proxied. So make sure to provide the `ClientSession` where needed when interacting directly with a `MongoCollection` or `MongoDatabase` and not via one of the `#execute` callbacks on `MongoOperations`. -.ClientSession with MongoOperations. +.ClientSession with `MongoOperations` ==== [source,java] ---- @@ -37,14 +37,14 @@ session.close() <4> ---- <1> Obtain a new session from the server. <2> Use `MongoOperation` methods as before. The `ClientSession` gets applied automatically. -<3> Important! Do not forget to close the session. +<3> Make sure to close the `ClientSession`. ==== -WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded. +WARNING: When dealing with ``DBRef``s, especially lazily loaded ones, it is essential to **not** close the `ClientSession` before all data is loaded. Otherwise, lazy fetch fails. The reactive counterpart uses the very same building blocks as the imperative one. -.ClientSession with ReactiveMongoOperations. +.ClientSession with `ReactiveMongoOperations` ==== [source,java] ---- @@ -66,14 +66,14 @@ template.withSession(session) return action.insert(azoth); <2> }); - }, ClientSession::close) <4> + }, ClientSession::close) <3> .subscribe(); ---- <1> Obtain a `Publisher` for new session retrieval. -<2> Use `MongoOperation` methods as before. The `ClientSession` is obtained and applied automatically. -<3> Important! Do not forget to close the session. +<2> Use `ReactiveMongoOperation` methods as before. The `ClientSession` is obtained and applied automatically. +<3> Make sure to close the `ClientSession`. ==== By using a `Publisher` providing the actual session you can defer session acquisition to the point of actual subscription. Still you need to close the session when done in order to not pollute the server with stale sessions. Use the `doFinally` hook on `execute` to call `ClientSession#close()` when you don't need the session any more. -In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`. \ No newline at end of file +In case you prefer having more control over the session itself, you can always obtain the `ClientSession` via the driver and provide it via a `Supplier`.