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