diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/MongoDatabaseResolver.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/MongoDatabaseResolver.java new file mode 100644 index 00000000000000..8fa939449d7587 --- /dev/null +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/MongoDatabaseResolver.java @@ -0,0 +1,8 @@ +package io.quarkus.mongodb.panache.common; + +/* + * This interface can be used to resolve the mongo database name in runtime, it'll help to implement multi-tenancy using tenant per database approach + */ +public interface MongoDatabaseResolver { + String resolve(); +} \ No newline at end of file diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/ReactiveMongoOperations.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/ReactiveMongoOperations.java index 2ba4c3a035b3fa..f8722224638a75 100644 --- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/ReactiveMongoOperations.java +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/reactive/runtime/ReactiveMongoOperations.java @@ -3,6 +3,7 @@ import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.beanName; import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.clientFromArc; import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.getDatabaseName; +import static io.quarkus.mongodb.panache.common.runtime.BeanUtils.getDatabaseNameFromResolver; import java.util.ArrayList; import java.util.Arrays; @@ -26,6 +27,7 @@ import com.mongodb.client.model.ReplaceOneModel; import com.mongodb.client.model.ReplaceOptions; import com.mongodb.client.model.WriteModel; +import com.mongodb.client.result.DeleteResult; import io.quarkus.mongodb.panache.common.MongoEntity; import io.quarkus.mongodb.panache.common.binder.NativeQueryBinder; @@ -74,7 +76,7 @@ public Uni persist(Iterable entities) { objects.add(entity); } - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -99,7 +101,7 @@ public Uni persist(Object firstEntity, Object... entities) { public Uni persist(Stream entities) { return Uni.createFrom().deferred(() -> { List objects = entities.collect(Collectors.toList()); - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -122,7 +124,7 @@ public Uni update(Iterable entities) { objects.add(entity); } - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -147,7 +149,7 @@ public Uni update(Object firstEntity, Object... entities) { public Uni update(Stream entities) { return Uni.createFrom().deferred(() -> { List objects = entities.collect(Collectors.toList()); - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -170,7 +172,7 @@ public Uni persistOrUpdate(Iterable entities) { objects.add(entity); } - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -195,7 +197,7 @@ public Uni persistOrUpdate(Object firstEntity, Object... entities) { public Uni persistOrUpdate(Stream entities) { return Uni.createFrom().deferred(() -> { List objects = entities.collect(Collectors.toList()); - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); ReactiveMongoCollection collection = mongoCollection(firstEntity); @@ -314,7 +316,7 @@ private ReactiveMongoDatabase mongoDatabase(MongoEntity mongoEntity) { if (mongoEntity != null && !mongoEntity.database().isEmpty()) { return mongoClient.getDatabase(mongoEntity.database()); } - String databaseName = getDefaultDatabaseName(mongoEntity); + String databaseName = getDatabaseNameFromResolver().orElse(getDefaultDatabaseName(mongoEntity)); return mongoClient.getDatabase(databaseName); } @@ -621,7 +623,7 @@ public Uni count(Class entityClass, Document query) { public Uni deleteAll(Class entityClass) { ReactiveMongoCollection collection = mongoCollection(entityClass); - return collection.deleteMany(new Document()).map(deleteResult -> deleteResult.getDeletedCount()); + return collection.deleteMany(new Document()).map(DeleteResult::getDeletedCount); } public Uni deleteById(Class entityClass, Object id) { @@ -634,14 +636,14 @@ public Uni delete(Class entityClass, String query, Object... params) { String bindQuery = bindFilter(entityClass, query, params); BsonDocument docQuery = BsonDocument.parse(bindQuery); ReactiveMongoCollection collection = mongoCollection(entityClass); - return collection.deleteMany(docQuery).map(deleteResult -> deleteResult.getDeletedCount()); + return collection.deleteMany(docQuery).map(DeleteResult::getDeletedCount); } public Uni delete(Class entityClass, String query, Map params) { String bindQuery = bindFilter(entityClass, query, params); BsonDocument docQuery = BsonDocument.parse(bindQuery); ReactiveMongoCollection collection = mongoCollection(entityClass); - return collection.deleteMany(docQuery).map(deleteResult -> deleteResult.getDeletedCount()); + return collection.deleteMany(docQuery).map(DeleteResult::getDeletedCount); } public Uni delete(Class entityClass, String query, Parameters params) { @@ -651,7 +653,7 @@ public Uni delete(Class entityClass, String query, Parameters params) { //specific Mongo query public Uni delete(Class entityClass, Document query) { ReactiveMongoCollection collection = mongoCollection(entityClass); - return collection.deleteMany(query).map(deleteResult -> deleteResult.getDeletedCount()); + return collection.deleteMany(query).map(DeleteResult::getDeletedCount); } public UpdateType update(Class entityClass, String update, Map params) { diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/BeanUtils.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/BeanUtils.java index 14f0e4560dc505..e6da75e2fa2c66 100644 --- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/BeanUtils.java +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/BeanUtils.java @@ -1,12 +1,15 @@ package io.quarkus.mongodb.panache.common.runtime; import java.lang.annotation.Annotation; +import java.util.Optional; +import java.util.function.Function; import javax.inject.Named; import io.quarkus.arc.Arc; import io.quarkus.arc.InjectableBean; import io.quarkus.arc.InstanceHandle; +import io.quarkus.mongodb.panache.common.MongoDatabaseResolver; import io.quarkus.mongodb.panache.common.MongoEntity; import io.quarkus.mongodb.runtime.MongoClientBeanUtil; import io.quarkus.mongodb.runtime.MongoClientConfig; @@ -17,6 +20,13 @@ public final class BeanUtils { private BeanUtils() { } + private static Optional getValueFromArcInstance(Class instanceClass, Function mapFn) { + return Optional.of(Arc.container().instance(instanceClass)) + .filter(InstanceHandle::isAvailable) + .map(InstanceHandle::get) + .map(mapFn); + } + public static String beanName(MongoEntity entity) { if (entity != null && !entity.clientName().isEmpty()) { return entity.clientName(); @@ -82,4 +92,9 @@ public static String getDatabaseName(MongoEntity mongoEntity, String clientBeanN "The database attribute was not set for the @MongoEntity annotation neither was the database property configured for the named Mongo Client (via 'quarkus.mongodb.%s.database')", mongoEntity.clientName())); } + + public static Optional getDatabaseNameFromResolver() { + return getValueFromArcInstance(MongoDatabaseResolver.class, MongoDatabaseResolver::resolve); + } + } diff --git a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/MongoOperations.java b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/MongoOperations.java index 27ef1a5c68452d..83a5c4cba65565 100644 --- a/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/MongoOperations.java +++ b/extensions/panache/mongodb-panache-common/runtime/src/main/java/io/quarkus/mongodb/panache/common/runtime/MongoOperations.java @@ -112,7 +112,7 @@ public void update(Iterable entities) { objects.add(entity); } - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); MongoCollection collection = mongoCollection(firstEntity); @@ -134,7 +134,7 @@ public void update(Object firstEntity, Object... entities) { public void update(Stream entities) { List objects = entities.collect(Collectors.toList()); - if (objects.size() > 0) { + if (!objects.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = objects.get(0); MongoCollection collection = mongoCollection(firstEntity); @@ -214,7 +214,7 @@ private void persist(MongoCollection collection, Object entity) { } private void persist(List entities) { - if (entities.size() > 0) { + if (!entities.isEmpty()) { // get the first entity to be able to retrieve the collection with it Object firstEntity = entities.get(0); MongoCollection collection = mongoCollection(firstEntity); @@ -383,7 +383,7 @@ private MongoDatabase mongoDatabase(MongoEntity mongoEntity) { if (mongoEntity != null && !mongoEntity.database().isEmpty()) { return mongoClient.getDatabase(mongoEntity.database()); } - String databaseName = getDefaultDatabaseName(mongoEntity); + String databaseName = BeanUtils.getDatabaseNameFromResolver().orElse(getDefaultDatabaseName(mongoEntity)); return mongoClient.getDatabase(databaseName); } diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/ResolversPropertiesConstants.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/ResolversPropertiesConstants.java new file mode 100644 index 00000000000000..692da29cd3760b --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/ResolversPropertiesConstants.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mongodb.panache; + +public class ResolversPropertiesConstants { + public static final String MONGO_DATABASE_RESOLVER_ENABLED_PROPERTY = "quarkus.mongodb.database-resolver.enabled"; + + private ResolversPropertiesConstants() { + } +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/profiles/MongoDatabaseResolverTestProfile.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/profiles/MongoDatabaseResolverTestProfile.java new file mode 100644 index 00000000000000..5d0d4bd65323c4 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/profiles/MongoDatabaseResolverTestProfile.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.panache.profiles; + +import io.quarkus.test.junit.QuarkusTestProfile; + +public class MongoDatabaseResolverTestProfile implements QuarkusTestProfile { + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/resolvers/ReactiveMongodbPanacheResourceMongoDatabaseResolverTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/resolvers/ReactiveMongodbPanacheResourceMongoDatabaseResolverTest.java new file mode 100644 index 00000000000000..2ca4695962aa14 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/reactive/resolvers/ReactiveMongodbPanacheResourceMongoDatabaseResolverTest.java @@ -0,0 +1,14 @@ +package io.quarkus.it.mongodb.panache.reactive.resolvers; + +import io.quarkus.it.mongodb.panache.resolvers.BaseMongodbPanacheResolversTest; +import io.quarkus.mongodb.panache.reactive.runtime.JavaReactiveMongoOperations; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class ReactiveMongodbPanacheResourceMongoDatabaseResolverTest extends BaseMongodbPanacheResolversTest { + + public ReactiveMongodbPanacheResourceMongoDatabaseResolverTest() { + super(JavaReactiveMongoOperations.INSTANCE); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/BaseMongodbPanacheResolversTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/BaseMongodbPanacheResolversTest.java new file mode 100644 index 00000000000000..f3f74d7bc530e9 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/BaseMongodbPanacheResolversTest.java @@ -0,0 +1,92 @@ +package io.quarkus.it.mongodb.panache.resolvers; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import javax.enterprise.context.ApplicationScoped; + +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.DisabledOnOs; +import org.junit.jupiter.api.condition.OS; +import org.mockito.Mockito; + +import io.quarkus.mongodb.panache.common.MongoDatabaseResolver; +import io.quarkus.mongodb.panache.common.reactive.runtime.ReactiveMongoOperations; +import io.quarkus.mongodb.panache.common.runtime.MongoOperations; +import io.quarkus.test.Mock; +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.mockito.InjectMock; +import io.quarkus.test.mongodb.MongoReplicaSetTestResource; + +@QuarkusTestResource(MongoReplicaSetTestResource.class) +@DisabledOnOs(OS.WINDOWS) +@SuppressWarnings({ "rawtypes", "unchecked" }) +public abstract class BaseMongodbPanacheResolversTest { + + private static final String EXPECTED_DATABASE_NAME_FROM_RESOLVER = "tenant-a"; + + @ConfigProperty(name = "quarkus.mongodb.database") + String databaseNameFromProperty; + + @InjectMock + private MongoDatabaseResolver mongoDatabaseResolver; + + private Object mongoOperations; + + public BaseMongodbPanacheResolversTest(Object mongoOperations) { + this.mongoOperations = mongoOperations; + } + + @Test + public void testMongoDatabaseResolverUsingDatabaseFromResolve() throws Exception { + testMongoDatabaseResolver(ResolverEntity.class, EXPECTED_DATABASE_NAME_FROM_RESOLVER, true, false); + } + + @Test + public void testMongoDatabaseResolverKeepingDatabaseFromAnnotation() throws Exception { + testMongoDatabaseResolver(ResolverEntityWithDatabase.Entity.class, ResolverEntityWithDatabase.DATABASE_NAME, true, + true); + } + + @Test + public void testMongoDatabaseResolverUsingDefaultDatabaseFromProperties() throws Exception { + testMongoDatabaseResolver(ResolverEntity.class, databaseNameFromProperty, false, false); + } + + private void testMongoDatabaseResolver(Class entityClass, String expectedDatabaseName, boolean shouldMockResolveReturn, + boolean expectedNoCallInResolveMethod) + throws Exception { + //arrange + if (shouldMockResolveReturn) { + Mockito.when(mongoDatabaseResolver.resolve()).thenReturn(expectedDatabaseName); + } + + //act + String databaseName = null; + if (mongoOperations instanceof MongoOperations) { + MongoOperations auxMongoOperations = (MongoOperations) mongoOperations; + databaseName = auxMongoOperations.mongoDatabase(entityClass).getName(); + } + + if (mongoOperations instanceof ReactiveMongoOperations) { + ReactiveMongoOperations auxReactiveMongoOperations = (ReactiveMongoOperations) mongoOperations; + databaseName = auxReactiveMongoOperations.mongoDatabase(entityClass).getName(); + } + + //assert + Mockito.verify(mongoDatabaseResolver, Mockito.times(expectedNoCallInResolveMethod ? 0 : 1)).resolve(); + assertEquals(expectedDatabaseName, databaseName); + } + + @Mock + @ApplicationScoped + public static class TestMongoDatabaseResolver implements MongoDatabaseResolver { + + @Override + public String resolve() { + return null; + } + + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/MongodbPanacheResolversTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/MongodbPanacheResolversTest.java new file mode 100644 index 00000000000000..060de017da7ca6 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/MongodbPanacheResolversTest.java @@ -0,0 +1,13 @@ +package io.quarkus.it.mongodb.panache.resolvers; + +import io.quarkus.mongodb.panache.runtime.JavaMongoOperations; +import io.quarkus.test.junit.QuarkusTest; + +@QuarkusTest +class MongodbPanacheResolversTest extends BaseMongodbPanacheResolversTest { + + public MongodbPanacheResolversTest() { + super(JavaMongoOperations.INSTANCE); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntity.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntity.java new file mode 100644 index 00000000000000..e7f55bc501ddbc --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntity.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.panache.resolvers; + +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; + +public class ResolverEntity extends PanacheMongoEntityBase { + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntityWithDatabase.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntityWithDatabase.java new file mode 100644 index 00000000000000..724d880da2e45f --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/resolvers/ResolverEntityWithDatabase.java @@ -0,0 +1,12 @@ +package io.quarkus.it.mongodb.panache.resolvers; + +import io.quarkus.mongodb.panache.common.MongoEntity; + +public class ResolverEntityWithDatabase { + public static final String DATABASE_NAME = "database-from-anotation"; + + @MongoEntity(database = DATABASE_NAME) + public static class Entity { + + } +}