From 6b8ed902b0d53584754558b54b3fb6fa4dcab128 Mon Sep 17 00:00:00 2001 From: Gytis Trikleris Date: Wed, 17 Jun 2020 11:25:33 +0200 Subject: [PATCH] Panache REST Data with MongoDB --- bom/application/pom.xml | 10 + .../java/io/quarkus/deployment/Feature.java | 1 + .../deployment/pom.xml | 48 ++++ .../EntityDataAccessImplementor.java | 65 +++++ .../deployment/MongoPanacheRestProcessor.java | 121 ++++++++ .../RepositoryDataAccessImplementor.java | 90 ++++++ .../RestDataEntityInfoProvider.java | 86 ++++++ .../panache/mongodb-rest-data-panache/pom.xml | 21 ++ .../mongodb-rest-data-panache/runtime/pom.xml | 54 ++++ .../panache/PanacheMongoEntityResource.java | 21 ++ .../PanacheMongoRepositoryResource.java | 23 ++ .../resources/META-INF/quarkus-extension.yaml | 15 + extensions/panache/pom.xml | 1 + .../mongodb-rest-data-panache/pom.xml | 144 ++++++++++ .../it/mongodb/rest/data/panache/Author.java | 17 ++ .../rest/data/panache/AuthorsResource.java | 20 ++ .../it/mongodb/rest/data/panache/Book.java | 39 +++ .../rest/data/panache/BookRepository.java | 9 + .../rest/data/panache/BooksResource.java | 10 + .../rest/data/panache/TestResource.java | 48 ++++ .../src/main/resources/application.properties | 3 + .../panache/MongoDbRestDataPanacheIT.java | 7 + .../panache/MongoDbRestDataPanacheTest.java | 265 ++++++++++++++++++ .../rest/data/panache/MongoTestResource.java | 75 +++++ integration-tests/pom.xml | 1 + 25 files changed, 1194 insertions(+) create mode 100644 extensions/panache/mongodb-rest-data-panache/deployment/pom.xml create mode 100644 extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/EntityDataAccessImplementor.java create mode 100644 extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java create mode 100644 extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RepositoryDataAccessImplementor.java create mode 100644 extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RestDataEntityInfoProvider.java create mode 100644 extensions/panache/mongodb-rest-data-panache/pom.xml create mode 100644 extensions/panache/mongodb-rest-data-panache/runtime/pom.xml create mode 100644 extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java create mode 100644 extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java create mode 100644 extensions/panache/mongodb-rest-data-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml create mode 100644 integration-tests/mongodb-rest-data-panache/pom.xml create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Author.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/AuthorsResource.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Book.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BookRepository.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BooksResource.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/TestResource.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/main/resources/application.properties create mode 100644 integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheIT.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheTest.java create mode 100644 integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoTestResource.java diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 1c181f45cc856..0961c69ddc0f9 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -754,6 +754,16 @@ quarkus-hibernate-orm-rest-data-panache-deployment ${project.version} + + io.quarkus + quarkus-mongodb-rest-data-panache + ${project.version} + + + io.quarkus + quarkus-mongodb-rest-data-panache-deployment + ${project.version} + io.quarkus quarkus-mongodb-panache diff --git a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java index c7f0554418d6b..0291977649dbb 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/Feature.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/Feature.java @@ -62,6 +62,7 @@ public enum Feature { MONGODB_CLIENT, MONGODB_PANACHE, MONGODB_PANACHE_KOTLIN, + MONGODB_REST_DATA_PANACHE, MUTINY, NARAYANA_JTA, NARAYANA_STM, diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/pom.xml b/extensions/panache/mongodb-rest-data-panache/deployment/pom.xml new file mode 100644 index 0000000000000..b8ae1296c58d8 --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/deployment/pom.xml @@ -0,0 +1,48 @@ + + + 4.0.0 + + io.quarkus + quarkus-mongodb-rest-data-panache-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-mongodb-rest-data-panache-deployment + Quarkus - MongoDB REST data with Panache - Deployment + + + + io.quarkus + quarkus-mongodb-rest-data-panache + + + io.quarkus + quarkus-rest-data-panache-deployment + + + io.quarkus + quarkus-mongodb-panache-deployment + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/EntityDataAccessImplementor.java b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/EntityDataAccessImplementor.java new file mode 100644 index 0000000000000..15167d5b81dad --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/EntityDataAccessImplementor.java @@ -0,0 +1,65 @@ +package io.quarkus.mongodb.rest.data.panache.deployment; + +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +import java.util.List; + +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.mongodb.panache.PanacheQuery; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.rest.data.panache.deployment.DataAccessImplementor; + +final class EntityDataAccessImplementor implements DataAccessImplementor { + + private final String entityClassName; + + EntityDataAccessImplementor(String entityClassName) { + this.entityClassName = entityClassName; + } + + @Override + public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { + return creator.invokeStaticMethod( + ofMethod(entityClassName, "findById", PanacheMongoEntityBase.class, Object.class), id); + } + + @Override + public ResultHandle listAll(BytecodeCreator creator, ResultHandle sort) { + return creator.invokeStaticMethod(ofMethod(entityClassName, "listAll", List.class, Sort.class), sort); + } + + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeStaticMethod( + ofMethod(entityClassName, "findAll", PanacheQuery.class, Sort.class), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + @Override + public ResultHandle persist(BytecodeCreator creator, ResultHandle entity) { + creator.invokeVirtualMethod(ofMethod(entityClassName, "persist", void.class), entity); + return entity; + } + + @Override + public ResultHandle update(BytecodeCreator creator, ResultHandle entity) { + creator.invokeVirtualMethod(ofMethod(entityClassName, "persistOrUpdate", void.class), entity); + return entity; + } + + @Override + public ResultHandle deleteById(BytecodeCreator creator, ResultHandle id) { + return creator.invokeStaticMethod(ofMethod(entityClassName, "deleteById", boolean.class, Object.class), id); + } + + @Override + public ResultHandle pageCount(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeStaticMethod(ofMethod(entityClassName, "findAll", PanacheQuery.class)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "pageCount", int.class), query); + } +} diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java new file mode 100644 index 0000000000000..809cd588a3feb --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/MongoPanacheRestProcessor.java @@ -0,0 +1,121 @@ +package io.quarkus.mongodb.rest.data.panache.deployment; + +import static io.quarkus.deployment.Feature.MONGODB_REST_DATA_PANACHE; + +import java.lang.reflect.Modifier; +import java.util.List; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Type; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; + +import io.quarkus.arc.deployment.UnremovableBeanBuildItem; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.gizmo.Gizmo; +import io.quarkus.mongodb.rest.data.panache.PanacheMongoEntityResource; +import io.quarkus.mongodb.rest.data.panache.PanacheMongoRepositoryResource; +import io.quarkus.rest.data.panache.deployment.DataAccessImplementor; +import io.quarkus.rest.data.panache.deployment.RestDataEntityInfo; +import io.quarkus.rest.data.panache.deployment.RestDataResourceBuildItem; +import io.quarkus.rest.data.panache.deployment.RestDataResourceInfo; + +class MongoPanacheRestProcessor { + + private static final DotName PANACHE_MONGO_ENTITY_RESOURCE_INTERFACE = DotName + .createSimple(PanacheMongoEntityResource.class.getName()); + + private static final DotName PANACHE_MONGO_REPOSITORY_RESOURCE_INTERFACE = DotName + .createSimple(PanacheMongoRepositoryResource.class.getName()); + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem(MONGODB_REST_DATA_PANACHE); + } + + @BuildStep + void findEntityResources(CombinedIndexBuildItem index, BuildProducer resourcesProducer) { + RestDataEntityInfoProvider entityInfoProvider = new RestDataEntityInfoProvider(index.getIndex()); + for (ClassInfo classInfo : index.getIndex().getKnownDirectImplementors(PANACHE_MONGO_ENTITY_RESOURCE_INTERFACE)) { + validateResource(index.getIndex(), classInfo); + List generics = getGenericTypes(classInfo); + String entityClassName = generics.get(0).toString(); + String idClassName = generics.get(1).toString(); + RestDataEntityInfo entityInfo = entityInfoProvider.get(entityClassName, idClassName); + DataAccessImplementor dataAccessImplementor = new EntityDataAccessImplementor(entityClassName); + RestDataResourceInfo resourceInfo = new RestDataResourceInfo(classInfo.toString(), entityInfo, + dataAccessImplementor); + resourcesProducer.produce(new RestDataResourceBuildItem(resourceInfo)); + } + } + + @BuildStep + void findRepositoryResources(CombinedIndexBuildItem index, BuildProducer resourcesProducer, + BuildProducer unremovableBeansProducer, + BuildProducer bytecodeTransformersProducer) { + RestDataEntityInfoProvider entityInfoProvider = new RestDataEntityInfoProvider(index.getIndex()); + for (ClassInfo classInfo : index.getIndex().getKnownDirectImplementors(PANACHE_MONGO_REPOSITORY_RESOURCE_INTERFACE)) { + validateResource(index.getIndex(), classInfo); + List generics = getGenericTypes(classInfo); + String repositoryClassName = generics.get(0).toString(); + String entityClassName = generics.get(1).toString(); + String idClassName = generics.get(2).toString(); + RestDataEntityInfo entityInfo = entityInfoProvider.get(entityClassName, idClassName); + DataAccessImplementor dataAccessImplementor = new RepositoryDataAccessImplementor(repositoryClassName); + RestDataResourceInfo resourceInfo = new RestDataResourceInfo(classInfo.toString(), entityInfo, + dataAccessImplementor); + resourcesProducer.produce(new RestDataResourceBuildItem(resourceInfo)); + unremovableBeansProducer.produce( + new UnremovableBeanBuildItem(new UnremovableBeanBuildItem.BeanClassNameExclusion(repositoryClassName))); + bytecodeTransformersProducer + .produce(getEntityIdAnnotationTransformer(entityClassName, entityInfo.getIdField().name())); + } + } + + private void validateResource(IndexView index, ClassInfo classInfo) { + if (!Modifier.isInterface(classInfo.flags())) { + throw new RuntimeException(classInfo.name() + " has to be an interface"); + } + + if (classInfo.interfaceNames().size() > 1) { + throw new RuntimeException(classInfo.name() + " should only extend REST Data Panache interface"); + } + + if (!index.getKnownDirectImplementors(classInfo.name()).isEmpty()) { + throw new RuntimeException(classInfo.name() + " should not be extended or implemented"); + } + } + + private List getGenericTypes(ClassInfo classInfo) { + return classInfo.interfaceTypes() + .stream() + .findFirst() + .orElseThrow(() -> new RuntimeException(classInfo.toString() + " does not have generic types")) + .asParameterizedType() + .arguments(); + } + + /** + * Annotate Mongo entity ID fields with a RESTEasy links annotation. + * Otherwise RESTEasy will not be able to generate links that use ID as path parameter. + */ + private BytecodeTransformerBuildItem getEntityIdAnnotationTransformer(String entityClassName, String idFieldName) { + return new BytecodeTransformerBuildItem(entityClassName, + (className, classVisitor) -> new ClassVisitor(Gizmo.ASM_API_VERSION, classVisitor) { + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + FieldVisitor fieldVisitor = super.visitField(access, name, descriptor, signature, value); + if (name.equals(idFieldName)) { + fieldVisitor.visitAnnotation("Lorg/jboss/resteasy/links/ResourceID;", true).visitEnd(); + } + return fieldVisitor; + } + }); + } +} diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RepositoryDataAccessImplementor.java b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RepositoryDataAccessImplementor.java new file mode 100644 index 0000000000000..f7f6b719908fb --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RepositoryDataAccessImplementor.java @@ -0,0 +1,90 @@ +package io.quarkus.mongodb.rest.data.panache.deployment; + +import static io.quarkus.gizmo.MethodDescriptor.ofMethod; + +import java.lang.annotation.Annotation; +import java.util.List; + +import io.quarkus.arc.Arc; +import io.quarkus.arc.ArcContainer; +import io.quarkus.arc.InstanceHandle; +import io.quarkus.gizmo.BytecodeCreator; +import io.quarkus.gizmo.ResultHandle; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.mongodb.panache.PanacheQuery; +import io.quarkus.panache.common.Page; +import io.quarkus.panache.common.Sort; +import io.quarkus.rest.data.panache.deployment.DataAccessImplementor; + +final class RepositoryDataAccessImplementor implements DataAccessImplementor { + + private final String repositoryClassName; + + RepositoryDataAccessImplementor(String repositoryClassName) { + this.repositoryClassName = repositoryClassName; + } + + @Override + public ResultHandle findById(BytecodeCreator creator, ResultHandle id) { + return creator.invokeInterfaceMethod(ofMethod(PanacheMongoRepositoryBase.class, "findById", Object.class, Object.class), + getRepositoryInstance(creator), id); + } + + @Override + public ResultHandle listAll(BytecodeCreator creator, ResultHandle sort) { + return creator.invokeInterfaceMethod(ofMethod(PanacheMongoRepositoryBase.class, "listAll", List.class, Sort.class), + getRepositoryInstance(creator), sort); + } + + @Override + public ResultHandle findAll(BytecodeCreator creator, ResultHandle page, ResultHandle sort) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheMongoRepositoryBase.class, "findAll", PanacheQuery.class, Sort.class), + getRepositoryInstance(creator), sort); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "list", List.class), query); + } + + @Override + public ResultHandle persist(BytecodeCreator creator, ResultHandle entity) { + creator.invokeInterfaceMethod(ofMethod(PanacheMongoRepositoryBase.class, "persist", void.class, Object.class), + getRepositoryInstance(creator), entity); + return entity; + } + + @Override + public ResultHandle update(BytecodeCreator creator, ResultHandle entity) { + creator.invokeInterfaceMethod(ofMethod(PanacheMongoRepositoryBase.class, "persistOrUpdate", void.class, Object.class), + getRepositoryInstance(creator), entity); + return entity; + } + + @Override + public ResultHandle deleteById(BytecodeCreator creator, ResultHandle id) { + return creator.invokeInterfaceMethod( + ofMethod(PanacheMongoRepositoryBase.class, "deleteById", boolean.class, Object.class), + getRepositoryInstance(creator), id); + } + + @Override + public ResultHandle pageCount(BytecodeCreator creator, ResultHandle page) { + ResultHandle query = creator.invokeInterfaceMethod( + ofMethod(PanacheMongoRepositoryBase.class, "findAll", PanacheQuery.class), getRepositoryInstance(creator)); + creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "page", PanacheQuery.class, Page.class), query, page); + return creator.invokeInterfaceMethod(ofMethod(PanacheQuery.class, "pageCount", int.class), query); + } + + private ResultHandle getRepositoryInstance(BytecodeCreator creator) { + ResultHandle arcContainer = creator.invokeStaticMethod(ofMethod(Arc.class, "container", ArcContainer.class)); + ResultHandle instanceHandle = creator.invokeInterfaceMethod( + ofMethod(ArcContainer.class, "instance", InstanceHandle.class, Class.class, Annotation[].class), + arcContainer, creator.loadClass(repositoryClassName), creator.newArray(Annotation.class, 0)); + ResultHandle instance = creator.invokeInterfaceMethod( + ofMethod(InstanceHandle.class, "get", Object.class), instanceHandle); + + creator.ifNull(instance).trueBranch() + .throwException(RuntimeException.class, repositoryClassName + " instance was not found"); + + return instance; + } +} diff --git a/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RestDataEntityInfoProvider.java b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RestDataEntityInfoProvider.java new file mode 100644 index 0000000000000..adb1223d35c6a --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/deployment/src/main/java/io/quarkus/mongodb/rest/data/panache/deployment/RestDataEntityInfoProvider.java @@ -0,0 +1,86 @@ +package io.quarkus.mongodb.rest.data.panache.deployment; + +import org.bson.codecs.pojo.annotations.BsonId; +import org.bson.types.ObjectId; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; + +import io.quarkus.deployment.bean.JavaBeanUtil; +import io.quarkus.gizmo.MethodDescriptor; +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.rest.data.panache.deployment.RestDataEntityInfo; + +final class RestDataEntityInfoProvider { + + private static final DotName PANACHE_MONGO_ENTITY_BASE = DotName.createSimple(PanacheMongoEntityBase.class.getName()); + + private static final DotName OBJECT_ID = DotName.createSimple(ObjectId.class.getName()); + + private static final DotName BSON_ID_ANNOTATION = DotName.createSimple(BsonId.class.getName()); + + private final IndexView index; + + RestDataEntityInfoProvider(IndexView index) { + this.index = index; + } + + RestDataEntityInfo get(String entityType, String idType) { + ClassInfo classInfo = index.getClassByName(DotName.createSimple(entityType)); + FieldInfo idField = getIdField(classInfo); + return new RestDataEntityInfo(classInfo.toString(), idType, idField, getSetter(classInfo, idField, idType)); + } + + private FieldInfo getIdField(ClassInfo classInfo) { + ClassInfo tmpClassInfo = classInfo; + while (tmpClassInfo != null) { + for (FieldInfo field : tmpClassInfo.fields()) { + if (field.type().name().equals(OBJECT_ID) || field.hasAnnotation(BSON_ID_ANNOTATION)) { + return field; + } + } + if (classInfo.superName() != null) { + tmpClassInfo = index.getClassByName(classInfo.superName()); + } else { + tmpClassInfo = null; + } + } + throw new IllegalArgumentException("Couldn't find id field of " + classInfo); + } + + private MethodDescriptor getSetter(ClassInfo entityClass, FieldInfo field, String fieldType) { + if (isPanacheEntity(entityClass)) { + return getPanacheEntitySetter(entityClass, field.name(), fieldType); + } + return getGenericEntitySetter(entityClass, field); + } + + private boolean isPanacheEntity(ClassInfo entityClass) { + if (entityClass == null || entityClass.superName() == null) { + return false; + } + if (PANACHE_MONGO_ENTITY_BASE.equals(entityClass.superName())) { + return true; + } + return isPanacheEntity(index.getClassByName(entityClass.superName())); + } + + private MethodDescriptor getPanacheEntitySetter(ClassInfo entityClass, String fieldName, String fieldType) { + return MethodDescriptor.ofMethod(entityClass.toString(), JavaBeanUtil.getSetterName(fieldName), void.class, fieldType); + } + + private MethodDescriptor getGenericEntitySetter(ClassInfo entityClass, FieldInfo field) { + if (entityClass == null) { + return null; + } + MethodInfo methodInfo = entityClass.method(JavaBeanUtil.getSetterName(field.name()), field.type()); + if (methodInfo != null) { + return MethodDescriptor.of(methodInfo); + } else if (entityClass.superName() != null) { + return getGenericEntitySetter(index.getClassByName(entityClass.superName()), field); + } + return null; + } +} diff --git a/extensions/panache/mongodb-rest-data-panache/pom.xml b/extensions/panache/mongodb-rest-data-panache/pom.xml new file mode 100644 index 0000000000000..c9b287775b9df --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/pom.xml @@ -0,0 +1,21 @@ + + + 4.0.0 + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + + quarkus-mongodb-rest-data-panache-parent + Quarkus - MongoDB REST data with Panache - Parent + pom + + + deployment + runtime + + diff --git a/extensions/panache/mongodb-rest-data-panache/runtime/pom.xml b/extensions/panache/mongodb-rest-data-panache/runtime/pom.xml new file mode 100644 index 0000000000000..38260d4dd6967 --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/runtime/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.quarkus + quarkus-mongodb-rest-data-panache-parent + 999-SNAPSHOT + ../pom.xml + + + quarkus-mongodb-rest-data-panache + Quarkus - MongoDB REST data with Panache - Runtime + Generate JAX-RS resources for your MongoDB entities and repositories + + + + io.quarkus + quarkus-rest-data-panache + + + io.quarkus + quarkus-mongodb-panache + + + + jakarta.persistence + jakarta.persistence-api + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + diff --git a/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java new file mode 100644 index 0000000000000..83caf44a8f423 --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoEntityResource.java @@ -0,0 +1,21 @@ +package io.quarkus.mongodb.rest.data.panache; + +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.rest.data.panache.MethodProperties; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.rest.data.panache.RestDataResource; + +/** + * REST data Panache resource that uses {@link PanacheMongoEntityBase} instance for data access and exposes it as a JAX-RS + * resource. + *

+ * See {@link RestDataResource} for the methods provided by this resource. + *

+ * See {@link ResourceProperties} and {@link MethodProperties} for the ways to customize this resource. + * + * @param {@link PanacheMongoEntityBase} that is handled by this resource. + * @param ID type of the entity. + */ +public interface PanacheMongoEntityResource extends RestDataResource { + +} diff --git a/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java new file mode 100644 index 0000000000000..fb517235f33ec --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/java/io/quarkus/mongodb/rest/data/panache/PanacheMongoRepositoryResource.java @@ -0,0 +1,23 @@ +package io.quarkus.mongodb.rest.data.panache; + +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.rest.data.panache.MethodProperties; +import io.quarkus.rest.data.panache.ResourceProperties; +import io.quarkus.rest.data.panache.RestDataResource; + +/** + * REST data Panache resource that uses {@link PanacheMongoRepositoryBase} instance for data access and exposes it as a JAX-RS + * resource. + *

+ * See {@link RestDataResource} for the methods provided by this resource. + *

+ * See {@link ResourceProperties} and {@link MethodProperties} for the ways to customize this resource. + * + * @param {@link PanacheMongoRepositoryBase} instance that should be used for data access. + * @param Entity type that is handled by this resource and the linked {@link PanacheMongoRepositoryBase} instance. + * @param ID type of the entity. + */ +public interface PanacheMongoRepositoryResource, Entity, ID> + extends RestDataResource { + +} diff --git a/extensions/panache/mongodb-rest-data-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 0000000000000..aaf2d62245861 --- /dev/null +++ b/extensions/panache/mongodb-rest-data-panache/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,15 @@ +--- +name: "REST resources for MongoDB with Panache" +metadata: + keywords: + - "mongodb-panache" + - "panache" + - "mongodb" + - "rest" + - "jaxrs" + - "resteasy" + guide: "https://quarkus.io/guides/rest-data-panache" + categories: + - "data" + - "web" + status: "experimental" diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml index 9259ac07a72bd..b1f972fa0003a 100644 --- a/extensions/panache/pom.xml +++ b/extensions/panache/pom.xml @@ -28,6 +28,7 @@ panacheql rest-data-panache hibernate-orm-rest-data-panache + mongodb-rest-data-panache diff --git a/integration-tests/mongodb-rest-data-panache/pom.xml b/integration-tests/mongodb-rest-data-panache/pom.xml new file mode 100644 index 0000000000000..e64dafda461ec --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/pom.xml @@ -0,0 +1,144 @@ + + + 4.0.0 + + quarkus-integration-tests-parent + io.quarkus + 999-SNAPSHOT + + + quarkus-integration-test-mongodb-rest-data-panache + Quarkus - Integration Tests - MongoDB REST Data with Panache + + + + io.quarkus + quarkus-mongodb-rest-data-panache + + + io.quarkus + quarkus-resteasy-jackson + + + io.quarkus + quarkus-junit5 + test + + + org.assertj + assertj-core + test + + + io.rest-assured + rest-assured + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + + + io.quarkus + quarkus-mongodb-rest-data-panache-deployment + ${project.version} + pom + test + + + * + * + + + + + io.quarkus + quarkus-resteasy-jackson-deployment + ${project.version} + pom + test + + + * + * + + + + + + + + + io.quarkus + quarkus-maven-plugin + + + + build + + + + + + maven-compiler-plugin + + true + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + io.quarkus + quarkus-maven-plugin + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Author.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Author.java new file mode 100644 index 0000000000000..7c8e71f6b9ae1 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Author.java @@ -0,0 +1,17 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import java.time.LocalDate; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.quarkus.mongodb.panache.MongoEntity; +import io.quarkus.mongodb.panache.PanacheMongoEntity; + +@MongoEntity +public class Author extends PanacheMongoEntity { + + public String name; + + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + public LocalDate dob; +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/AuthorsResource.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/AuthorsResource.java new file mode 100644 index 0000000000000..fcb3f0a98abde --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/AuthorsResource.java @@ -0,0 +1,20 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import javax.ws.rs.core.Response; + +import org.bson.types.ObjectId; + +import io.quarkus.mongodb.rest.data.panache.PanacheMongoEntityResource; +import io.quarkus.rest.data.panache.MethodProperties; + +public interface AuthorsResource extends PanacheMongoEntityResource { + + @MethodProperties(exposed = false) + Response add(Author entity); + + @MethodProperties(exposed = false) + Response update(ObjectId id, Author entity); + + @MethodProperties(exposed = false) + void delete(ObjectId id); +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Book.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Book.java new file mode 100644 index 0000000000000..4d6eeb913788a --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/Book.java @@ -0,0 +1,39 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import org.bson.types.ObjectId; + +import io.quarkus.mongodb.panache.MongoEntity; + +@MongoEntity +public class Book { + + private ObjectId id; + + private String title; + + private Author author; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BookRepository.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BookRepository.java new file mode 100644 index 0000000000000..aa967190d994c --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BookRepository.java @@ -0,0 +1,9 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepository; + +@ApplicationScoped +public class BookRepository implements PanacheMongoRepository { +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BooksResource.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BooksResource.java new file mode 100644 index 0000000000000..8379249f89ca5 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/BooksResource.java @@ -0,0 +1,10 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import org.bson.types.ObjectId; + +import io.quarkus.mongodb.rest.data.panache.PanacheMongoRepositoryResource; +import io.quarkus.rest.data.panache.ResourceProperties; + +@ResourceProperties(hal = true) +public interface BooksResource extends PanacheMongoRepositoryResource { +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/TestResource.java b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/TestResource.java new file mode 100644 index 0000000000000..c8238f6b178a5 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/java/io/quarkus/it/mongodb/rest/data/panache/TestResource.java @@ -0,0 +1,48 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import javax.enterprise.context.ApplicationScoped; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +@ApplicationScoped +@Path("/test") +public class TestResource { + + @Inject + BookRepository bookRepository; + + @POST + @Path("/authors") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Author createAuthor(Author author) { + author.persist(); + return author; + } + + @DELETE + @Path("/authors") + public void deleteAllAuthors() { + Author.deleteAll(); + } + + @POST + @Path("/books") + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Book createBook(Book book) { + bookRepository.persist(book); + return book; + } + + @DELETE + @Path("/books") + public void deleteAllBooks() { + bookRepository.deleteAll(); + } +} diff --git a/integration-tests/mongodb-rest-data-panache/src/main/resources/application.properties b/integration-tests/mongodb-rest-data-panache/src/main/resources/application.properties new file mode 100644 index 0000000000000..c3a2666d88ea0 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/main/resources/application.properties @@ -0,0 +1,3 @@ +quarkus.mongodb.connection-string=mongodb://localhost:27018 +quarkus.mongodb.write-concern.journal=false +quarkus.mongodb.database=test diff --git a/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheIT.java b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheIT.java new file mode 100644 index 0000000000000..ecfd9f0d78da0 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheIT.java @@ -0,0 +1,7 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import io.quarkus.test.junit.NativeImageTest; + +@NativeImageTest +class MongoDbRestDataPanacheIT extends MongoDbRestDataPanacheTest { +} diff --git a/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheTest.java b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheTest.java new file mode 100644 index 0000000000000..8507a862009c6 --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoDbRestDataPanacheTest.java @@ -0,0 +1,265 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.endsWith; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.is; + +import javax.json.Json; +import javax.json.JsonObject; +import javax.ws.rs.core.MediaType; + +import org.bson.types.ObjectId; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.response.Response; + +@QuarkusTest +@QuarkusTestResource(MongoTestResource.class) +class MongoDbRestDataPanacheTest { + + private Author dostoevsky; + + private Book crimeAndPunishment; + + private Book idiot; + + @BeforeEach + void beforeEach() { + dostoevsky = createTestAuthor("Fyodor Dostoevsky", "1821-11-11"); + crimeAndPunishment = createTestBook("Crime and Punishment", dostoevsky); + idiot = createTestBook("Idiot", dostoevsky); + } + + @AfterEach + void afterEach() { + deleteTestBooks(); + deleteTestAuthors(); + } + + @Test + void shouldGetAuthor() { + given().accept("application/json") + .when().get("/authors/" + dostoevsky.id) + .then().statusCode(200) + .and().body("id", is(equalTo(dostoevsky.id.toString()))) + .and().body("name", is(equalTo(dostoevsky.name))) + .and().body("dob", is(equalTo(dostoevsky.dob.toString()))); + } + + @Test + void shouldNotGetAuthorHal() { + given().accept("application/hal+json") + .when().get("/authors/" + dostoevsky.id) + .then().statusCode(406); + } + + @Test + void shouldGetBook() { + given().accept("application/json") + .when().get("/books/" + crimeAndPunishment.getId()) + .then().statusCode(200) + .and().body("id", is(equalTo(crimeAndPunishment.getId().toString()))) + .and().body("title", is(equalTo(crimeAndPunishment.getTitle()))) + .and().body("author.id", is(equalTo(dostoevsky.id.toString()))) + .and().body("author.name", is(equalTo(dostoevsky.name))) + .and().body("author.dob", is(equalTo(dostoevsky.dob.toString()))); + } + + @Test + void shouldGetBookHal() { + given().accept("application/hal+json") + .when().get("/books/" + crimeAndPunishment.getId()) + .then().statusCode(200) + .and().body("id", is(equalTo(crimeAndPunishment.getId().toString()))) + .and().body("title", is(equalTo(crimeAndPunishment.getTitle()))) + .and().body("author.id", is(equalTo(dostoevsky.id.toString()))) + .and().body("author.name", is(equalTo(dostoevsky.name))) + .and().body("author.dob", is(equalTo(dostoevsky.dob.toString()))) + .and().body("_links.add.href", endsWith("/books")) + .and().body("_links.list.href", endsWith("/books")) + .and().body("_links.self.href", endsWith("/books/" + crimeAndPunishment.getId())) + .and().body("_links.update.href", endsWith("/books/" + crimeAndPunishment.getId())) + .and().body("_links.remove.href", endsWith("/books/" + crimeAndPunishment.getId())); + } + + @Test + void shouldListAuthors() { + given().accept("application/json") + .when().get("/authors") + .then().statusCode(200) + .and().body("id", contains(dostoevsky.id.toString())) + .and().body("name", contains(dostoevsky.name)) + .and().body("dob", contains(dostoevsky.dob.toString())); + } + + @Test + void shouldListBooks() { + given().accept("application/json") + .when().get("/books") + .then().statusCode(200) + .and().body("id", contains(crimeAndPunishment.getId().toString(), idiot.getId().toString())) + .and().body("title", contains(crimeAndPunishment.getTitle(), idiot.getTitle())) + .and().body("author.id", contains(dostoevsky.id.toString(), dostoevsky.id.toString())) + .and().body("author.name", contains(dostoevsky.name, dostoevsky.name)) + .and().body("author.dob", contains(dostoevsky.dob.toString(), dostoevsky.dob.toString())); + } + + @Test + void shouldListBooksHal() { + given().accept("application/hal+json") + .when().get("/books") + .then().statusCode(200) + .and().body("_embedded.books.id", contains(crimeAndPunishment.getId().toString(), idiot.getId().toString())) + .and().body("_embedded.books.title", contains(crimeAndPunishment.getTitle(), idiot.getTitle())) + .and().body("_embedded.books.author.id", contains(dostoevsky.id.toString(), dostoevsky.id.toString())) + .and().body("_embedded.books.author.name", contains(dostoevsky.name, dostoevsky.name)) + .and().body("_embedded.books.author.dob", contains(dostoevsky.dob.toString(), dostoevsky.dob.toString())) + .and().body("_embedded.books._links.add.href", contains(endsWith("/books"), endsWith("/books"))) + .and().body("_embedded.books._links.list.href", contains(endsWith("/books"), endsWith("/books"))) + .and().body("_embedded.books._links.self.href", + contains(endsWith("/books/" + crimeAndPunishment.getId()), endsWith("/books/" + idiot.getId()))) + .and().body("_embedded.books._links.update.href", + contains(endsWith("/books/" + crimeAndPunishment.getId()), endsWith("/books/" + idiot.getId()))) + .and().body("_embedded.books._links.remove.href", + contains(endsWith("/books/" + crimeAndPunishment.getId()), endsWith("/books/" + idiot.getId()))) + .and().body("_links.add.href", endsWith("/books")) + .and().body("_links.list.href", endsWith("/books")); + } + + @Test + void shouldNotCreateOrDeleteAuthor() { + JsonObject author = Json.createObjectBuilder() + .add("name", "test") + .add("dob", "1900-01-01") + .build(); + given().contentType("application/json") + .and().body(author.toString()) + .when().post("/authors") + .then().statusCode(405); + when().delete("/authors/" + dostoevsky.id) + .then().statusCode(405); + } + + @Test + void shouldCreateAndDeleteBook() { + JsonObject book = Json.createObjectBuilder() + .add("title", "The Brothers Karamazov") + .add("author", Json.createObjectBuilder() + .add("id", dostoevsky.id.toString()) + .add("name", dostoevsky.name) + .add("dob", dostoevsky.dob.toString()) + .build()) + .build(); + Response response = given().accept("application/json") + .and().contentType("application/json") + .and().body(book.toString()) + .when().post("/books") + .thenReturn(); + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.header("Location")).isNotEmpty(); + + when().delete(response.getHeader("Location")) + .then().statusCode(204); + given().accept("application/json") + .when().get(response.getHeader("Location")) + .then().statusCode(404); + } + + @Test + void shouldNotUpdateAuthor() { + JsonObject author = Json.createObjectBuilder() + .add("id", dostoevsky.id.toString()) + .add("name", "test") + .add("dob", dostoevsky.dob.toString()) + .build(); + given().contentType("application/json") + .and().body(author.toString()) + .when().put("/authors/" + dostoevsky.id) + .then().statusCode(405); + } + + @Test + void shouldCreateUpdateAndDeleteBook() { + JsonObject dostoevskyJson = Json.createObjectBuilder() + .add("id", dostoevsky.id.toString()) + .add("name", dostoevsky.name) + .add("dob", dostoevsky.dob.toString()) + .build(); + JsonObject book = Json.createObjectBuilder() + .add("title", "The Brothers Karamazov") + .add("author", dostoevskyJson) + .build(); + Response response = given().accept("application/json") + .and().contentType("application/json") + .and().body(book.toString()) + .when().put("/books/" + new ObjectId()) + .thenReturn(); + assertThat(response.statusCode()).isEqualTo(201); + assertThat(response.header("Location")).isNotEmpty(); + assertThat(response.body().jsonPath().getString("title")).isEqualTo("The Brothers Karamazov"); + + String location = response.header("Location"); + JsonObject updateBook = Json.createObjectBuilder() + .add("title", "Notes from Underground") + .add("author", dostoevskyJson) + .build(); + given().accept("application/json") + .and().contentType("application/json") + .and().body(updateBook.toString()) + .when().put(location) + .then().statusCode(204); + given().accept("application/json") + .when().get(location) + .then().body("title", is(equalTo("Notes from Underground"))); + when().delete(location) + .then().statusCode(204); + } + + private Author createTestAuthor(String name, String dob) { + return given().contentType(MediaType.APPLICATION_JSON) + .and().accept(MediaType.APPLICATION_JSON) + .and().body(Json.createObjectBuilder() + .add("name", name) + .add("dob", dob) + .build() + .toString()) + .post("/test/authors") + .thenReturn() + .as(Author.class); + } + + private void deleteTestAuthors() { + when().delete("/test/authors") + .then().statusCode(204); + } + + private Book createTestBook(String title, Author author) { + return given().contentType(MediaType.APPLICATION_JSON) + .and().accept(MediaType.APPLICATION_JSON) + .and().body(Json.createObjectBuilder() + .add("title", title) + .add("author", Json.createObjectBuilder() + .add("id", author.id.toString()) + .add("name", author.name) + .add("dob", author.dob.toString()) + .build()) + .build() + .toString()) + .post("/test/books") + .thenReturn() + .as(Book.class); + } + + private void deleteTestBooks() { + when().delete("/test/books") + .then().statusCode(204); + } +} diff --git a/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoTestResource.java b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoTestResource.java new file mode 100644 index 0000000000000..3adc2b8d2a27a --- /dev/null +++ b/integration-tests/mongodb-rest-data-panache/src/test/java/io/quarkus/it/mongodb/rest/data/panache/MongoTestResource.java @@ -0,0 +1,75 @@ +package io.quarkus.it.mongodb.rest.data.panache; + +import java.util.Collections; +import java.util.Map; + +import org.jboss.logging.Logger; + +import de.flapdoodle.embed.mongo.MongodExecutable; +import de.flapdoodle.embed.mongo.MongodStarter; +import de.flapdoodle.embed.mongo.config.IMongodConfig; +import de.flapdoodle.embed.mongo.config.MongodConfigBuilder; +import de.flapdoodle.embed.mongo.config.Net; +import de.flapdoodle.embed.mongo.distribution.Version; +import de.flapdoodle.embed.process.runtime.Network; +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class MongoTestResource implements QuarkusTestResourceLifecycleManager { + + private static final Logger LOGGER = Logger.getLogger(MongoTestResource.class); + private static MongodExecutable MONGO; + + @Override + public Map start() { + try { + Version.Main version = Version.Main.V4_0; + int port = 27018; + LOGGER.infof("Starting Mongo %s on port %s", version, port); + IMongodConfig config = new MongodConfigBuilder() + .version(version) + .net(new Net(port, Network.localhostIsIPv6())) + .build(); + MONGO = getMongodExecutable(config); + try { + MONGO.start(); + } catch (Exception e) { + //every so often mongo fails to start on CI runs + //see if this helps + Thread.sleep(1000); + MONGO.start(); + } + return Collections.emptyMap(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private MongodExecutable getMongodExecutable(IMongodConfig config) { + try { + return doGetExecutable(config); + } catch (Exception e) { + // sometimes the download process can timeout so just sleep and try again + try { + Thread.sleep(1000); + } catch (InterruptedException ignored) { + + } + return doGetExecutable(config); + } + } + + private MongodExecutable doGetExecutable(IMongodConfig config) { + return MongodStarter.getDefaultInstance().prepare(config); + } + + private void initData() { + + } + + @Override + public void stop() { + if (MONGO != null) { + MONGO.stop(); + } + } +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index 42df019ea591f..64ddf1c59b55e 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -106,6 +106,7 @@ kotlin mongodb-panache mongodb-panache-kotlin + mongodb-rest-data-panache narayana-stm narayana-jta elytron-security-jdbc