From 1946498698eef487ddb5a7635e4818de8c6fe105 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Mon, 23 Sep 2019 15:48:09 +0200 Subject: [PATCH] feat: implements MongoDB with Panache --- azure-pipelines.yml | 1 + bom/deployment/pom.xml | 5 + bom/runtime/pom.xml | 29 +- build-parent/pom.xml | 8 + .../builditem/FeatureBuildItem.java | 1 + .../common/src/main/filtered/extensions.json | 12 + docs/src/main/asciidoc/index.adoc | 2 + docs/src/main/asciidoc/mongo-guide.adoc | 3 + .../main/asciidoc/mongodb-panache-guide.adoc | 512 ++++++++++ .../mongodb-panache/deployment/pom.xml | 57 ++ .../PanacheMongoEntityEnhancer.java | 89 ++ .../PanacheMongoRepositoryEnhancer.java | 85 ++ .../deployment/PanacheResourceProcessor.java | 121 +++ extensions/panache/mongodb-panache/pom.xml | 22 + .../panache/mongodb-panache/runtime/pom.xml | 95 ++ .../quarkus/mongodb/panache/MongoEntity.java | 21 + .../mongodb/panache/PanacheMongoEntity.java | 30 + .../panache/PanacheMongoEntityBase.java | 870 +++++++++++++++++ .../panache/PanacheMongoRepository.java | 15 + .../panache/PanacheMongoRepositoryBase.java | 875 ++++++++++++++++++ .../quarkus/mongodb/panache/PanacheQuery.java | 157 ++++ .../panache/jackson/ObjectIdDeserializer.java | 23 + .../panache/jackson/ObjectIdSerializer.java | 20 + .../panache/jackson/ObjectMapperProducer.java | 32 + .../panache/jsonb/ObjectIdDeserializer.java | 20 + .../panache/jsonb/ObjectIdSerializer.java | 17 + .../PanacheMongoJsonbContextResolver.java | 25 + .../quarkus/mongodb/panache/package-info.java | 70 ++ .../panache/runtime/CommonQueryBinder.java | 39 + .../panache/runtime/MongoOperations.java | 547 +++++++++++ .../panache/runtime/MongoParserVisitor.java | 103 +++ .../panache/runtime/NativeQueryBinder.java | 26 + .../panache/runtime/PanacheQlQueryBinder.java | 83 ++ .../panache/runtime/PanacheQueryImpl.java | 146 +++ .../panache/runtime/MongoOperationsTest.java | 208 +++++ extensions/panache/panacheql/pom.xml | 53 ++ .../io/quarkus/panacheql/internal/HqlLexer.g4 | 231 +++++ .../quarkus/panacheql/internal/HqlParser.g4 | 822 ++++++++++++++++ .../java/io/quarkus/panacheql/LexerTest.java | 66 ++ extensions/panache/pom.xml | 2 + extensions/resteasy-common/deployment/pom.xml | 4 + .../deployment/ResteasyCommonProcessor.java | 1 + extensions/resteasy-common/pom.xml | 1 + extensions/resteasy-common/spi/pom.xml | 23 + .../spi}/ResteasyJaxrsProviderBuildItem.java | 2 +- .../deployment/ResteasyJacksonProcessor.java | 4 +- .../deployment/ResteasyJsonbProcessor.java | 2 +- .../deployment/ResteasyBuiltinsProcessor.java | 2 +- .../deployment/ResteasyServletProcessor.java | 0 .../SmallRyeOpenTracingProcessor.java | 2 +- extensions/spring-web/deployment/pom.xml | 4 + .../web/deployment/SpringWebProcessor.java | 2 +- integration-tests/mongodb-panache/pom.xml | 146 +++ .../quarkus/it/mongodb/panache/book/Book.java | 90 ++ .../it/mongodb/panache/book/BookDetail.java | 26 + .../it/mongodb/panache/book/BookEntity.java | 80 ++ .../panache/book/BookEntityResource.java | 105 +++ .../mongodb/panache/book/BookRepository.java | 9 + .../panache/book/BookRepositoryResource.java | 108 +++ .../it/mongodb/panache/person/Person.java | 10 + .../mongodb/panache/person/PersonEntity.java | 12 + .../panache/person/PersonEntityResource.java | 73 ++ .../panache/person/PersonRepository.java | 9 + .../person/PersonRepositoryResource.java | 78 ++ .../src/main/resources/application.properties | 5 + .../quarkus/it/mongodb/panache/BookDTO.java | 100 ++ .../it/mongodb/panache/BookResourceTest.java | 327 +++++++ .../mongodb/panache/NativeBookResourceIT.java | 8 + integration-tests/pom.xml | 3 +- 69 files changed, 6770 insertions(+), 9 deletions(-) create mode 100644 docs/src/main/asciidoc/mongodb-panache-guide.adoc create mode 100644 extensions/panache/mongodb-panache/deployment/pom.xml create mode 100644 extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java create mode 100644 extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java create mode 100644 extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java create mode 100644 extensions/panache/mongodb-panache/pom.xml create mode 100644 extensions/panache/mongodb-panache/runtime/pom.xml create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/MongoEntity.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdDeserializer.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdSerializer.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectMapperProducer.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdDeserializer.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdSerializer.java create mode 100755 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/PanacheMongoJsonbContextResolver.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoParserVisitor.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/NativeQueryBinder.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java create mode 100644 extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java create mode 100644 extensions/panache/panacheql/pom.xml create mode 100644 extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4 create mode 100644 extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4 create mode 100644 extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java create mode 100644 extensions/resteasy-common/spi/pom.xml rename extensions/resteasy-common/{deployment/src/main/java/io/quarkus/resteasy/common/deployment => spi/src/main/java/io/quarkus/resteasy/common/spi}/ResteasyJaxrsProviderBuildItem.java (88%) mode change 100755 => 100644 extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java create mode 100755 integration-tests/mongodb-panache/pom.xml create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/Book.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookDetail.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntity.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntityResource.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepository.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepositoryResource.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntityResource.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepository.java create mode 100644 integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java create mode 100644 integration-tests/mongodb-panache/src/main/resources/application.properties create mode 100644 integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookDTO.java create mode 100644 integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookResourceTest.java create mode 100644 integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/NativeBookResourceIT.java diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 00e3561160a8a..d137beab2569a 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -261,6 +261,7 @@ stages: - infinispan-cache-jpa - infinispan-client - mongodb-client + - mongodb-panache - neo4j name: data diff --git a/bom/deployment/pom.xml b/bom/deployment/pom.xml index c6711fbdd1f4c..c0358eb90323e 100644 --- a/bom/deployment/pom.xml +++ b/bom/deployment/pom.xml @@ -216,6 +216,11 @@ quarkus-resteasy-server-common-spi ${project.version} + + io.quarkus + quarkus-resteasy-common-spi + ${project.version} + io.quarkus quarkus-resteasy-server-common-deployment diff --git a/bom/runtime/pom.xml b/bom/runtime/pom.xml index aad9a845ff911..0d6438f47c19b 100644 --- a/bom/runtime/pom.xml +++ b/bom/runtime/pom.xml @@ -163,6 +163,8 @@ 2.1.9.RELEASE 2.4.4.Final 3.0.0 + 5.3.1 + 4.7.2 @@ -317,11 +319,21 @@ quarkus-panache-common ${project.version} + + io.quarkus + quarkus-panacheql + ${project.version} + io.quarkus quarkus-hibernate-orm-panache ${project.version} + + io.quarkus + quarkus-mongodb-panache + ${project.version} + io.quarkus quarkus-hibernate-search-elasticsearch @@ -693,7 +705,6 @@ - commons-logging commons-logging @@ -876,6 +887,11 @@ ${debezium.version} test-jar + + org.antlr + antlr4-runtime + ${antlr.version} + org.scala-lang scala-reflect @@ -1243,11 +1259,22 @@ json-smart ${json-smart.version} + + + net.java.dev.jna + jna + ${jna.version} + org.testcontainers testcontainers ${test-containers.version} + + org.testcontainers + junit-jupiter + ${test-containers.version} + org.codehaus.plexus diff --git a/build-parent/pom.xml b/build-parent/pom.xml index bee4d6e4bab8b..05d659fc2a83f 100644 --- a/build-parent/pom.xml +++ b/build-parent/pom.xml @@ -58,6 +58,9 @@ 1.0 1.2.1 1.1.2 + + + 4.7.2 @@ -245,6 +248,11 @@ ${elasticsearch-server.version} + + org.antlr + antlr4-maven-plugin + ${antlr.version} + diff --git a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java index 8b1f80a28f564..82e44bd0f6ccc 100644 --- a/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java +++ b/core/deployment/src/main/java/io/quarkus/deployment/builditem/FeatureBuildItem.java @@ -34,6 +34,7 @@ public final class FeatureBuildItem extends MultiBuildItem { public static final String KUBERNETES_CLIENT = "kubernetes-client"; public static final String MAILER = "mailer"; public static final String MONGODB_CLIENT = "mongodb-client"; + public static final String MONGODB_PANACHE = "mongodb-panache"; public static final String NARAYANA_JTA = "narayana-jta"; public static final String REACTIVE_PG_CLIENT = "reactive-pg-client"; public static final String REACTIVE_MYSQL_CLIENT = "reactive-mysql-client"; diff --git a/devtools/common/src/main/filtered/extensions.json b/devtools/common/src/main/filtered/extensions.json index c3597e9dca8f8..332f6c288145c 100644 --- a/devtools/common/src/main/filtered/extensions.json +++ b/devtools/common/src/main/filtered/extensions.json @@ -305,6 +305,18 @@ "groupId": "io.quarkus", "artifactId": "quarkus-mongodb-client" }, + { + "name": "MongoDB with Panache", + "labels": [ + "mongo", + "mongodb", + "nosql", + "datastore", + "panache" + ], + "groupId": "io.quarkus", + "artifactId": "quarkus-mongodb-panache" + }, { "name": "Narayana JTA - Transaction manager", "labels": [ diff --git a/docs/src/main/asciidoc/index.adoc b/docs/src/main/asciidoc/index.adoc index ec869f52f1971..df55b45b7e656 100644 --- a/docs/src/main/asciidoc/index.adoc +++ b/docs/src/main/asciidoc/index.adoc @@ -47,6 +47,8 @@ include::quarkus-intro.adoc[tag=intro] * link:kogito-guide.html[Using Kogito (business automation with processes and rules)] * link:oauth2-guide.html[Using OAuth2 RBAC] * link:tika-guide.html[Using Apache Tika] +* link:mongo-guide.html[Using MongoDB] +* link:mongodb-panache-guide.html[Simplified MongoDB with Panache] * link:faq.html[FAQs] diff --git a/docs/src/main/asciidoc/mongo-guide.adoc b/docs/src/main/asciidoc/mongo-guide.adoc index a10186fd7d258..141ee6a3b44fe 100644 --- a/docs/src/main/asciidoc/mongo-guide.adoc +++ b/docs/src/main/asciidoc/mongo-guide.adoc @@ -462,6 +462,9 @@ public class CodecFruitService { } ---- +== Simplifying MongoDB with Panache + +The link:mongodb-panache-guide.html[MongoDB with Panache] extension facilitates the usage of MongoDB by providing active record style entities (and repositories) like you have in link:hibernate-orm-panache-guide.html[Hibernate ORM with Panache] and focuses on making your entities trivial and fun to write in Quarkus. == Building a native executable diff --git a/docs/src/main/asciidoc/mongodb-panache-guide.adoc b/docs/src/main/asciidoc/mongodb-panache-guide.adoc new file mode 100644 index 0000000000000..cc233fafc968a --- /dev/null +++ b/docs/src/main/asciidoc/mongodb-panache-guide.adoc @@ -0,0 +1,512 @@ +//// +This guide is maintained in the main Quarkus repository +and pull requests should be submitted there: +https://github.com/quarkusio/quarkus/tree/master/docs/src/main/asciidoc +//// += Quarkus - Simplified MongoDB with Panache + +include::./attributes.adoc[] +:config-file: application.properties + +MongoDB is a well known NoSQL Database that is widely used, but using its raw API can be cumbersome as you need to express your entities and your queries as a MongoDB link:https://mongodb.github.io/mongo-java-driver/3.11/bson/documents/#document[`Document`]. + +MongoDB with Panache provides active record style entities (and repositories) like you have in link:hibernate-orm-panache-guide.html[Hibernate ORM with Panache] and focuses on making your entities trivial and fun to write in Quarkus. + +It is built on top of the link:mongo-guide.html[MongoDB Client] extension. + +WARNING: This extension is still experimental, feedback is welcome on the https://groups.google.com/d/forum/quarkus-dev[mailing list] or as issues in our https://github.com/quarkusio/quarkus/issues[GitHub issue tracker]. + +== First: an example + +Panache allows you to write your MongoDB entities like this: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + public String name; + public LocalDate birth; + public Status status; + + public static Person findByName(String name){ + return find("name", name).firstResult(); + } + + public static List findAlive(){ + return list("status", Status.Alive); + } + + public static void deleteLoics(){ + delete("name", "Loïc"); + } +} +---- + +You have noticed how much more compact and readable the code is compared to using the MongoDB API? +Does this look interesting? Read on! + +NOTE: the `list()` method might be surprising at first. It takes fragments of PanacheQL queries (subset of JPQL) and contextualizes the rest. +That makes for very concise but yet readable code. +MongoDB native queries are also supported. + +== Solution + +We recommend that you follow the instructions in the next sections and create the application step by step. +However, you can go right to the completed example. + +Clone the Git repository: `git clone {quickstarts-clone-url}`, or download an {quickstarts-archive-url}[archive]. + +The solution is located in the `using-mongodb-panache` {quickstarts-tree-url}/using-mongodb-panache[directory]. + +== Creating the Maven project + +First, we need a new project. Create a new project with the following command: + +[source,shell,subs=attributes+] +---- +mvn io.quarkus:quarkus-maven-plugin:{quarkus-version}:create \ + -DprojectGroupId=org.acme \ + -DprojectArtifactId=using-mongodb-panache \ + -DclassName="org.acme.mongodb.panache.FruitResource" \ + -Dpath="/fruits" \ + -Dextensions="resteasy-jsonb,mongodb-panache" +---- + +This command generates a Maven structure importing the RESTEasy/JAX-RS, JSON-B and MongoDB with Panache extensions. +After this, the `quarkus-mongodb-panache` extension has been added to your `pom.xml`. + +If you don't want to generate a new project you can add the dependency in your `pom.xml`: + +[source,xml] +---- + + + io.quarkus + quarkus-mongodb-panache + + +---- + +== Setting up and configuring MongoDB with Panache + +To get started: + +* add your settings in `{config-file}` +* Make your entities extend `PanacheMongoEntity`, you can use the `@MongoEntity` annotation to specify the name of the database and the name of the collection (it will default to the name of your entity). +* place your entity logic in static methods in your entities + +Then add the relevant configuration properties in `{config-file}`. + +[source,properties] +---- +# configure the MongoDB client for a replica set of two nodes +quarkus.mongodb.connection-string = mongodb://mongo1:27017,mongo2:27017 +# mandatory if you don't specify the name of the database using @MongoEntity +quarkus.mongodb.database = person +---- + +The `quarkus.mongodb.database` property will be used by MongoDB with Panache to determine the name of the database where your entities will be persisted. + +For advanced configuration of the MongoDB client, you can follow the link:mongo-guide.html#configuring-the-mongodb-database[Configuring the MongoDB database guide]. + +== Defining your entity + +To define a Panache entity, simply extend `PanacheMongoEntity` and add your columns as public fields. +You can add the `@MongoEntity` annotation to your entity if you need to customize the name of the collection and/or the database. + +[source,java] +---- +@MongoEntity(collection="ThePerson") +public class Person extends PanacheMongoEntity { + public String name; + + // will be persisted as a 'birth' field in MongoDB + @BsonProperty("birth") + public LocalDate birthDate; + + public Status status; +} +---- + +NOTE: annotating with `@MongoEntity` is optional, it allows you to configure the name of the collection and the name of the database. Here the entity will be stored in the `ThePerson` collection instead of the default `Person` collection. + +MongoDB with Panache uses the link:https://mongodb.github.io/mongo-java-driver/3.10/bson/pojos/[PojoCodecProvider] to map your entities to a MongoDB `Document`. + +You will be allowed to use the following annotations to customize this mapping: + +- `@BsonId`: allows you to customize the ID field, see <>. +- `@BsonProperty`: customize the serialized name of the field. +- `@BsonIgnore`: ignore a field during the serialization. + +If you need to write accessors, you can: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + public String name; + public LocalDate birth; + public Status status; + + // return name as uppercase in the model + public String getName(){ + return name.toUpperCase(); + } + + // store all names in lowercase in the DB + public void setName(String name){ + this.name = name.toLowerCase(); + } +} +---- + +And thanks to our field access rewrite, when your users read `person.name` they will actually call your `getName()` accessor, and similarly for field writes and the setter. +This allows for proper encapsulation at runtime as all fields calls will be replaced by the corresponding getter/setter calls. + +== Most useful operations + +Once you have written your entity, here are the most common operations you will be able to do: + +[source,java] +---- +// creating a person +Person person = new Person(); +person.name = "Loïc"; +person.birth = LocalDate.of(1910, Month.FEBRUARY, 1); +person.status = Status.Alive; + +// persist it +person.persist(); + +person.status = Status.Dead; + +// Your must call update() in order to send your entity modifications to MongoDB +person.update(); + +// delete it +person.delete(); + +// getting a list of all Person entities +List allPersons = Person.listAll(); + +// finding a specific person by ID +person = Person.findById(personId); + +// finding all living persons +List livingPersons = Person.list("status", Status.Alive); + +// counting all persons +long countAll = Person.count(); + +// counting all living persons +long countAlive = Person.count("status", Status.Alive); + +// delete all living persons +Person.delete("status", Status.Alive); + +// delete all persons +Person.deleteAll(); +---- + +All `list` methods have equivalent `stream` versions. + +[source,java] +---- +Stream persons = Person.streamAll(); +List namesButEmmanuels = persons + .map(p -> p.name.toLowerCase() ) + .filter( n -> ! "emmanuel".equals(n) ) + .collect(Collectors.toList()); +---- + +NOTE: A `persistOrUpdate()` method exist that persist or update an entity in the database, it uses the __upsert__ capability of MongoDB to do it in a single query. + +== Paging + +You should only use `list` and `stream` methods if your collection contains small enough data sets. For larger data +sets you can use the `find` method equivalents, which return a `PanacheQuery` on which you can do paging: + +[source,java] +---- +// create a query for all living persons +PanacheQuery livingPersons = Person.find("status", Status.Alive); + +// make it use pages of 25 entries at a time +livingPersons.page(Page.ofSize(25)); + +// get the first page +List firstPage = livingPersons.list(); + +// get the second page +List secondPage = livingPersons.nextPage().list(); + +// get page 7 +List page7 = livingPersons.page(Page.of(7, 25)).list(); + +// get the number of pages +int numberOfPages = livingPersons.pageCount(); + +// get the total number of entities returned by this query without paging +int count = livingPersons.count(); + +// and you can chain methods of course +return Person.find("status", Status.Alive) + .page(Page.ofSize(25)) + .nextPage() + .stream() +---- + +The `PanacheQuery` type has many other methods to deal with paging and returning streams. + +== Sorting + +All methods accepting a query string also accept an optional `Sort` parameter, which allows you to abstract your sorting: + +[source,java] +---- +List persons = Person.list(Sort.by("name").and("birth")); + +// and with more restrictions +List persons = Person.list("status", Sort.by("name").and("birth"), Status.Alive); +---- + +The `Sort` class has plenty of methods for adding columns and specifying sort direction. + +== Adding entity methods + +In general, we recommend not adding custom queries for your entities outside of the entities themselves, +to keep all model queries close to the models they operate on. So we recommend adding them as static methods +in your entity class: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + public String name; + public LocalDate birth; + public Status status; + + public static Person findByName(String name){ + return find("name", name).firstResult(); + } + + public static List findAlive(){ + return list("status", Status.Alive); + } + + public static void deleteLoics(){ + delete("name", "Loïc"); + } +} +---- + +== Simplified queries + +Normally, MongoDB queries are of this form: `{'firstname': 'John', 'lastname':'Doe'}`, this is what we call MongoDB native queries. + +You can use them if you want, but we also support what we call **PanacheQL** that can be seen as a subset of link:https://docs.oracle.com/javaee/7/tutorial/persistence-querylanguage.htm#BNBTG[JPQL] (or link:https://docs.jboss.org/hibernate/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#hql[HQL]) and allows you to easily express a query. +MongoDB with Panache will then map it to a MongoDB native query. + +If your query does not start with `{`, we will consider it a PanacheQL query: + +- `` (and single parameter) which will expand to `{'singleColumnName': '?'}` +- `` will expand to `{}` where we will map the PanacheQL query to MongoDB native query form. We support the following operators that will be mapped to the corresponding MongoDB operators: 'and', 'or' ( mixing 'and' and 'or' is not currently supported), '=', '>', '>=', '<', '<=', '!=', 'is null', 'is not null', and 'like' that is mapped to the MongoDB `$regex` operator. + +Here are some query examples: + +- `firstname = ?1 and status = ?2` will be mapped to `{'firstname': ?1, 'status': ?2}` +- `amount > ?1 and firstname != ?2` will be mapped to `{'amount': {'$gt': ?1}, 'firstname': {'$ne': ?2}}` +- `lastname like ?1` will be mapped to `{'lastname': {'$regex': ?1}}`. Be careful that this will be link:https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex[MongoDB regex] support and not SQL like pattern. +- `lastname is not null` will be mapped to `{'lastname':{'$exists': true}}` + +We also handle some basic date type transformations: all fields of type `Date`, `LocalDate` or `LocalDateTime` will be mapped to the link:https://docs.mongodb.com/manual/reference/bson-types/#document-bson-type-date[BSON Date] using the `ISODate` type (UTC datetime). + +MongoDB with Panache also supports extended MongoDB queries by providing a `Document` query, this is supported by the find/list/stream/count/delete methods. + +== Query parameters + +You can pass query parameters, for both native and PanacheQL queries, by index (1-based) as shown below: + +[source,java] +---- +Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive); +Person.find("{'name': ?1, 'status': ?2}", "Loïc", Status.Alive); +---- + +Or by name using a `Map`: + +[source,java] +---- +Map params = new HashMap<>(); +params.put("name", "Loïc"); +params.put("status", Status.Alive); +Person.find("name = :name and status = :status", params); +Person.find("{'name': :name, 'status', :status}", params); +---- + +Or using the convenience class `Parameters` to either build a `Map` or just use as-is: + +[source,java] +---- +// generate a Map +Person.find("name = :name and status = :status", + Parameters.with("name", "Loïc").and("status", Status.Alive).map()); + +// use it as-is +Person.find("{'name': :name, 'status': :status}", + Parameters.with("name", "Loïc").and("status", Status.Alive)); +---- + +Every query operation accepts passing parameters by index (`Object...`), or by name (`Map` or `Parameters`). + +When you use query parameters, be careful that PanacheQL queries will refer to the Object parameters name but native queries will refer to MongoDB field names. + +Imagine the following entity: + +[source,java] +---- +public class Person extends PanacheMongoEntity { + @BsonProperty("lastname") + public String name; + public LocalDate birth; + public Status status; + + public static Person findByNameWithPanacheQLQuery(String name){ + return find("name", name).firstResult(); + } + + public static Person findByNameWithNativeQuery(String name){ + return find("{'lastname': ?1}", name).firstResult(); + } +} +---- + +Both `findByNameWithPanacheQLQuery()` and `findByNameWithNativeQuery()` methods will return the same result but query written in PanacheQL +will use the entity field name: `name`, and native query will use the MongoDB field name: `lastname`. + +== The DAO/Repository option + +Look, we get it: you have a love/hate relationship with DAOs/Repositories but you can't live without them. We don't judge, we know life is tough and we've got you covered. + +If you want to have Repositories, you can get the exact same convenient methods injected in your Repository by making it +implement `PanacheMongoRepository`: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheMongoRepository { + + // put your custom logic here as instance methods + + public Person findByName(String name){ + return find("name", name).firstResult(); + } + + public List findAlive(){ + return list("status", Status.Alive); + } + + public void deleteLoics(){ + delete("name", "Loïc"); + } +} +---- + +Absolutely all the operations that are defined on `PanacheMongoEntityBase` are available on your DAO, so using it +is exactly the same except you need to inject it: + +[source,java] +---- +@Inject +PersonRepository personRepository; + +@GET +public long count(){ + return personRepository.count(); +} +---- + +So if Repositories are your thing, you can keep doing them. Even with repositories, you can keep your entities as +subclasses of `PanacheMongoEntity` in order to get the ID and public fields working, but you can even skip that and +go back to specifying your ID and using getters and setters if that's your thing. We're not judging. + +== Transactions + +WARNING: MongoDB offers ACID transactions since version 4.0. MongoDB with Panache doesn't provide support for them. + +== Custom IDs + +IDs are often a touchy subject. In MongoDB, they are usually auto-generated by the database with an `ObjectId` type. +In MongoDB with Panache the ID are defined by a field named `id` of the `org.bson.types.ObjectId` type, +but if you want ot customize them, once again we have you covered. + +You can specify your own ID strategy by extending `PanacheMongoEntityBase` instead of `PanacheMongoEntity`. Then +you just declare whatever ID you want as a public field by annotating it by @BsonId: + +[source,java] +---- +@MongoEntity +public class Person extends PanacheMongoEntityBase { + + @BsonId + public Integer myId; + + //... +} +---- + +If you're using repositories, then you will want to extend `PanacheMongoRepositoryBase` instead of `PanacheMongoRepository` +and specify your ID type as an extra type parameter: + +[source,java] +---- +@ApplicationScoped +public class PersonRepository implements PanacheMongoRepositoryBase { + //... +} +---- + +[NOTE] +==== +When using `ObjectId`, MongoDB will automatically provide a value for you, but if you use a custom field type, +you need to provide the value by yourself. +==== + +`ObjectId` can be difficult to use if you want to expose its value in your REST service. +So we created JSON-B and Jackson providers to serialize/deserialize them as a `String` which are are automatically registered if your project depends on one of the RESTEasy with JSON-B or RESTEasy with Jackson extensions. + +== How and why we simplify MongoDB API + +When it comes to writing MongoDB entities, there are a number of annoying things that users have grown used to +reluctantly deal with, such as: + +- Duplicating ID logic: most entities need an ID, most people don't care how it's set, because it's not really +relevant to your model. +- Dumb getters and setters: since Java lacks support for properties in the language, we have to create fields, +then generate getters and setters for those fields, even if they don't actually do anything more than read/write +the fields. +- Traditional EE patterns advise to split entity definition (the model) from the operations you can do on them +(DAOs, Repositories), but really that requires an unnatural split between the state and its operations even though +we would never do something like that for regular objects in the Object Oriented architecture, where state and methods are in the same class. Moreover, this requires two classes per entity, and requires injection of the DAO or Repository where you need to do entity operations, which breaks your edit flow and requires you to get out of the code you're writing to set up an injection point before coming back to use it. +- MongoDB queries are super powerful, but overly verbose for common operations, requiring you to write queries even +when you don't need all the parts. +- MongoDB queries are JSON based, so you will need some String manipulation or using the `Document` type and it will need a lot of boilerplate code. + +With Panache, we took an opinionated approach to tackle all these problems: + +- Make your entities extend `PanacheMongoEntity`: it has an ID field that is auto-generated. If you require +a custom ID strategy, you can extend `PanacheMongoEntityBase` instead and handle the ID yourself. +- Use public fields. Get rid of dumb getter and setters. Under the hood, we will generate all getters and setters +that are missing, and rewrite every access to these fields to use the accessor methods. This way you can still +write _useful_ accessors when you need them, which will be used even though your entity users still use field accesses. +- Don't use DAOs or Repositories: put all your entity logic in static methods in your entity class. Your entity superclass comes with lots of super useful static methods and you can add your own in your entity class. Users can just start using your entity `Person` by typing `Person.` and getting completion for all the operations in a single place. +- Don't write parts of the query that you don't need: write `Person.find("order by name")` or +`Person.find("name = ?1 and status = ?2", "Loïc", Status.Alive)` or even better `Person.find("name", "Loïc")`. + +That's all there is to it: with Panache, MongoDB has never looked so trim and neat. + +== Defining entities in external projects or jars + +MongoDB with Panache relies on compile-time bytecode enhancements to your entities. +If you define your entities in the same project where you build your Quarkus application, everything will work fine. +If the entities come from external projects or jars, you can make sure that your jar is treated like a Quarkus application library +by indexing it via Jandex, see link:cdi-reference.html#how-to-generate-a-jandex-index[How to Generate a Jandex Index] in the CDI guide. +This will allow Quarkus to index and enhance your entities as if they were inside the current project. + diff --git a/extensions/panache/mongodb-panache/deployment/pom.xml b/extensions/panache/mongodb-panache/deployment/pom.xml new file mode 100644 index 0000000000000..e3b43b025fd7b --- /dev/null +++ b/extensions/panache/mongodb-panache/deployment/pom.xml @@ -0,0 +1,57 @@ + + + + quarkus-mongodb-panache-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-mongodb-panache-deployment + Quarkus - MongoDB with Panache - Deployment + + + + io.quarkus + quarkus-core-deployment + + + io.quarkus + quarkus-panache-common-deployment + + + io.quarkus + quarkus-resteasy-common-spi + + + io.quarkus + quarkus-mongodb-client-deployment + + + io.quarkus + quarkus-mongodb-panache + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${project.version} + + + + + + + + + \ No newline at end of file diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java new file mode 100644 index 0000000000000..27d5b0c216769 --- /dev/null +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoEntityEnhancer.java @@ -0,0 +1,89 @@ +package io.quarkus.mongodb.panache.deployment; + +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.Map; + +import org.bson.codecs.pojo.annotations.BsonIgnore; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.gizmo.DescriptorUtils; +import io.quarkus.mongodb.panache.runtime.MongoOperations; +import io.quarkus.panache.common.deployment.EntityField; +import io.quarkus.panache.common.deployment.EntityModel; +import io.quarkus.panache.common.deployment.MetamodelInfo; +import io.quarkus.panache.common.deployment.PanacheEntityEnhancer; + +public class PanacheMongoEntityEnhancer extends PanacheEntityEnhancer>> { + public final static String MONGO_OPERATIONS_NAME = MongoOperations.class.getName(); + public final static String MONGO_OPERATIONS_BINARY_NAME = MONGO_OPERATIONS_NAME.replace('.', '/'); + + private static final DotName DOTNAME_BSON_IGNORE = DotName.createSimple(BsonIgnore.class.getName()); + + final Map entities = new HashMap<>(); + + public PanacheMongoEntityEnhancer(IndexView index) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_ENTITY_BASE); + modelInfo = new MetamodelInfo<>(); + } + + @Override + public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { + return new PanacheMongoEntityClassVisitor(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo); + } + + static class PanacheMongoEntityClassVisitor extends PanacheEntityClassVisitor { + + public PanacheMongoEntityClassVisitor(String className, ClassVisitor outputClassVisitor, + MetamodelInfo> modelInfo, ClassInfo panacheEntityBaseClassInfo) { + super(className, outputClassVisitor, modelInfo, panacheEntityBaseClassInfo); + } + + @Override + protected void injectModel(MethodVisitor mv) { + mv.visitLdcInsn(thisClass); + } + + @Override + protected String getModelDescriptor() { + return "Ljava/lang/Class;"; + } + + @Override + protected String getPanacheOperationsBinaryName() { + return MONGO_OPERATIONS_BINARY_NAME; + } + + @Override + protected void generateAccessorSetField(MethodVisitor mv, EntityField field) { + mv.visitFieldInsn(Opcodes.PUTFIELD, thisClass.getInternalName(), field.name, field.descriptor); + } + + @Override + protected void generateAccessorGetField(MethodVisitor mv, EntityField field) { + mv.visitFieldInsn(Opcodes.GETFIELD, thisClass.getInternalName(), field.name, field.descriptor); + } + + @Override + public void visitEnd() { + super.visitEnd(); + } + } + + public void collectFields(ClassInfo classInfo) { + EntityModel entityModel = new EntityModel<>(classInfo); + for (FieldInfo fieldInfo : classInfo.fields()) { + String name = fieldInfo.name(); + if (Modifier.isPublic(fieldInfo.flags()) && !fieldInfo.hasAnnotation(DOTNAME_BSON_IGNORE)) { + entityModel.addField(new EntityField(name, DescriptorUtils.typeToString(fieldInfo.type()))); + } + } + modelInfo.addEntityModel(entityModel); + } +} diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java new file mode 100644 index 0000000000000..988a942d7420b --- /dev/null +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheMongoRepositoryEnhancer.java @@ -0,0 +1,85 @@ +package io.quarkus.mongodb.panache.deployment; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.IndexView; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import io.quarkus.mongodb.panache.PanacheMongoRepository; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.panache.common.deployment.PanacheRepositoryEnhancer; + +public class PanacheMongoRepositoryEnhancer extends PanacheRepositoryEnhancer { + public final static DotName PANACHE_REPOSITORY_BASE_NAME = DotName.createSimple(PanacheMongoRepositoryBase.class.getName()); + + public final static DotName PANACHE_REPOSITORY_NAME = DotName.createSimple(PanacheMongoRepository.class.getName()); + + public PanacheMongoRepositoryEnhancer(IndexView index) { + super(index, PanacheResourceProcessor.DOTNAME_PANACHE_REPOSITORY_BASE); + } + + @Override + public ClassVisitor apply(String className, ClassVisitor outputClassVisitor) { + return new PanacheMongoRepositoryClassVisitor(className, outputClassVisitor, panacheRepositoryBaseClassInfo, + this.indexView); + } + + static class PanacheMongoRepositoryClassVisitor extends PanacheRepositoryClassVisitor { + + public PanacheMongoRepositoryClassVisitor(String className, ClassVisitor outputClassVisitor, + ClassInfo panacheRepositoryBaseClassInfo, IndexView indexView) { + super(className, outputClassVisitor, panacheRepositoryBaseClassInfo, indexView); + } + + @Override + protected DotName getPanacheRepositoryDotName() { + return PANACHE_REPOSITORY_NAME; + } + + @Override + protected DotName getPanacheRepositoryBaseDotName() { + return PANACHE_REPOSITORY_BASE_NAME; + } + + @Override + protected String getPanacheOperationsBinaryName() { + return PanacheMongoEntityEnhancer.MONGO_OPERATIONS_BINARY_NAME; + } + + @Override + public void visitEnd() { + // Bridge for findById + MethodVisitor mv = super.visitMethod(Opcodes.ACC_PUBLIC | Opcodes.ACC_SYNTHETIC | Opcodes.ACC_BRIDGE, + "findById", + "(Ljava/lang/Object;)Ljava/lang/Object;", + null, + null); + mv.visitParameter("id", 0); + mv.visitCode(); + mv.visitIntInsn(Opcodes.ALOAD, 0); + mv.visitIntInsn(Opcodes.ALOAD, 1); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + daoBinaryName, + "findById", + "(Ljava/lang/Object;)" + entitySignature, false); + mv.visitInsn(Opcodes.ARETURN); + mv.visitMaxs(0, 0); + mv.visitEnd(); + + super.visitEnd(); + } + + @Override + protected void injectModel(MethodVisitor mv) { + // inject Class + mv.visitLdcInsn(entityType); + } + + @Override + protected String getModelDescriptor() { + return "Ljava/lang/Class;"; + } + } +} diff --git a/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java new file mode 100644 index 0000000000000..2dbb00bec2809 --- /dev/null +++ b/extensions/panache/mongodb-panache/deployment/src/main/java/io/quarkus/mongodb/panache/deployment/PanacheResourceProcessor.java @@ -0,0 +1,121 @@ +package io.quarkus.mongodb.panache.deployment; + +import java.util.HashSet; +import java.util.Set; + +import org.bson.types.ObjectId; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.Type; + +import io.quarkus.arc.deployment.AdditionalBeanBuildItem; +import io.quarkus.deployment.Capabilities; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.ApplicationIndexBuildItem; +import io.quarkus.deployment.builditem.BytecodeTransformerBuildItem; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; +import io.quarkus.deployment.builditem.substrate.ReflectiveHierarchyBuildItem; +import io.quarkus.deployment.index.IndexingUtil; +import io.quarkus.mongodb.panache.PanacheMongoEntity; +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; +import io.quarkus.mongodb.panache.PanacheMongoRepository; +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; +import io.quarkus.mongodb.panache.jackson.ObjectMapperProducer; +import io.quarkus.mongodb.panache.jsonb.PanacheMongoJsonbContextResolver; +import io.quarkus.panache.common.deployment.PanacheFieldAccessEnhancer; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; + +public class PanacheResourceProcessor { + static final DotName DOTNAME_PANACHE_REPOSITORY_BASE = DotName.createSimple(PanacheMongoRepositoryBase.class.getName()); + private static final DotName DOTNAME_PANACHE_REPOSITORY = DotName.createSimple(PanacheMongoRepository.class.getName()); + static final DotName DOTNAME_PANACHE_ENTITY_BASE = DotName.createSimple(PanacheMongoEntityBase.class.getName()); + private static final DotName DOTNAME_PANACHE_ENTITY = DotName.createSimple(PanacheMongoEntity.class.getName()); + + private static final DotName DOTNAME_OBJECT_ID = DotName.createSimple(ObjectId.class.getName()); + + @BuildStep(providesCapabilities = "io.quarkus.mongodb.panache") + FeatureBuildItem featureBuildItem() { + return new FeatureBuildItem(FeatureBuildItem.MONGODB_PANACHE); + } + + @BuildStep + AdditionalBeanBuildItem registerJacksonSerDer(Capabilities capabilities) { + if (capabilities.isCapabilityPresent("io.quarkus.resteasy.jackson")) { + return AdditionalBeanBuildItem.unremovableOf(ObjectMapperProducer.class); + } + return null; + } + + @BuildStep + ResteasyJaxrsProviderBuildItem registerJsonbSerDer(Capabilities capabilities) { + if (capabilities.isCapabilityPresent("io.quarkus.resteasy.jsonb")) { + return new ResteasyJaxrsProviderBuildItem(PanacheMongoJsonbContextResolver.class.getName()); + } + return null; + } + + @BuildStep + ReflectiveHierarchyBuildItem registerForReflection(CombinedIndexBuildItem index) { + Indexer indexer = new Indexer(); + Set additionalIndex = new HashSet<>(); + IndexingUtil.indexClass(ObjectId.class.getName(), indexer, index.getIndex(), additionalIndex, + PanacheResourceProcessor.class.getClassLoader()); + CompositeIndex compositeIndex = CompositeIndex.create(index.getIndex(), indexer.complete()); + Type type = Type.create(DOTNAME_OBJECT_ID, Type.Kind.CLASS); + return new ReflectiveHierarchyBuildItem(type, compositeIndex); + } + + @BuildStep + void build(CombinedIndexBuildItem index, + ApplicationIndexBuildItem applicationIndex, + BuildProducer transformers) throws Exception { + + PanacheMongoRepositoryEnhancer daoEnhancer = new PanacheMongoRepositoryEnhancer(index.getIndex()); + Set daoClasses = new HashSet<>(); + for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_PANACHE_REPOSITORY_BASE)) { + // Skip PanacheRepository + if (classInfo.name().equals(DOTNAME_PANACHE_REPOSITORY)) + continue; + daoClasses.add(classInfo.name().toString()); + } + for (ClassInfo classInfo : index.getIndex().getAllKnownImplementors(DOTNAME_PANACHE_REPOSITORY)) { + daoClasses.add(classInfo.name().toString()); + } + for (String daoClass : daoClasses) { + transformers.produce(new BytecodeTransformerBuildItem(daoClass, daoEnhancer)); + } + + PanacheMongoEntityEnhancer modelEnhancer = new PanacheMongoEntityEnhancer(index.getIndex()); + Set modelClasses = new HashSet<>(); + // Note that we do this in two passes because for some reason Jandex does not give us subtypes + // of PanacheMongoEntity if we ask for subtypes of PanacheMongoEntityBase + for (ClassInfo classInfo : index.getIndex().getAllKnownSubclasses(DOTNAME_PANACHE_ENTITY_BASE)) { + if (classInfo.name().equals(DOTNAME_PANACHE_ENTITY)) + continue; + if (modelClasses.add(classInfo.name().toString())) + modelEnhancer.collectFields(classInfo); + } + for (ClassInfo classInfo : index.getIndex().getAllKnownSubclasses(DOTNAME_PANACHE_ENTITY)) { + if (modelClasses.add(classInfo.name().toString())) + modelEnhancer.collectFields(classInfo); + } + for (String modelClass : modelClasses) { + transformers.produce(new BytecodeTransformerBuildItem(modelClass, modelEnhancer)); + } + + if (!modelEnhancer.entities.isEmpty()) { + PanacheFieldAccessEnhancer panacheFieldAccessEnhancer = new PanacheFieldAccessEnhancer( + modelEnhancer.getModelInfo()); + for (ClassInfo classInfo : applicationIndex.getIndex().getKnownClasses()) { + String className = classInfo.name().toString(); + if (!modelClasses.contains(className)) { + transformers.produce(new BytecodeTransformerBuildItem(className, panacheFieldAccessEnhancer)); + } + } + } + } +} diff --git a/extensions/panache/mongodb-panache/pom.xml b/extensions/panache/mongodb-panache/pom.xml new file mode 100644 index 0000000000000..c10a0f718e2fb --- /dev/null +++ b/extensions/panache/mongodb-panache/pom.xml @@ -0,0 +1,22 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + 4.0.0 + + quarkus-mongodb-panache-parent + Quarkus - MongoDB with Panache - Parent + pom + + runtime + deployment + + + + \ No newline at end of file diff --git a/extensions/panache/mongodb-panache/runtime/pom.xml b/extensions/panache/mongodb-panache/runtime/pom.xml new file mode 100644 index 0000000000000..7e618e4a907ed --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/pom.xml @@ -0,0 +1,95 @@ + + + + quarkus-mongodb-panache-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-mongodb-panache + Quarkus - MongoDB with Panache - Runtime + + + + io.quarkus + quarkus-core + + + io.quarkus + quarkus-panache-common + + + io.quarkus + quarkus-panacheql + + + io.quarkus + quarkus-mongodb-client + + + + + io.quarkus + quarkus-resteasy-jsonb + true + + + + + io.quarkus + quarkus-resteasy-jackson + true + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + io.quarkus + quarkus-bootstrap-maven-plugin + + + + org.jboss.jandex + jandex-maven-plugin + + + make-index + + jandex + + + + + + + ${project.build.directory}/classes + + + **/PanacheMongoJsonbContextResolver.class + + **/ObjectMapperProducer.class + + + + + + + + + + \ No newline at end of file diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/MongoEntity.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/MongoEntity.java new file mode 100755 index 0000000000000..5fef718d89ab0 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/MongoEntity.java @@ -0,0 +1,21 @@ +package io.quarkus.mongodb.panache; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * This annotation can be used to specify some configuration of the mapping of an entity to MongoDB. + */ +@Retention(RetentionPolicy.RUNTIME) +public @interface MongoEntity { + /** + * The name of the collection (if not set the name of the entity class will be used) + */ + String collection() default ""; + + /** + * the name of the database (if not set the default from the property + * quarkus.mongodb.database will be used. + */ + String database() default ""; +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java new file mode 100755 index 0000000000000..c3ffb4437c151 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntity.java @@ -0,0 +1,30 @@ +package io.quarkus.mongodb.panache; + +import org.bson.types.ObjectId; + +/** + * Represents an entity with a generated ID field {@link #id} of type {@link ObjectId}. If your + * Mongo entities extend this class they gain the ID field and auto-generated accessors + * to all their public fields, as well as all the useful methods from {@link PanacheMongoEntityBase}. + * + * If you want a custom ID type or strategy, you can directly extend {@link PanacheMongoEntityBase} + * instead, and write your own ID field. You will still get auto-generated accessors and + * all the useful methods. + * + * @see PanacheMongoEntityBase + */ +public abstract class PanacheMongoEntity extends PanacheMongoEntityBase { + + /** + * The auto-generated ID field. + * This field is set by Mongo when this entity is persisted. + * + * @see #persist() + */ + public ObjectId id; + + @Override + public String toString() { + return this.getClass().getSimpleName() + "<" + id + ">"; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java new file mode 100755 index 0000000000000..c4ed06d9573cf --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoEntityBase.java @@ -0,0 +1,870 @@ +package io.quarkus.mongodb.panache; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.bson.Document; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +import io.quarkus.mongodb.panache.runtime.MongoOperations; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.impl.GenerateBridge; + +/** + * Represents an entity. If your Mongo entities extend this class they gain auto-generated accessors + * to all their public fields, as well as a lot of useful + * methods. Unless you have a custom ID strategy, you should not extend this class directly but extend + * {@link PanacheMongoEntity} instead. + * + * @see PanacheMongoEntity + */ +public abstract class PanacheMongoEntityBase { + + // Operations + + /** + * Persist this entity in the database. + * This will set it's ID field if not already set. + * + * @see #persist(Iterable) + * @see #persist(Stream) + * @see #persist(Object, Object...) + */ + public void persist() { + MongoOperations.persist(this); + } + + /** + * Update this entity in the database. + * + * @see #update(Iterable) + * @see #update(Stream) + * @see #update(Object, Object...) + */ + public void update() { + MongoOperations.update(this); + } + + /** + * Persist this entity in the database or update it if it already exist. + * + * @see #persistOrUpdate(Iterable) + * @see #persistOrUpdate(Stream) + * @see #persistOrUpdate(Object, Object...) + */ + public void persistOrUpdate() { + MongoOperations.persistOrUpdate(this); + } + + /** + * Delete this entity from the database, if it is already persisted. + * + * @see #delete(String, Object...) + * @see #delete(String, Map) + * @see #delete(String, Parameters) + * @see #deleteAll() + */ + public void delete() { + MongoOperations.delete(this); + } + + // Queries + + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return the entity found, or null if not found. + */ + @GenerateBridge(targetReturnTypeErased = true) + public static T findById(Object id) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Object...) + * @see #find(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Object...) + * @see #stream(String, Object...) + */ + @GenerateBridge + public static PanacheQuery find(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Object...) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Object...) + * @see #stream(String, Sort, Object...) + */ + @GenerateBridge + public static PanacheQuery find(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Map) + * @see #find(String, Object...) + * @see #find(String, Parameters) + * @see #list(String, Map) + * @see #stream(String, Map) + */ + @GenerateBridge + public static PanacheQuery find(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Map) + * @see #find(String, Sort, Object...) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Map) + * @see #stream(String, Sort, Map) + */ + @GenerateBridge + public static PanacheQuery find(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Parameters) + * @see #find(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Parameters) + * @see #stream(String, Parameters) + */ + @GenerateBridge + public static PanacheQuery find(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static PanacheQuery find(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static PanacheQuery find(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static PanacheQuery find(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * + * @return a new {@link PanacheQuery} instance to find all entities of this type. + * @see #findAll(Sort) + * @see #listAll() + * @see #streamAll() + */ + @GenerateBridge + public static PanacheQuery findAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * + * @param sort the sort order to use + * @return a new {@link PanacheQuery} instance to find all entities of this type. + * @see #findAll() + * @see #listAll(Sort) + * @see #streamAll(Sort) + */ + @GenerateBridge + public static PanacheQuery findAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with optional indexed parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Object...) + * @see #list(String, Map) + * @see #list(String, Parameters) + * @see #find(String, Object...) + * @see #stream(String, Object...) + */ + @GenerateBridge + public static List list(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with optional indexed parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Object...) + * @see #list(String, Sort, Map) + * @see #list(String, Sort, Parameters) + * @see #find(String, Sort, Object...) + * @see #stream(String, Sort, Object...) + */ + @GenerateBridge + public static List list(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Map) + * @see #list(String, Object...) + * @see #list(String, Parameters) + * @see #find(String, Map) + * @see #stream(String, Map) + */ + @GenerateBridge + public static List list(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Map) + * @see #list(String, Sort, Object...) + * @see #list(String, Sort, Parameters) + * @see #find(String, Sort, Map) + * @see #stream(String, Sort, Map) + */ + @GenerateBridge + public static List list(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Parameters) + * @see #list(String, Object...) + * @see #list(String, Map) + * @see #find(String, Parameters) + * @see #stream(String, Parameters) + */ + @GenerateBridge + public static List list(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Parameters) + * @see #list(String, Sort, Object...) + * @see #list(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static List list(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * This method is a shortcut for find(query).list(). + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static List list(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * This method is a shortcut for find(query, sort).list(). + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static List list(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * This method is a shortcut for findAll().list(). + * + * @return a {@link List} containing all results, without paging + * @see #listAll(Sort) + * @see #findAll() + * @see #streamAll() + */ + @GenerateBridge + public static List listAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * This method is a shortcut for findAll(sort).list(). + * + * @param sort the sort order to use + * @return a {@link List} containing all results, without paging + * @see #listAll() + * @see #findAll(Sort) + * @see #streamAll(Sort) + */ + @GenerateBridge + public static List listAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with optional indexed parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Object...) + * @see #stream(String, Map) + * @see #stream(String, Parameters) + * @see #find(String, Object...) + * @see #list(String, Object...) + */ + @GenerateBridge + public static Stream stream(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with optional indexed parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Object...) + * @see #stream(String, Sort, Map) + * @see #stream(String, Sort, Parameters) + * @see #find(String, Sort, Object...) + * @see #list(String, Sort, Object...) + */ + @GenerateBridge + public static Stream stream(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Map) + * @see #stream(String, Object...) + * @see #stream(String, Parameters) + * @see #find(String, Map) + * @see #list(String, Map) + */ + @GenerateBridge + public static Stream stream(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Map) + * @see #stream(String, Sort, Object...) + * @see #stream(String, Sort, Parameters) + * @see #find(String, Sort, Map) + * @see #list(String, Sort, Map) + */ + @GenerateBridge + public static Stream stream(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Parameters) + * @see #stream(String, Object...) + * @see #stream(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Parameters) + */ + @GenerateBridge + public static Stream stream(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Parameters) + * @see #stream(String, Sort, Object...) + * @see #stream(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + */ + @GenerateBridge + public static Stream stream(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * This method is a shortcut for find(query).stream(). + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static Stream stream(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * This method is a shortcut for find(query, sort).stream(). + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public static Stream stream(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * This method is a shortcut for findAll().stream(). + * + * @return a {@link Stream} containing all results, without paging + * @see #streamAll(Sort) + * @see #findAll() + * @see #listAll() + */ + @GenerateBridge + public static Stream streamAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * This method is a shortcut for findAll(sort).stream(). + * + * @param sort the sort order to use + * @return a {@link Stream} containing all results, without paging + * @see #streamAll() + * @see #findAll(Sort) + * @see #listAll(Sort) + */ + @GenerateBridge + public static Stream streamAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity in the database. + * + * @return the number of this type of entity in the database. + * @see #count(String, Object...) + * @see #count(String, Map) + * @see #count(String, Parameters) + */ + @GenerateBridge + public static long count() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Map) + * @see #count(String, Parameters) + */ + @GenerateBridge + public static long count(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Parameters) + */ + @GenerateBridge + public static long count(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public static long count(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query + * + * @param query a {@link org.bson.Document} query + * @return he number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public static long count(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type from the database. + * + * @return the number of entities deleted. + * @see #delete(String, Object...) + * @see #delete(String, Map) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public static long deleteAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Map) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public static long delete(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Object...) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public static long delete(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Object...) + * @see #delete(String, Map) + */ + @GenerateBridge + public static long delete(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query + * + * @param query a {@link org.bson.Document} query + * @return he number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public static long delete(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Insert all given entities. + * + * @param entities the entities to insert + * @see #persist() + * @see #persist(Stream) + * @see #persist(Object,Object...) + */ + public static void persist(Iterable entities) { + MongoOperations.persist(entities); + } + + /** + * Insert all given entities. + * + * @param entities the entities to insert + * @see #persist() + * @see #persist(Iterable) + * @see #persist(Object,Object...) + */ + public static void persist(Stream entities) { + MongoOperations.persist(entities); + } + + /** + * Insert all given entities. + * + * @param entities the entities to update + * @see #persist() + * @see #persist(Stream) + * @see #persist(Iterable) + */ + public static void persist(Object firstEntity, Object... entities) { + MongoOperations.persist(firstEntity, entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to update + * @see #update() + * @see #update(Stream) + * @see #update(Object,Object...) + */ + public static void update(Iterable entities) { + MongoOperations.update(entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to insert + * @see #update() + * @see #update(Iterable) + * @see #update(Object,Object...) + */ + public static void update(Stream entities) { + MongoOperations.update(entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to update + * @see #update() + * @see #update(Stream) + * @see #update(Iterable) + */ + public static void update(Object firstEntity, Object... entities) { + MongoOperations.update(firstEntity, entities); + } + + /** + * Persist all given entities or update them if they already exist. + * + * @param entities the entities to update + * @see #persistOrUpdate() + * @see #persistOrUpdate(Stream) + * @see #persistOrUpdate(Object,Object...) + */ + public static void persistOrUpdate(Iterable entities) { + MongoOperations.persistOrUpdate(entities); + } + + /** + * Persist all given entities. + * + * @param entities the entities to insert + * @see #persistOrUpdate() + * @see #persistOrUpdate(Iterable) + * @see #persistOrUpdate(Object,Object...) + */ + public static void persistOrUpdate(Stream entities) { + MongoOperations.persistOrUpdate(entities); + } + + /** + * Persist all given entities. + * + * @param entities the entities to update + * @see #persistOrUpdate() + * @see #persistOrUpdate(Stream) + * @see #persistOrUpdate(Iterable) + */ + public static void persistOrUpdate(Object firstEntity, Object... entities) { + MongoOperations.persistOrUpdate(firstEntity, entities); + } + + /** + * Allow to access the underlying Mongo Collection. + */ + @GenerateBridge + public static MongoCollection mongoCollection() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Allow to access the underlying Mongo Database. + */ + @GenerateBridge + public static MongoDatabase mongoDatabase() { + throw MongoOperations.implementationInjectionMissing(); + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java new file mode 100755 index 0000000000000..8c259489ffd2f --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepository.java @@ -0,0 +1,15 @@ +package io.quarkus.mongodb.panache; + +import org.bson.types.ObjectId; + +/** + * Represents a Repository for a specific type of entity {@code Entity}, with an ID type + * of {@code ObjectId}. Implementing this repository will gain you the exact same useful methods + * that are on {@link PanacheMongoEntityBase}. If you have a custom ID strategy, you should + * implement {@link PanacheMongoRepositoryBase} instead. + * + * @param The type of entity to operate on + */ +public interface PanacheMongoRepository extends PanacheMongoRepositoryBase { + +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java new file mode 100755 index 0000000000000..c99958fd3df43 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheMongoRepositoryBase.java @@ -0,0 +1,875 @@ +package io.quarkus.mongodb.panache; + +import java.util.List; +import java.util.Map; +import java.util.stream.Stream; + +import org.bson.Document; + +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +import io.quarkus.mongodb.panache.runtime.MongoOperations; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; +import io.quarkus.panache.common.impl.GenerateBridge; + +/** + * Represents a Repository for a specific type of entity {@code Entity}, with an ID type + * of {@code Id}. Implementing this repository will gain you the exact same useful methods + * that are on {@link PanacheMongoEntityBase}. Unless you have a custom ID strategy, you should not + * implement this interface directly but implement {@link PanacheMongoRepository} instead. + * + * @param The type of entity to operate on + * @param The ID type of the entity + * @see PanacheMongoRepository + */ +public interface PanacheMongoRepositoryBase { + + // Operations + + /** + * Persist the given entity in the database. + * This will set it's ID field if not already set. + * + * @param entity the entity to insert. + * @see #persist(Iterable) + * @see #persist(Stream) + * @see #persist(Object, Object...) + */ + public default void persist(Entity entity) { + MongoOperations.persist(entity); + } + + /** + * Update the given entity in the database. + * + * @param entity the entity to update. + * @see #update(Iterable) + * @see #update(Stream) + * @see #update(Object, Object...) + */ + public default void update(Entity entity) { + MongoOperations.update(entity); + } + + /** + * Persist the given entity in the database or update it if it already exist. + * + * @param entity the entity to update. + * @see #persistOrUpdate(Iterable) + * @see #persistOrUpdate(Stream) + * @see #persistOrUpdate(Object, Object...) + */ + public default void persistOrUpdate(Entity entity) { + MongoOperations.persistOrUpdate(entity); + } + + /** + * Delete the given entity from the database, if it is already persisted. + * + * @param entity the entity to delete. + * @see #delete(String, Object...) + * @see #delete(String, Map) + * @see #delete(String, Parameters) + * @see #deleteAll() + */ + public default void delete(Entity entity) { + MongoOperations.delete(entity); + } + + // Queries + + /** + * Find an entity of this type by ID. + * + * @param id the ID of the entity to find. + * @return the entity found, or null if not found. + */ + @GenerateBridge(targetReturnTypeErased = true) + public default Entity findById(Id id) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Object...) + * @see #find(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Object...) + * @see #stream(String, Object...) + */ + @GenerateBridge + public default PanacheQuery find(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Object...) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Object...) + * @see #stream(String, Sort, Object...) + */ + @GenerateBridge + public default PanacheQuery find(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Map) + * @see #find(String, Object...) + * @see #find(String, Parameters) + * @see #list(String, Map) + * @see #stream(String, Map) + */ + @GenerateBridge + public default PanacheQuery find(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Map) + * @see #find(String, Sort, Object...) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Map) + * @see #stream(String, Sort, Map) + */ + @GenerateBridge + public default PanacheQuery find(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Sort, Parameters) + * @see #find(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Parameters) + * @see #stream(String, Parameters) + */ + @GenerateBridge + public default PanacheQuery find(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a query and the given sort options, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery find(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery find(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery find(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * + * @return a new {@link PanacheQuery} instance to find all entities of this type. + * @see #findAll(Sort) + * @see #listAll() + * @see #streamAll() + */ + @GenerateBridge + public default PanacheQuery findAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * + * @param sort the sort order to use + * @return a new {@link PanacheQuery} instance to find all entities of this type. + * @see #findAll() + * @see #listAll(Sort) + * @see #streamAll(Sort) + */ + @GenerateBridge + public default PanacheQuery findAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with optional indexed parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Object...) + * @see #list(String, Map) + * @see #list(String, Parameters) + * @see #find(String, Object...) + * @see #stream(String, Object...) + */ + @GenerateBridge + public default List list(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with optional indexed parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Object...) + * @see #list(String, Sort, Map) + * @see #list(String, Sort, Parameters) + * @see #find(String, Sort, Object...) + * @see #stream(String, Sort, Object...) + */ + @GenerateBridge + public default List list(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Map) + * @see #list(String, Object...) + * @see #list(String, Parameters) + * @see #find(String, Map) + * @see #stream(String, Map) + */ + @GenerateBridge + public default List list(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Map) + * @see #list(String, Sort, Object...) + * @see #list(String, Sort, Parameters) + * @see #find(String, Sort, Map) + * @see #stream(String, Sort, Map) + */ + @GenerateBridge + public default List list(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Sort, Parameters) + * @see #list(String, Object...) + * @see #list(String, Map) + * @see #find(String, Parameters) + * @see #stream(String, Parameters) + */ + @GenerateBridge + public default List list(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).list(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a {@link List} containing all results, without paging + * @see #list(String, Parameters) + * @see #list(String, Sort, Object...) + * @see #list(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default List list(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * This method is a shortcut for find(query).list(). + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery list(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * This method is a shortcut for find(query, sort).list(). + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery list(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * This method is a shortcut for findAll().list(). + * + * @return a {@link List} containing all results, without paging + * @see #listAll(Sort) + * @see #findAll() + * @see #streamAll() + */ + @GenerateBridge + public default List listAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * This method is a shortcut for findAll(sort).list(). + * + * @param sort the sort order to use + * @return a {@link List} containing all results, without paging + * @see #listAll() + * @see #findAll(Sort) + * @see #streamAll(Sort) + */ + @GenerateBridge + public default List listAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with optional indexed parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Object...) + * @see #stream(String, Map) + * @see #stream(String, Parameters) + * @see #find(String, Object...) + * @see #list(String, Object...) + */ + @GenerateBridge + public default Stream stream(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with optional indexed parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params optional sequence of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Object...) + * @see #stream(String, Sort, Map) + * @see #stream(String, Sort, Parameters) + * @see #find(String, Sort, Object...) + * @see #list(String, Sort, Object...) + */ + @GenerateBridge + public default Stream stream(String query, Sort sort, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Map) + * @see #stream(String, Object...) + * @see #stream(String, Parameters) + * @see #find(String, Map) + * @see #list(String, Map) + */ + @GenerateBridge + public default Stream stream(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Map} of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Map) + * @see #stream(String, Sort, Object...) + * @see #stream(String, Sort, Parameters) + * @see #find(String, Sort, Map) + * @see #list(String, Sort, Map) + */ + @GenerateBridge + public default Stream stream(String query, Sort sort, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query, with named parameters. + * This method is a shortcut for find(query, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Sort, Parameters) + * @see #stream(String, Object...) + * @see #stream(String, Map) + * @see #find(String, Parameters) + * @see #list(String, Parameters) + */ + @GenerateBridge + public default Stream stream(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities matching a query and the given sort options, with named parameters. + * This method is a shortcut for find(query, sort, params).stream(). + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param sort the sort strategy to use + * @param params {@link Parameters} of indexed parameters + * @return a {@link Stream} containing all results, without paging + * @see #stream(String, Parameters) + * @see #stream(String, Sort, Object...) + * @see #stream(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + */ + @GenerateBridge + public default Stream stream(String query, Sort sort, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a BSON query. + * This method is a shortcut for find(query).stream(). + * + * @param query a {@link org.bson.Document} query + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery stream(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find entities using a a BSON query and a BSON sort. + * This method is a shortcut for find(query, sort).stream(). + * + * @param query a {@link org.bson.Document} query + * @param sort the {@link org.bson.Document} sort + * @return a new {@link PanacheQuery} instance for the given query + * @see #find(String, Parameters) + * @see #find(String, Sort, Map) + * @see #find(String, Sort, Parameters) + * @see #list(String, Sort, Parameters) + * @see #stream(String, Sort, Parameters) + */ + @GenerateBridge + public default PanacheQuery stream(Document query, Document sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type. + * This method is a shortcut for findAll().stream(). + * + * @return a {@link Stream} containing all results, without paging + * @see #streamAll(Sort) + * @see #findAll() + * @see #listAll() + */ + @GenerateBridge + public default Stream streamAll(Sort sort) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Find all entities of this type, in the given order. + * This method is a shortcut for findAll(sort).stream(). + * + * @return a {@link Stream} containing all results, without paging + * @see #streamAll() + * @see #findAll(Sort) + * @see #listAll(Sort) + */ + @GenerateBridge + public default Stream streamAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity in the database. + * + * @return the number of this type of entity in the database. + * @see #count(String, Object...) + * @see #count(String, Map) + * @see #count(String, Parameters) + */ + @GenerateBridge + public default long count() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Map) + * @see #count(String, Parameters) + */ + @GenerateBridge + public default long count(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Parameters) + */ + @GenerateBridge + public default long count(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public default long count(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Counts the number of this type of entity matching the given query + * + * @param query a {@link org.bson.Document} query + * @return he number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public default long count(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type from the database. + * + * @return the number of entities deleted. + * @see #delete(String, Object...) + * @see #delete(String, Map) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public default long deleteAll() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with optional indexed parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params optional sequence of indexed parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Map) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public default long delete(String query, Object... params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Map} of named parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Object...) + * @see #delete(String, Parameters) + */ + @GenerateBridge + public default long delete(String query, Map params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query, with named parameters. + * + * @param query a {@link io.quarkus.mongodb.panache query string} + * @param params {@link Parameters} of named parameters + * @return the number of entities deleted. + * @see #deleteAll() + * @see #delete(String, Object...) + * @see #delete(String, Map) + */ + @GenerateBridge + public default long delete(String query, Parameters params) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Delete all entities of this type matching the given query + * + * @param query a {@link org.bson.Document} query + * @return he number of entities counted. + * @see #count() + * @see #count(String, Object...) + * @see #count(String, Map) + */ + @GenerateBridge + public default long delete(Document query) { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Persist all given entities. + * + * @param entities the entities to insert + * @see #persist(Object) + * @see #persist(Stream) + * @see #persist(Object,Object...) + */ + public default void persist(Iterable entities) { + MongoOperations.persist(entities); + } + + /** + * Persist all given entities. + * + * @param entities the entities to insert + * @see #persist(Object) + * @see #persist(Iterable) + * @see #persist(Object,Object...) + */ + public default void persist(Stream entities) { + MongoOperations.persist(entities); + } + + /** + * Persist all given entities. + * + * @param entities the entities to insert + * @see #persist(Object) + * @see #persist(Stream) + * @see #persist(Iterable) + */ + public default void persist(Entity firstEntity, @SuppressWarnings("unchecked") Entity... entities) { + MongoOperations.persist(firstEntity, entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to update + * @see #update(Object) + * @see #update(Stream) + * @see #update(Object,Object...) + */ + public default void update(Iterable entities) { + MongoOperations.update(entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to update + * @see #update(Object) + * @see #update(Iterable) + * @see #update(Object,Object...) + */ + public default void update(Stream entities) { + MongoOperations.update(entities); + } + + /** + * Update all given entities. + * + * @param entities the entities to update + * @see #update(Object) + * @see #update(Stream) + * @see #update(Iterable) + */ + public default void update(Entity firstEntity, @SuppressWarnings("unchecked") Entity... entities) { + MongoOperations.update(firstEntity, entities); + } + + /** + * Persist all given entities or update them if they already exist. + * + * @param entities the entities to update + * @see #persistOrUpdate(Object) + * @see #persistOrUpdate(Stream) + * @see #persistOrUpdate(Object,Object...) + */ + public default void persistOrUpdate(Iterable entities) { + MongoOperations.persistOrUpdate(entities); + } + + /** + * Persist all given entities or update them if they already exist. + * + * @param entities the entities to update + * @see #persistOrUpdate(Object) + * @see #persistOrUpdate(Iterable) + * @see #persistOrUpdate(Object,Object...) + */ + public default void persistOrUpdate(Stream entities) { + MongoOperations.persistOrUpdate(entities); + } + + /** + * Persist all given entities or update them if they already exist. + * + * @param entities the entities to update + * @see #update(Object) + * @see #update(Stream) + * @see #update(Iterable) + */ + public default void persistOrUpdate(Entity firstEntity, @SuppressWarnings("unchecked") Entity... entities) { + MongoOperations.persistOrUpdate(firstEntity, entities); + } + + /** + * Allow to access the underlying Mongo Collection + */ + @GenerateBridge + public default MongoCollection mongoCollection() { + throw MongoOperations.implementationInjectionMissing(); + } + + /** + * Allow to access the underlying Mongo Database. + */ + @GenerateBridge + public default MongoDatabase mongoDatabase() { + throw MongoOperations.implementationInjectionMissing(); + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java new file mode 100755 index 0000000000000..3ba5f0dd381cd --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/PanacheQuery.java @@ -0,0 +1,157 @@ +package io.quarkus.mongodb.panache; + +import java.util.List; +import java.util.stream.Stream; + +import io.quarkus.panache.common.Page; + +/** + * Interface representing an entity query, which abstracts the use of paging, getting the number of results, and + * operating on {@link List} or {@link Stream}. + * + * Instances of this interface cannot mutate the query itself or its parameters: only paging information can be + * modified, and instances of this interface can be reused to obtain multiple pages of results. + * + * @param The entity type being queried + */ +public interface PanacheQuery { + + // Builder + + /** + * Sets the current page. + * + * @param page the new page + * @return this query, modified + * @see #page(int, int) + * @see #page() + */ + public PanacheQuery page(Page page); + + /** + * Sets the current page. + * + * @param pageIndex the page index + * @param pageSize the page size + * @return this query, modified + * @see #page(Page) + * @see #page() + */ + public PanacheQuery page(int pageIndex, int pageSize); + + /** + * Sets the current page to the next page + * + * @return this query, modified + * @see #previousPage() + */ + public PanacheQuery nextPage(); + + /** + * Sets the current page to the previous page (or the first page if there is no previous page) + * + * @return this query, modified + * @see #nextPage() + */ + public PanacheQuery previousPage(); + + /** + * Sets the current page to the first page + * + * @return this query, modified + * @see #lastPage() + */ + public PanacheQuery firstPage(); + + /** + * Sets the current page to the last page. This will cause reading of the entity count. + * + * @return this query, modified + * @see #firstPage() + * @see #count() + */ + public PanacheQuery lastPage(); + + /** + * Returns true if there is another page to read after the current one. + * This will cause reading of the entity count. + * + * @return true if there is another page to read + * @see #hasPreviousPage() + * @see #count() + */ + public boolean hasNextPage(); + + /** + * Returns true if there is a page to read before the current one. + * + * @return true if there is a previous page to read + * @see #hasNextPage() + */ + public boolean hasPreviousPage(); + + /** + * Returns the total number of pages to be read using the current page size. + * This will cause reading of the entity count. + * + * @return the total number of pages to be read using the current page size. + */ + public int pageCount(); + + /** + * Returns the current page. + * + * @return the current page + * @see #page(Page) + * @see #page(int,int) + */ + public Page page(); + + // Results + + /** + * Reads and caches the total number of entities this query operates on. This causes a database + * query with SELECT COUNT(*) and a query equivalent to the current query, minus + * ordering. + * + * @return the total number of entities this query operates on, cached. + */ + public long count(); + + /** + * Returns the current page of results as a {@link List}. + * + * @return the current page of results as a {@link List}. + * @see #stream() + * @see #page(Page) + * @see #page() + */ + public List list(); + + /** + * Returns the current page of results as a {@link Stream}. + * + * @return the current page of results as a {@link Stream}. + * @see #list() + * @see #page(Page) + * @see #page() + */ + public Stream stream(); + + /** + * Returns the first result of the current page index. This ignores the current page size to fetch + * a single result. + * + * @return the first result of the current page index, or null if there are no results. + * @see #singleResult() + */ + public T firstResult(); + + /** + * Executes this query for the current page and return a single result. + * + * @return the single result (throws if there is not exactly one) + * @see #firstResult() + */ + public T singleResult(); +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdDeserializer.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdDeserializer.java new file mode 100644 index 0000000000000..153959cea58a7 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdDeserializer.java @@ -0,0 +1,23 @@ +package io.quarkus.mongodb.panache.jackson; + +import java.io.IOException; + +import org.bson.types.ObjectId; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; + +public class ObjectIdDeserializer extends JsonDeserializer { + + @Override + public ObjectId deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) + throws IOException, JsonProcessingException { + String value = jsonParser.getValueAsString(); + if (value != null) { + return new ObjectId(value); + } + return null; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdSerializer.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdSerializer.java new file mode 100644 index 0000000000000..b474976094f9a --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectIdSerializer.java @@ -0,0 +1,20 @@ +package io.quarkus.mongodb.panache.jackson; + +import java.io.IOException; + +import org.bson.types.ObjectId; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; + +public class ObjectIdSerializer extends JsonSerializer { + + @Override + public void serialize(ObjectId objectId, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) + throws IOException { + if (objectId != null) { + jsonGenerator.writeString(objectId.toString()); + } + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectMapperProducer.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectMapperProducer.java new file mode 100755 index 0000000000000..ccf41d8367add --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jackson/ObjectMapperProducer.java @@ -0,0 +1,32 @@ +package io.quarkus.mongodb.panache.jackson; + +import javax.enterprise.context.ApplicationScoped; +import javax.enterprise.inject.Produces; +import javax.inject.Singleton; + +import org.bson.types.ObjectId; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.module.SimpleModule; + +/** + * This ObjectMapperProducer will produce an ObjectMapper that will override the default one from resteasy-jackson. + * + * Note: to avoid automatically installing it, it is removed from the index via an exclusion on the Jandex Maven plugin. + * The PanacheResourceProcessor will include it as a CDI bean if the 'quarkus-resteasy-jackson' extension is used + * and it will replace the default ObjectMapperProducer. + */ +@ApplicationScoped +public class ObjectMapperProducer { + + @Singleton + @Produces + public ObjectMapper objectMapper() { + ObjectMapper objectMapper = new ObjectMapper(); + SimpleModule module = new SimpleModule("ObjectIdModule"); + module.addSerializer(ObjectId.class, new ObjectIdSerializer()); + module.addDeserializer(ObjectId.class, new ObjectIdDeserializer()); + objectMapper.registerModule(module); + return objectMapper; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdDeserializer.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdDeserializer.java new file mode 100644 index 0000000000000..9a2868983ea43 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdDeserializer.java @@ -0,0 +1,20 @@ +package io.quarkus.mongodb.panache.jsonb; + +import java.lang.reflect.Type; + +import javax.json.bind.serializer.DeserializationContext; +import javax.json.bind.serializer.JsonbDeserializer; +import javax.json.stream.JsonParser; + +import org.bson.types.ObjectId; + +public class ObjectIdDeserializer implements JsonbDeserializer { + @Override + public ObjectId deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { + String id = parser.getString(); + if (id != null) { + return new ObjectId(id); + } + return null; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdSerializer.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdSerializer.java new file mode 100644 index 0000000000000..74928d3299ba1 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/ObjectIdSerializer.java @@ -0,0 +1,17 @@ +package io.quarkus.mongodb.panache.jsonb; + +import javax.json.bind.serializer.JsonbSerializer; +import javax.json.bind.serializer.SerializationContext; +import javax.json.stream.JsonGenerator; + +import org.bson.types.ObjectId; + +public class ObjectIdSerializer implements JsonbSerializer { + + @Override + public void serialize(ObjectId obj, JsonGenerator generator, SerializationContext ctx) { + if (obj != null) { + generator.write(obj.toString()); + } + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/PanacheMongoJsonbContextResolver.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/PanacheMongoJsonbContextResolver.java new file mode 100755 index 0000000000000..460015239f9b6 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/jsonb/PanacheMongoJsonbContextResolver.java @@ -0,0 +1,25 @@ +package io.quarkus.mongodb.panache.jsonb; + +import javax.json.bind.Jsonb; +import javax.json.bind.JsonbBuilder; +import javax.json.bind.JsonbConfig; +import javax.ws.rs.ext.ContextResolver; +import javax.ws.rs.ext.Provider; + +/** + * This will provide serialization/deserialization of a MongoDB ObjectId as a String. + * + * Note: to avoid automatically installing it, it is removed from the index via an exclusion on the Jandex Maven plugin. + * The PanacheResourceProcessor will include it as a CDI bean if the 'quarus-resteasy-jsonb' extension is used + * and it will replace the default Jsonb ContextResolver. + */ +@Provider +public class PanacheMongoJsonbContextResolver implements ContextResolver { + + public Jsonb getContext(Class clazz) { + JsonbConfig config = new JsonbConfig(); + config.withSerializers(new ObjectIdSerializer()).withDeserializers(new ObjectIdDeserializer()); + return JsonbBuilder.create(config); + } + +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java new file mode 100644 index 0000000000000..a8008fb532476 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/package-info.java @@ -0,0 +1,70 @@ +/** + *

API usage

+ * + * Make your entities extend {@link io.quarkus.mongodb.panache.PanacheMongoEntity}, use public fields or getter/setter + * for your columns, use the existing operations defined as static methods on your entity class, + * and define custom ones as static methods on your entity class: + * + *
+ * public class Person extends PanacheMongoEntity {
+ *     public String name;
+ *     public LocalDate birth;
+ *     public PersonStatus status;
+ *     
+ *     public static Person findByName(String name){
+ *       return find("name", name).firstResult();
+ *     }
+ *     
+ *     public static List<Person> findAlive(){
+ *       return list("status", Status.Alive);
+ *     }
+ *     
+ *     public static void deleteStefs(){
+ *       delete("name", "Stef");
+ *     }
+ * }
+ * 
+ * + * You can also use {@link io.quarkus.mongodb.panache.PanacheMongoRepository} if you prefer the repository approach. + * + *

+ * You can use the @MongoEntity annotation to define the name of the MongoDB collection, + * otherwise it will be the name of your entity. + *

+ *

+ * The Mongo PojoCodec is used to serialize your entity to Bson Document, you can find more information on it's + * documentation page: https://mongodb.github.io/mongo-java-driver/3.10/bson/pojos/ + * You can use the MongoDB annotations to control the mapping to the database : @BsonId, + * @BsonProperty("fieldName"), @BsonIgnore. + *

+ * + *

Simplified queries

+ * + *

+ * Normally, MongoDB queries are of this form: {"field1": "value1", "field2": "value2"} + *

+ *

+ * We support multiple convenience query implementations, this is what we called PanacheQL queries: + *

    + *
  • You can use one of the three flavours or parameterized query: + *
      + *
    • find("field", value)
    • + *
    • find("field = ?1", value)
    • + *
    • find("field = :value", Parameters.with("value", value)
    • + *
    + * They will all generates the same query : {"field": "value"}. + *
  • + *
  • We support the following query operators: 'and', 'or' ( mixing 'and' and 'or' is not currently supported), '=', + * '>', '>=', '<', '<=', '!=', 'is null', 'is not null', and 'like' that is mapped to the MongoDB `$regex` operator.
  • + *
  • field replacement is supported based on the value of the @BsonProperty annotations
  • + *
+ *

+ *

+ * You can also write native MongoDB queries, in this case the field names are not replaced even if you use + * @BsonProperty, but you can still use parameterized queries by index or name.
+ * find("{'field':?1}", value) or find("{'field'::key}", value) + *

+ * + * @author Loïc Mathieu + */ +package io.quarkus.mongodb.panache; diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java new file mode 100644 index 0000000000000..8ab9230ad6d4a --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/CommonQueryBinder.java @@ -0,0 +1,39 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.text.SimpleDateFormat; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +final class CommonQueryBinder { + + static final String ISO_DATE_PATTERN = "yyyy-MM-dd'T'HH:mm:ss.SSS"; + + private CommonQueryBinder() { + } + + static String replace(String query, String oldChars, Object value) { + return query.replace(oldChars, escape(value)); + } + + static String escape(Object value) { + if (Number.class.isAssignableFrom(value.getClass()) || value instanceof Boolean) { + return value.toString(); + } + if (value instanceof Date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(ISO_DATE_PATTERN); + Date dateValue = (Date) value; + return "ISODate('" + dateFormat.format(dateValue) + "')"; + } + if (value instanceof LocalDate) { + LocalDate dateValue = (LocalDate) value; + return "ISODate('" + DateTimeFormatter.ISO_LOCAL_DATE.format(dateValue) + "')"; + } + if (value instanceof LocalDateTime) { + LocalDateTime dateValue = (LocalDateTime) value; + return "ISODate('" + DateTimeFormatter.ISO_LOCAL_DATE_TIME.format(dateValue) + "')"; + } + return "'" + value.toString().replace("\\", "\\\\").replace("'", "\\'") + "'"; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java new file mode 100644 index 0000000000000..4584a9cacdc04 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoOperations.java @@ -0,0 +1,547 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.bson.BsonDocument; +import org.bson.BsonDocumentWriter; +import org.bson.BsonValue; +import org.bson.Document; +import org.bson.codecs.Codec; +import org.bson.codecs.EncoderContext; +import org.eclipse.microprofile.config.ConfigProvider; +import org.jboss.logging.Logger; + +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; +import com.mongodb.client.model.InsertOneModel; +import com.mongodb.client.model.ReplaceOneModel; +import com.mongodb.client.model.ReplaceOptions; +import com.mongodb.client.model.UpdateOptions; +import com.mongodb.client.model.WriteModel; + +import io.quarkus.arc.Arc; +import io.quarkus.mongodb.panache.MongoEntity; +import io.quarkus.mongodb.panache.PanacheQuery; +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +public class MongoOperations { + private static final Logger LOGGER = Logger.getLogger(MongoOperations.class); + public static final String ID = "_id"; + public static final String MONGODB_DATABASE = "quarkus.mongodb.database"; + // + // Instance methods + + public static void persist(Object entity) { + MongoCollection collection = mongoCollection(entity); + persist(collection, entity); + } + + public static void persist(Iterable entities) { + // not all iterables are re-traversal, so we traverse it once for copying inside a list + List objects = new ArrayList<>(); + for (Object entity : entities) { + objects.add(entity); + } + + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + persist(collection, objects); + } + } + + public static void persist(Object firstEntity, Object... entities) { + MongoCollection collection = mongoCollection(firstEntity); + if (entities == null || entities.length == 0) { + persist(collection, firstEntity); + } else { + List entityList = new ArrayList<>(); + entityList.add(firstEntity); + entityList.addAll(Arrays.asList(entities)); + persist(collection, entityList); + } + } + + public static void persist(Stream entities) { + List objects = entities.collect(Collectors.toList()); + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + persist(collection, objects); + } + } + + public static void update(Object entity) { + MongoCollection collection = mongoCollection(entity); + update(collection, entity); + } + + public static void update(Iterable entities) { + // not all iterables are re-traversal, so we traverse it once for copying inside a list + List objects = new ArrayList<>(); + for (Object entity : entities) { + objects.add(entity); + } + + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + update(collection, objects); + } + } + + public static void update(Object firstEntity, Object... entities) { + MongoCollection collection = mongoCollection(firstEntity); + if (entities == null || entities.length == 0) { + update(collection, firstEntity); + } else { + List entityList = new ArrayList<>(); + entityList.add(firstEntity); + entityList.addAll(Arrays.asList(entities)); + update(collection, entityList); + } + } + + public static void update(Stream entities) { + List objects = entities.collect(Collectors.toList()); + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + update(collection, objects); + } + } + + public static void persistOrUpdate(Object entity) { + MongoCollection collection = mongoCollection(entity); + persistOrUpdate(collection, entity); + } + + public static void persistOrUpdate(Iterable entities) { + // not all iterables are re-traversal, so we traverse it once for copying inside a list + List objects = new ArrayList<>(); + for (Object entity : entities) { + objects.add(entity); + } + + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + persistOrUpdate(collection, objects); + } + } + + public static void persistOrUpdate(Object firstEntity, Object... entities) { + MongoCollection collection = mongoCollection(firstEntity); + if (entities == null || entities.length == 0) { + persistOrUpdate(collection, firstEntity); + } else { + List entityList = new ArrayList<>(); + entityList.add(firstEntity); + entityList.addAll(Arrays.asList(entities)); + persistOrUpdate(collection, entityList); + } + } + + public static void persistOrUpdate(Stream entities) { + List objects = entities.collect(Collectors.toList()); + if (objects.size() > 0) { + // get the first entity to be able to retrieve the collection with it + Object firstEntity = objects.get(0); + MongoCollection collection = mongoCollection(firstEntity); + persistOrUpdate(collection, objects); + } + } + + public static void delete(Object entity) { + MongoCollection collection = mongoCollection(entity); + BsonDocument document = getBsonDocument(collection, entity); + BsonValue id = document.get(ID); + BsonDocument query = new BsonDocument().append(ID, id); + collection.deleteOne(query); + } + + public static MongoCollection mongoCollection(Class entityClass) { + MongoEntity mongoEntity = entityClass.getAnnotation(MongoEntity.class); + MongoDatabase database = mongoDatabase(mongoEntity); + if (mongoEntity != null && !mongoEntity.collection().isEmpty()) { + return database.getCollection(mongoEntity.collection(), entityClass); + } + return database.getCollection(entityClass.getSimpleName(), entityClass); + } + + public static MongoDatabase mongoDatabase(Class entityClass) { + MongoEntity mongoEntity = entityClass.getAnnotation(MongoEntity.class); + return mongoDatabase(mongoEntity); + } + + // + // Private stuff + + private static void persist(MongoCollection collection, Object entity) { + collection.insertOne(entity); + } + + private static void persist(MongoCollection collection, List entities) { + collection.insertMany(entities); + } + + private static void update(MongoCollection collection, Object entity) { + //we transform the entity as a document first + BsonDocument document = getBsonDocument(collection, entity); + + //then we get its id field and create a new Document with only this one that will be our replace query + BsonValue id = document.get(ID); + BsonDocument query = new BsonDocument().append(ID, id); + collection.replaceOne(query, entity); + } + + private static void update(MongoCollection collection, List entities) { + for (Object entity : entities) { + update(collection, entity); + } + } + + private static void persistOrUpdate(MongoCollection collection, Object entity) { + //we transform the entity as a document first + BsonDocument document = getBsonDocument(collection, entity); + + //then we get its id field and create a new Document with only this one that will be our replace query + BsonValue id = document.get(ID); + if (id == null) { + //insert with autogenerated ID + collection.insertOne(entity); + } else { + //insert with user provided ID or update + BsonDocument query = new BsonDocument().append(ID, id); + collection.replaceOne(query, entity, ReplaceOptions.createReplaceOptions(new UpdateOptions().upsert(true))); + } + } + + private static void persistOrUpdate(MongoCollection collection, List entities) { + //this will be an ordered bulk: it's less performant than a unordered one but will fail at the first failed write + List bulk = new ArrayList<>(); + for (Object entity : entities) { + //we transform the entity as a document first + BsonDocument document = getBsonDocument(collection, entity); + + //then we get its id field and create a new Document with only this one that will be our replace query + BsonValue id = document.get(ID); + if (id == null) { + //insert with autogenerated ID + bulk.add(new InsertOneModel(entity)); + } else { + //insert with user provided ID or update + BsonDocument query = new BsonDocument().append(ID, id); + bulk.add(new ReplaceOneModel(query, entity, + ReplaceOptions.createReplaceOptions(new UpdateOptions().upsert(true)))); + } + } + + collection.bulkWrite(bulk); + } + + private static BsonDocument getBsonDocument(MongoCollection collection, Object entity) { + BsonDocument document = new BsonDocument(); + Codec codec = collection.getCodecRegistry().get(entity.getClass()); + codec.encode(new BsonDocumentWriter(document), entity, EncoderContext.builder().build()); + return document; + } + + private static MongoCollection mongoCollection(Object entity) { + Class entityClass = entity.getClass(); + return mongoCollection(entityClass); + } + + private static MongoDatabase mongoDatabase(MongoEntity entity) { + MongoClient mongoClient = Arc.container().instance(MongoClient.class).get(); + if (entity != null && !entity.database().isEmpty()) { + return mongoClient.getDatabase(entity.database()); + } + String databaseName = ConfigProvider.getConfig() + .getValue(MONGODB_DATABASE, String.class); + return mongoClient.getDatabase(databaseName); + } + + // + // Queries + + public static Object findById(Class entityClass, Object id) { + MongoCollection collection = mongoCollection(entityClass); + return collection.find(new Document(ID, id)).first(); + } + + public static PanacheQuery find(Class entityClass, String query, Object... params) { + return find(entityClass, query, null, params); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, String query, Sort sort, Object... params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + Document docSort = sortToDocument(sort); + MongoCollection collection = mongoCollection(entityClass); + return new PanacheQueryImpl(collection, entityClass, docQuery, docSort); + } + + /** + * We should have a query like {'firstname': ?1, 'lastname': ?2} for native one + * and like firstname = ?1 for PanacheQL one. + */ + static String bindQuery(Class clazz, String query, Object[] params) { + String bindQuery = null; + + //determine the type of the query + if (query.charAt(0) == '{') { + //this is a native query + bindQuery = NativeQueryBinder.bindQuery(query, params); + } else { + //this is a PanacheQL query + bindQuery = PanacheQlQueryBinder.bindQuery(clazz, query, params); + } + + LOGGER.debug(bindQuery); + return bindQuery; + } + + /** + * We should have a query like {'firstname': :firstname, 'lastname': :lastname} for native one + * and like firstname = :firstname and lastname = :lastname for PanacheQL one. + */ + static String bindQuery(Class clazz, String query, Map params) { + String bindQuery = null; + + //determine the type of the query + if (query.charAt(0) == '{') { + //this is a native query + bindQuery = NativeQueryBinder.bindQuery(query, params); + } else { + //this is a PanacheQL query + bindQuery = PanacheQlQueryBinder.bindQuery(clazz, query, params); + } + + LOGGER.debug(bindQuery); + return bindQuery; + } + + public static PanacheQuery find(Class entityClass, String query, Map params) { + return find(entityClass, query, null, params); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, String query, Sort sort, Map params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + Document docSort = sortToDocument(sort); + MongoCollection collection = mongoCollection(entityClass); + return new PanacheQueryImpl(collection, entityClass, docQuery, docSort); + } + + public static PanacheQuery find(Class entityClass, String query, Parameters params) { + return find(entityClass, query, null, params.map()); + } + + public static PanacheQuery find(Class entityClass, String query, Sort sort, Parameters params) { + return find(entityClass, query, sort, params.map()); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery find(Class entityClass, Document query, Sort sort) { + MongoCollection collection = mongoCollection(entityClass); + Document sortDoc = sortToDocument(sort); + return new PanacheQueryImpl(collection, entityClass, query, sortDoc); + } + + public static PanacheQuery find(Class entityClass, Document query, Document sort) { + MongoCollection collection = mongoCollection(entityClass); + return new PanacheQueryImpl(collection, entityClass, query, sort); + } + + public static PanacheQuery find(Class entityClass, Document query) { + return find(entityClass, query, (Document) null); + } + + public static List list(Class entityClass, String query, Object... params) { + return find(entityClass, query, params).list(); + } + + public static List list(Class entityClass, String query, Sort sort, Object... params) { + return find(entityClass, query, sort, params).list(); + } + + public static List list(Class entityClass, String query, Map params) { + return find(entityClass, query, params).list(); + } + + public static List list(Class entityClass, String query, Sort sort, Map params) { + return find(entityClass, query, sort, params).list(); + } + + public static List list(Class entityClass, String query, Parameters params) { + return find(entityClass, query, params).list(); + } + + public static List list(Class entityClass, String query, Sort sort, Parameters params) { + return find(entityClass, query, sort, params).list(); + } + + //specific Mongo query + public static List list(Class entityClass, Document query) { + return find(entityClass, query).list(); + } + + //specific Mongo query + public static List list(Class entityClass, Document query, Document sort) { + return find(entityClass, query, sort).list(); + } + + public static Stream stream(Class entityClass, String query, Object... params) { + return find(entityClass, query, params).stream(); + } + + public static Stream stream(Class entityClass, String query, Sort sort, Object... params) { + return find(entityClass, query, sort, params).stream(); + } + + public static Stream stream(Class entityClass, String query, Map params) { + return find(entityClass, query, params).stream(); + } + + public static Stream stream(Class entityClass, String query, Sort sort, Map params) { + return find(entityClass, query, sort, params).stream(); + } + + public static Stream stream(Class entityClass, String query, Parameters params) { + return find(entityClass, query, params).stream(); + } + + public static Stream stream(Class entityClass, String query, Sort sort, Parameters params) { + return find(entityClass, query, sort, params).stream(); + } + + //specific Mongo query + public static Stream stream(Class entityClass, Document query) { + return find(entityClass, query).stream(); + } + + //specific Mongo query + public static Stream stream(Class entityClass, Document query, Document sort) { + return find(entityClass, query, sort).stream(); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery findAll(Class entityClass) { + MongoCollection collection = mongoCollection(entityClass); + return new PanacheQueryImpl(collection, entityClass, null, null); + } + + @SuppressWarnings("rawtypes") + public static PanacheQuery findAll(Class entityClass, Sort sort) { + MongoCollection collection = mongoCollection(entityClass); + Document sortDoc = sortToDocument(sort); + return new PanacheQueryImpl(collection, entityClass, null, sortDoc); + } + + private static Document sortToDocument(Sort sort) { + if (sort == null) { + return null; + } + + Document sortDoc = new Document(); + for (Sort.Column col : sort.getColumns()) { + sortDoc.append(col.getName(), col.getDirection() == Sort.Direction.Ascending ? 1 : -1); + } + return sortDoc; + } + + public static List listAll(Class entityClass) { + return findAll(entityClass).list(); + } + + public static List listAll(Class entityClass, Sort sort) { + return findAll(entityClass, sort).list(); + } + + public static Stream streamAll(Class entityClass) { + return findAll(entityClass).stream(); + } + + public static Stream streamAll(Class entityClass, Sort sort) { + return findAll(entityClass, sort).stream(); + } + + public static long count(Class entityClass) { + MongoCollection collection = mongoCollection(entityClass); + return collection.countDocuments(); + } + + public static long count(Class entityClass, String query, Object... params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + MongoCollection collection = mongoCollection(entityClass); + return collection.countDocuments(docQuery); + } + + public static long count(Class entityClass, String query, Map params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + MongoCollection collection = mongoCollection(entityClass); + return collection.countDocuments(docQuery); + } + + public static long count(Class entityClass, String query, Parameters params) { + return count(entityClass, query, params.map()); + } + + //specific Mongo query + public static long count(Class entityClass, Document query) { + MongoCollection collection = mongoCollection(entityClass); + return collection.countDocuments(query); + } + + public static long deleteAll(Class entityClass) { + MongoCollection collection = mongoCollection(entityClass); + return collection.deleteMany(new Document()).getDeletedCount(); + } + + public static long delete(Class entityClass, String query, Object... params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + MongoCollection collection = mongoCollection(entityClass); + return collection.deleteMany(docQuery).getDeletedCount(); + } + + public static long delete(Class entityClass, String query, Map params) { + String bindQuery = bindQuery(entityClass, query, params); + Document docQuery = Document.parse(bindQuery); + MongoCollection collection = mongoCollection(entityClass); + return collection.deleteMany(docQuery).getDeletedCount(); + } + + public static long delete(Class entityClass, String query, Parameters params) { + return delete(entityClass, query, params.map()); + } + + //specific Mongo query + public static long delete(Class entityClass, Document query) { + MongoCollection collection = mongoCollection(entityClass); + return collection.deleteMany(query).getDeletedCount(); + } + + public static IllegalStateException implementationInjectionMissing() { + return new IllegalStateException( + "This method is normally automatically overridden in subclasses"); + } + +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoParserVisitor.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoParserVisitor.java new file mode 100644 index 0000000000000..1c093b86611b6 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/MongoParserVisitor.java @@ -0,0 +1,103 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.util.Map; + +import io.quarkus.panacheql.internal.HqlParser; +import io.quarkus.panacheql.internal.HqlParserBaseVisitor; + +class MongoParserVisitor extends HqlParserBaseVisitor { + private Map replacementMap; + private Map parameterMaps; + + public MongoParserVisitor(Map replacementMap, Map parameterMaps) { + this.replacementMap = replacementMap; + this.parameterMaps = parameterMaps; + } + + @Override + public String visitAndPredicate(HqlParser.AndPredicateContext ctx) { + StringBuilder sb = new StringBuilder(); + for (HqlParser.PredicateContext predicate : ctx.predicate()) { + if (sb.length() > 0) + sb.append(","); + sb.append(predicate.accept(this)); + } + return sb.toString(); + } + + @Override + public String visitOrPredicate(HqlParser.OrPredicateContext ctx) { + StringBuilder sb = new StringBuilder("'$or':["); + for (HqlParser.PredicateContext predicate : ctx.predicate()) { + if (sb.length() > 7) + sb.append(","); + sb.append('{').append(predicate.accept(this)).append('}'); + } + sb.append("]"); + return sb.toString(); + } + + @Override + public String visitEqualityPredicate(HqlParser.EqualityPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":" + ctx.expression(1).accept(this); + } + + @Override + public String visitInequalityPredicate(HqlParser.InequalityPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$ne':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitLessThanOrEqualPredicate(HqlParser.LessThanOrEqualPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$lte':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitLikePredicate(HqlParser.LikePredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$regex':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitGreaterThanPredicate(HqlParser.GreaterThanPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$gt':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitLessThanPredicate(HqlParser.LessThanPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$lt':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitGreaterThanOrEqualPredicate(HqlParser.GreaterThanOrEqualPredicateContext ctx) { + return ctx.expression(0).accept(this) + ":{'$gte':" + ctx.expression(1).accept(this) + "}"; + } + + @Override + public String visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) { + boolean exists = ctx.NOT() != null; + return ctx.expression().accept(this) + ":{'$exists':" + exists + "}"; + } + + @Override + public String visitLiteralExpression(HqlParser.LiteralExpressionContext ctx) { + return CommonQueryBinder.escape(ctx.getText()); + } + + @Override + public String visitParameterExpression(HqlParser.ParameterExpressionContext ctx) { + // this will match parameters used by PanacheQL : '?1' for index based or ':key' for named one. + if (parameterMaps.containsKey(ctx.getText())) { + Object value = parameterMaps.get(ctx.getText()); + return CommonQueryBinder.escape(value); + } else { + // we return the parameter to avoid an exception but the query will be invalid + return ctx.getText(); + } + } + + @Override + public String visitPathExpression(HqlParser.PathExpressionContext ctx) { + // this is the name of the field, we apply replacement and escape with ' + return "'" + replacementMap.getOrDefault(ctx.getText(), ctx.getText()) + "'"; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/NativeQueryBinder.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/NativeQueryBinder.java new file mode 100644 index 0000000000000..588c8b96f356b --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/NativeQueryBinder.java @@ -0,0 +1,26 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.util.Map; + +class NativeQueryBinder { + + public static String bindQuery(String query, Object[] params) { + String bindQuery = query; + for (int i = 1; i <= params.length; i++) { + String bindParamsKey = "?" + i; + bindQuery = CommonQueryBinder.replace(bindQuery, bindParamsKey, params[i - 1]); + } + + return bindQuery; + } + + public static String bindQuery(String query, Map params) { + String bindQuery = query; + for (Map.Entry entry : params.entrySet()) { + String bindParamsKey = ":" + entry.getKey(); + bindQuery = CommonQueryBinder.replace(bindQuery, bindParamsKey, entry.getValue()); + } + + return bindQuery; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java new file mode 100644 index 0000000000000..0110cc8ab93b6 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQlQueryBinder.java @@ -0,0 +1,83 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.beans.Introspector; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Map; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.bson.codecs.pojo.annotations.BsonProperty; + +import io.quarkus.panacheql.internal.HqlLexer; +import io.quarkus.panacheql.internal.HqlParser; +import io.quarkus.panacheql.internal.HqlParserBaseVisitor; + +public class PanacheQlQueryBinder { + + public static String bindQuery(Class clazz, String query, Object[] params) { + Map replacementMap = extractReplacementMap(clazz); + + //shorthand query + if (params.length == 1 && query.indexOf('?') == -1) { + return "{'" + replaceField(query, replacementMap) + "':" + CommonQueryBinder.escape(params[0]) + "}"; + } + + //classic query + Map parameterMaps = new HashMap<>(); + for (int i = 1; i <= params.length; i++) { + String bindParamsKey = "?" + i; + parameterMaps.put(bindParamsKey, params[i - 1]); + } + + return prepareQuery(query, replacementMap, parameterMaps); + } + + public static String bindQuery(Class clazz, String query, Map params) { + Map replacementMap = extractReplacementMap(clazz); + + Map parameterMaps = new HashMap<>(); + for (Map.Entry entry : params.entrySet()) { + String bindParamsKey = ":" + entry.getKey(); + parameterMaps.put(bindParamsKey, entry.getValue()); + } + + return prepareQuery(query, replacementMap, parameterMaps); + } + + private static String replaceField(String field, Map replacementMap) { + return replacementMap.getOrDefault(field, field); + } + + private static Map extractReplacementMap(Class clazz) { + //TODO cache the replacement map or pre-compute it during build (using reflection or jandex) + Map replacementMap = new HashMap<>(); + for (Field field : clazz.getDeclaredFields()) { + BsonProperty bsonProperty = field.getAnnotation(BsonProperty.class); + if (bsonProperty != null) { + replacementMap.put(field.getName(), bsonProperty.value()); + } + } + for (Method method : clazz.getDeclaredMethods()) { + if (method.getName().startsWith("get")) { + // we try to replace also for getter + BsonProperty bsonProperty = method.getAnnotation(BsonProperty.class); + if (bsonProperty != null) { + String fieldName = Introspector.decapitalize(method.getName().substring(3)); + replacementMap.put(fieldName, bsonProperty.value()); + } + } + } + return replacementMap; + } + + private static String prepareQuery(String query, Map replacementMap, Map parameterMaps) { + HqlLexer lexer = new HqlLexer(CharStreams.fromString(query)); + CommonTokenStream tokens = new CommonTokenStream(lexer); + HqlParser parser = new HqlParser(tokens); + HqlParser.PredicateContext predicate = parser.predicate(); + HqlParserBaseVisitor visitor = new MongoParserVisitor(replacementMap, parameterMaps); + return "{" + predicate.accept(visitor) + "}"; + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java new file mode 100644 index 0000000000000..0b3938f2475bb --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/main/java/io/quarkus/mongodb/panache/runtime/PanacheQueryImpl.java @@ -0,0 +1,146 @@ +package io.quarkus.mongodb.panache.runtime; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.bson.Document; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoCursor; + +import io.quarkus.mongodb.panache.PanacheQuery; +import io.quarkus.panache.common.Page; + +public class PanacheQueryImpl implements PanacheQuery { + private MongoCollection collection; + private Class entityClass; + private Document mongoQuery; + private Document sort; + + /* + * We store the pageSize and apply it for each request because getFirstResult() + * sets the page size to 1 + */ + private Page page; + private Long count; + + PanacheQueryImpl(MongoCollection collection, Class entityClass, Document mongoQuery, + Document sort) { + this.collection = collection; + this.entityClass = entityClass; + this.mongoQuery = mongoQuery; + this.sort = sort; + page = new Page(0, Integer.MAX_VALUE); + } + + // Builder + + @Override + @SuppressWarnings("unchecked") + public PanacheQuery page(Page page) { + this.page = page; + return (PanacheQuery) this; + } + + @Override + public PanacheQuery page(int pageIndex, int pageSize) { + return page(Page.of(pageIndex, pageSize)); + } + + @Override + public PanacheQuery nextPage() { + return page(page.next()); + } + + @Override + public PanacheQuery previousPage() { + return page(page.previous()); + } + + @Override + public PanacheQuery firstPage() { + return page(page.first()); + } + + @Override + public PanacheQuery lastPage() { + return page(page.index(pageCount() - 1)); + } + + @Override + public boolean hasNextPage() { + return page.index < (pageCount() - 1); + } + + @Override + public boolean hasPreviousPage() { + return page.index > 0; + } + + @Override + public int pageCount() { + long count = count(); + if (count == 0) + return 1; // a single page of zero results + return (int) Math.ceil((double) count / (double) page.size); + } + + @Override + public Page page() { + return page; + } + + // Results + + @Override + @SuppressWarnings("unchecked") + public long count() { + if (count == null) { + count = collection.countDocuments(mongoQuery); + } + return count; + } + + @Override + @SuppressWarnings("unchecked") + public List list() { + List list = new ArrayList<>(); + FindIterable find = mongoQuery == null ? collection.find() : collection.find(mongoQuery); + MongoCursor cursor = find.sort(sort).skip(page.index).limit(page.size).iterator(); + + try { + while (cursor.hasNext()) { + T entity = cursor.next(); + list.add(entity); + } + } finally { + cursor.close(); + } + return list; + } + + @Override + @SuppressWarnings("unchecked") + public Stream stream() { + return (Stream) list().stream(); + } + + @Override + public T firstResult() { + List list = list(); + return list.isEmpty() ? null : list.get(0); + } + + @Override + @SuppressWarnings("unchecked") + public T singleResult() { + List list = list(); + if (list.isEmpty() || list.size() > 1) { + throw new RuntimeException("There should be only one result");//TODO use proper exception + } + + return list.get(0); + } +} diff --git a/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java b/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java new file mode 100644 index 0000000000000..f8dc5d55b1df1 --- /dev/null +++ b/extensions/panache/mongodb-panache/runtime/src/test/java/io/quarkus/mongodb/panache/runtime/MongoOperationsTest.java @@ -0,0 +1,208 @@ +package io.quarkus.mongodb.panache.runtime; + +import static org.junit.jupiter.api.Assertions.*; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +import org.bson.codecs.pojo.annotations.BsonProperty; +import org.junit.jupiter.api.Test; + +import io.quarkus.panache.common.Parameters; + +class MongoOperationsTest { + + private static class DemoObj { + public String field; + public boolean isOk; + @BsonProperty("value") + public String property; + } + + @Test + public void testBindShorthandQuery() { + String query = MongoOperations.bindQuery(Object.class, "field", new Object[] { "a value" }); + assertEquals("{'field':'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "field", new Object[] { true }); + assertEquals("{'field':true}", query); + + query = MongoOperations.bindQuery(Object.class, "field", new Object[] { LocalDate.of(2019, 3, 4) }); + assertEquals("{'field':ISODate('2019-03-04')}", query); + + query = MongoOperations.bindQuery(Object.class, "field", new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + + query = MongoOperations.bindQuery(Object.class, "field", + new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); + + //test field replacement + query = MongoOperations.bindQuery(DemoObj.class, "property", new Object[] { "a value" }); + assertEquals("{'value':'a value'}", query); + } + + private Object toDate(LocalDateTime of) { + return Date.from(of.atZone(ZoneId.systemDefault()).toInstant()); + } + + @Test + public void testBindNativeQueryByIndex() { + String query = MongoOperations.bindQuery(DemoObj.class, "{'field': ?1}", new Object[] { "a value" }); + assertEquals("{'field': 'a value'}", query); + + query = MongoOperations.bindQuery(DemoObj.class, "{'field.sub': ?1}", new Object[] { "a value" }); + assertEquals("{'field.sub': 'a value'}", query); + + //test that there are no field replacement for native queries + query = MongoOperations.bindQuery(DemoObj.class, "{'property': ?1}", new Object[] { "a value" }); + assertEquals("{'property': 'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': ?1}", + new Object[] { LocalDate.of(2019, 3, 4) }); + assertEquals("{'field': ISODate('2019-03-04')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': ?1}", + new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); + assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': ?1}", + new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); + assertEquals("{'field': ISODate('2019-03-04T01:01:01.000')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': ?1, 'isOk': ?2}", new Object[] { "a value", true }); + assertEquals("{'field': 'a value', 'isOk': true}", query); + } + + @Test + public void testBindNativeQueryByName() { + String query = MongoOperations.bindQuery(Object.class, "{'field': :field}", + Parameters.with("field", "a value").map()); + assertEquals("{'field': 'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field.sub': :field}", + Parameters.with("field", "a value").map()); + assertEquals("{'field.sub': 'a value'}", query); + + //test that there are no field replacement for native queries + query = MongoOperations.bindQuery(DemoObj.class, "{'property': :field}", + Parameters.with("field", "a value").map()); + assertEquals("{'property': 'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': :field}", + Parameters.with("field", LocalDate.of(2019, 3, 4)).map()); + assertEquals("{'field': ISODate('2019-03-04')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': :field}", + Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1)).map()); + assertEquals("{'field': ISODate('2019-03-04T01:01:01')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': :field}", + Parameters.with("field", toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1))).map()); + assertEquals("{'field': ISODate('2019-03-04T01:01:01.000')}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field': :field, 'isOk': :isOk}", + Parameters.with("field", "a value").and("isOk", true).map()); + assertEquals("{'field': 'a value', 'isOk': true}", query); + } + + @Test + public void testBindEnhancedQueryByIndex() { + String query = MongoOperations.bindQuery(Object.class, "field = ?1", new Object[] { "a value" }); + assertEquals("{'field':'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "{'field.sub': :field}", + Parameters.with("field", "a value").map()); + assertEquals("{'field.sub': 'a value'}", query); + + //test field replacement + query = MongoOperations.bindQuery(DemoObj.class, "property = ?1", new Object[] { "a value" }); + assertEquals("{'value':'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "field = ?1", new Object[] { LocalDate.of(2019, 3, 4) }); + assertEquals("{'field':ISODate('2019-03-04')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = ?1", new Object[] { LocalDateTime.of(2019, 3, 4, 1, 1, 1) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = ?1", + new Object[] { toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1)) }); + assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = ?1 and isOk = ?2", new Object[] { "a value", true }); + assertEquals("{'field':'a value','isOk':true}", query); + + query = MongoOperations.bindQuery(Object.class, "field = ?1 or isOk = ?2", new Object[] { "a value", true }); + assertEquals("{'$or':[{'field':'a value'},{'isOk':true}]}", query); + + query = MongoOperations.bindQuery(Object.class, "count >= ?1 and count < ?2", new Object[] { 5, 10 }); + assertEquals("{'count':{'$gte':5},'count':{'$lt':10}}", query); + + query = MongoOperations.bindQuery(Object.class, "field != ?1", new Object[] { "a value" }); + assertEquals("{'field':{'$ne':'a value'}}", query); + + query = MongoOperations.bindQuery(Object.class, "field like ?1", new Object[] { "a value" }); + assertEquals("{'field':{'$regex':'a value'}}", query); + + query = MongoOperations.bindQuery(Object.class, "field is not null", new Object[] {}); + assertEquals("{'field':{'$exists':true}}", query); + + query = MongoOperations.bindQuery(Object.class, "field is null", new Object[] {}); + assertEquals("{'field':{'$exists':false}}", query); + + // test with hardcoded value + query = MongoOperations.bindQuery(Object.class, "field = 'some hardcoded value'", new Object[] {}); + assertEquals("{'field':'some hardcoded value'}", query); + } + + @Test + public void testBindEnhancedQueryByName() { + String query = MongoOperations.bindQuery(Object.class, "field = :field", + Parameters.with("field", "a value").map()); + assertEquals("{'field':'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "field.sub = :field", + Parameters.with("field", "a value").map()); + assertEquals("{'field.sub':'a value'}", query); + + //test field replacement + query = MongoOperations.bindQuery(DemoObj.class, "property = :field", + Parameters.with("field", "a value").map()); + assertEquals("{'value':'a value'}", query); + + query = MongoOperations.bindQuery(Object.class, "field = :field", + Parameters.with("field", LocalDate.of(2019, 3, 4)).map()); + assertEquals("{'field':ISODate('2019-03-04')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = :field", + Parameters.with("field", LocalDateTime.of(2019, 3, 4, 1, 1, 1)).map()); + assertEquals("{'field':ISODate('2019-03-04T01:01:01')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = :field", + Parameters.with("field", toDate(LocalDateTime.of(2019, 3, 4, 1, 1, 1))).map()); + assertEquals("{'field':ISODate('2019-03-04T01:01:01.000')}", query); + + query = MongoOperations.bindQuery(Object.class, "field = :field and isOk = :isOk", + Parameters.with("field", "a value").and("isOk", true).map()); + assertEquals("{'field':'a value','isOk':true}", query); + + query = MongoOperations.bindQuery(Object.class, "field = :field or isOk = :isOk", + Parameters.with("field", "a value").and("isOk", true).map()); + assertEquals("{'$or':[{'field':'a value'},{'isOk':true}]}", query); + + query = MongoOperations.bindQuery(Object.class, "count > :lower and count <= :upper", + Parameters.with("lower", 5).and("upper", 10).map()); + assertEquals("{'count':{'$gt':5},'count':{'$lte':10}}", query); + + query = MongoOperations.bindQuery(Object.class, "field != :field", + Parameters.with("field", "a value").map()); + assertEquals("{'field':{'$ne':'a value'}}", query); + + query = MongoOperations.bindQuery(Object.class, "field like :field", + Parameters.with("field", "a value").map()); + assertEquals("{'field':{'$regex':'a value'}}", query); + } + +} diff --git a/extensions/panache/panacheql/pom.xml b/extensions/panache/panacheql/pom.xml new file mode 100644 index 0000000000000..b4d2c3be902f5 --- /dev/null +++ b/extensions/panache/panacheql/pom.xml @@ -0,0 +1,53 @@ + + + + quarkus-build-parent + io.quarkus + 999-SNAPSHOT + ../../../build-parent/pom.xml + + 4.0.0 + + quarkus-panacheql + Quarkus - Panache - Query Language + + + + org.antlr + antlr4-runtime + + + org.junit.jupiter + junit-jupiter + test + + + org.junit.jupiter + junit-jupiter-api + test + + + + + + + org.antlr + antlr4-maven-plugin + + + antlr + + antlr4 + + + true + + + + + + + + diff --git a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4 b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4 new file mode 100644 index 0000000000000..cff2b1708253a --- /dev/null +++ b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlLexer.g4 @@ -0,0 +1,231 @@ +lexer grammar HqlLexer; + + +@header { +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +} + +WS : ( ' ' | '\t' | '\f' | EOL ) -> skip; + +fragment +EOL : [\r\n]+; + +INTEGER_LITERAL : INTEGER_NUMBER ; + +fragment +INTEGER_NUMBER : ('0' | '1'..'9' '0'..'9'*) ; + +LONG_LITERAL : INTEGER_NUMBER ('l'|'L'); + +BIG_INTEGER_LITERAL : INTEGER_NUMBER ('bi'|'BI') ; + +HEX_LITERAL : '0' ('x'|'X') HEX_DIGIT+ ('l'|'L')? ; + +fragment +HEX_DIGIT : ('0'..'9'|'a'..'f'|'A'..'F') ; + +OCTAL_LITERAL : '0' ('0'..'7')+ ('l'|'L')? ; + +FLOAT_LITERAL : FLOATING_POINT_NUMBER ('f'|'F')? ; + +fragment +FLOATING_POINT_NUMBER + : ('0'..'9')+ '.' ('0'..'9')* EXPONENT? + | '.' ('0'..'9')+ EXPONENT? + | ('0'..'9')+ EXPONENT + | ('0'..'9')+ + ; + +DOUBLE_LITERAL : FLOATING_POINT_NUMBER ('d'|'D') ; + +BIG_DECIMAL_LITERAL : FLOATING_POINT_NUMBER ('bd'|'BD') ; + +fragment +EXPONENT : ('e'|'E') ('+'|'-')? ('0'..'9')+ ; + +CHARACTER_LITERAL + : '\'' ( ESCAPE_SEQUENCE | ~('\''|'\\') ) '\'' {setText(getText().substring(1, getText().length()-1));} + ; + +STRING_LITERAL + : '"' ( ESCAPE_SEQUENCE | ~('\\'|'"') )* '"' {setText(getText().substring(1, getText().length()-1));} + | ('\'' ( ESCAPE_SEQUENCE | ~('\\'|'\'') )* '\'')+ {setText(getText().substring(1, getText().length()-1).replace("''", "'"));} + ; + +fragment +ESCAPE_SEQUENCE + : '\\' ('b'|'t'|'n'|'f'|'r'|'\\"'|'\''|'\\') + | UNICODE_ESCAPE + | OCTAL_ESCAPE + ; + +fragment +OCTAL_ESCAPE + : '\\' ('0'..'3') ('0'..'7') ('0'..'7') + | '\\' ('0'..'7') ('0'..'7') + | '\\' ('0'..'7') + ; + +fragment +UNICODE_ESCAPE + : '\\' 'u' HEX_DIGIT HEX_DIGIT HEX_DIGIT HEX_DIGIT + ; + +// ESCAPE start tokens +TIMESTAMP_ESCAPE_START : '{ts'; +DATE_ESCAPE_START : '{d'; +TIME_ESCAPE_START : '{t'; + +EQUAL : '='; +NOT_EQUAL : '!=' | '^=' | '<>'; +GREATER : '>'; +GREATER_EQUAL : '>='; +LESS : '<'; +LESS_EQUAL : '<='; + +COMMA : ','; +DOT : '.'; +LEFT_PAREN : '('; +RIGHT_PAREN : ')'; +LEFT_BRACKET : '['; +RIGHT_BRACKET : ']'; +LEFT_BRACE : '{'; +RIGHT_BRACE : '}'; +PLUS : '+'; +MINUS : '-'; +ASTERISK : '*'; +SLASH : '/'; +PERCENT : '%'; +AMPERSAND : '&'; +SEMICOLON : ';'; +COLON : ':'; +PIPE : '|'; +DOUBLE_PIPE : '||'; +QUESTION_MARK : '?'; +ARROW : '->'; + +// Keywords +ABS : [aA] [bB] [sS]; +AS : [aA] [sS]; +ALL : [aA] [lL] [lL]; +AND : [aA] [nN] [dD]; +ANY : [aA] [nN] [yY]; +ASC : [aA] [sS] [cC]; +AVG : [aA] [vV] [gG]; +BY : [bB] [yY]; +BETWEEN : [bB] [eE] [tT] [wW] [eE] [eE] [nN]; +BIT_LENGTH : [bB] [iI] [tT] [_] [lL] [eE] [nN] [gG] [tT] [hH]; +BOTH : [bB] [oO] [tT] [hH]; +CASE : [cC] [aA] [sS] [eE]; +CAST : [cC] [aA] [sS] [tT]; +CHARACTER_LENGTH : [cC] [hH] [aA] [rR] [aA] [cC] [tT] [eE] [rR] '_' [lL] [eE] [nN] [gG] [tT] [hH]; +CLASS : [cC] [lL] [aA] [sS] [sS]; +COALESCE : [cC] [oO] [aA] [lL] [eE] [sS] [cC] [eE]; +COLLATE : [cC] [oO] [lL] [lL] [aA] [tT] [eE]; +CONCAT : [cC] [oO] [nN] [cC] [aA] [tT]; +COUNT : [cC] [oO] [uU] [nN] [tT]; +CURRENT_DATE : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [dD] [aA] [tT] [eE]; +CURRENT_TIME : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE]; +CURRENT_TIMESTAMP : [cC] [uU] [rR] [rR] [eE] [nN] [tT] '_' [tT] [iI] [mM] [eE] [sS] [tT] [aA] [mM] [pP]; +CROSS : [cC] [rR] [oO] [sS] [sS]; +DAY : [dD] [aA] [yY]; +DELETE : [dD] [eE] [lL] [eE] [tT] [eE]; +DESC : [dD] [eE] [sS] [cC]; +DISTINCT : [dD] [iI] [sS] [tT] [iI] [nN] [cC] [tT]; +ELEMENTS : [eE] [lL] [eE] [mM] [eE] [nN] [tT] [sS]; +ELSE : [eE] [lL] [sS] [eE]; +EMPTY : [eE] [mM] [pP] [tT] [yY]; +END : [eE] [nN] [dD]; +ENTRY : [eE] [nN] [tT] [rR] [yY]; +ESCAPE : [eE] [sS] [cC] [aA] [pP] [eE]; +EXISTS : [eE] [xX] [iI] [sS] [tT] [sS]; +EXTRACT : [eE] [xX] [tT] [rR] [aA] [cC] [tT]; +FETCH : [fF] [eE] [tT] [cC] [hH]; +FROM : [fF] [rR] [oO] [mM]; +FULL : [fF] [uU] [lL] [lL]; +FUNCTION : [fF] [uU] [nN] [cC] [tT] [iI] [oO] [nN]; +GROUP : [gG] [rR] [oO] [uU] [pP]; +HAVING : [hH] [aA] [vV] [iI] [nN] [gG]; +HOUR : [hH] [oO] [uU] [rR]; +IN : [iI] [nN]; +INDEX : [iI] [nN] [dD] [eE] [xX]; +INNER : [iI] [nN] [nN] [eE] [rR]; +INSERT : [iI] [nN] [sS] [eE] [rR] [tT]; +INTO : [iI] [nN] [tT] [oO]; +IS : [iI] [sS]; +JOIN : [jJ] [oO] [iI] [nN]; +KEY : [kK] [eE] [yY]; +LEADING : [lL] [eE] [aA] [dD] [iI] [nN] [gG]; +LEFT : [lL] [eE] [fF] [tT]; +LENGTH : [lL] [eE] [nN] [gG] [tT] [hH]; +LIMIT : [lL] [iI] [mM] [iI] [tT]; +LIKE : [lL] [iI] [kK] [eE]; +LIST : [lL] [iI] [sS] [tT]; +LOCATE : [lL] [oO] [cC] [aA] [tT] [eE]; +LOWER : [lL] [oO] [wW] [eE] [rR]; +MAP : [mM] [aA] [pP]; +MAX : [mM] [aA] [xX]; +MAXELEMENT : [mM] [aA] [xX] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; +MAXINDEX : [mM] [aA] [xX] [iI] [nN] [dD] [eE] [xX]; +MEMBER : [mM] [eE] [mM] [bB] [eE] [rR]; +MIN : [mM] [iI] [nN]; +MINELEMENT : [mM] [iI] [nN] [eE] [lL] [eE] [mM] [eE] [nN] [tT]; +MININDEX : [mM] [iI] [nN] [iI] [nN] [dD] [eE] [xX]; +MINUTE : [mM] [iI] [nN] [uU] [tT] [eE]; +MOD : [mM] [oO] [dD]; +MONTH : [mM] [oO] [nN] [tT] [hH]; +NEW : [nN] [eE] [wW]; +NOT : [nN] [oO] [tT]; +NULLIF : [nN] [uU] [lL] [lL] [iI] [fF]; +OBJECT : [oO] [bB] [jJ] [eE] [cC] [tT]; +OCTET_LENGTH : [oO] [cC] [tT] [eE] [tT] '_' [lL] [eE] [nN] [gG] [tT] [hH]; +OF : [oO] [fF]; +OFFSET : [oO] [fF] [fF] [sS] [eE] [tT]; +ON : [oO] [nN]; +OR : [oO] [rR]; +ORDER : [oO] [rR] [dD] [eE] [rR]; +OUTER : [oO] [uU] [tT] [eE] [rR]; +POSITION : [pP] [oO] [sS] [iI] [tT] [iI] [oO] [nN]; +RIGHT : [rR] [iI] [gG] [hH] [tT]; +SECOND : [sS] [eE] [cC] [oO] [nN] [dD]; +SELECT : [sS] [eE] [lL] [eE] [cC] [tT]; +SET : [sS] [eE] [tT]; +SIZE : [sS] [iI] [zZ] [eE]; +SQRT : [sS] [qQ] [rR] [tT]; +STR : [sS] [tT] [rR]; +SUBSTRING : [sS] [uU] [bB] [sS] [tT] [rR] [iI] [nN] [gG]; +SUBSTR : [sS] [uU] [bB] [sS] [tT] [rR]; +SUM : [sS] [uU] [mM]; +THEN : [tT] [hH] [eE] [nN]; +TIMEZONE_HOUR : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [hH] [oO] [uU] [rR]; +TIMEZONE_MINUTE : [tT] [iI] [mM] [eE] [zZ] [oO] [nN] [eE] '_' [mM] [iI] [nN] [uU] [tT] [eE]; +TRAILING : [tT] [rR] [aA] [iI] [lL] [iI] [nN] [gG]; +TREAT : [tT] [rR] [eE] [aA] [tT]; +TRIM : [tT] [rR] [iI] [mM]; +TYPE : [tT] [yY] [pP] [eE]; +UPDATE : [uU] [pP] [dD] [aA] [tT] [eE]; +UPPER : [uU] [pP] [pP] [eE] [rR]; +VALUE : [vV] [aA] [lL] [uU] [eE]; +WHEN : [wW] [hH] [eE] [nN]; +WHERE : [wW] [hH] [eE] [rR] [eE]; +WITH : [wW] [iI] [tT] [hH]; +YEAR : [yY] [eE] [aA] [rR]; + +// case-insensitive true, false and null recognition (split vote :) +TRUE : [tT] [rR] [uU] [eE]; +FALSE : [fF] [aA] [lL] [sS] [eE]; +NULL : [nN] [uU] [lL] [lL]; + +// Identifiers +IDENTIFIER + : ('a'..'z'|'A'..'Z'|'_'|'$'|'\u0080'..'\ufffe')('a'..'z'|'A'..'Z'|'_'|'$'|'0'..'9'|'\u0080'..'\ufffe')* + ; + +QUOTED_IDENTIFIER + : '`' ( ESCAPE_SEQUENCE | ~('\\'|'`') )* '`' + ; diff --git a/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4 b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4 new file mode 100644 index 0000000000000..25810b07f25ce --- /dev/null +++ b/extensions/panache/panacheql/src/main/antlr4/io/quarkus/panacheql/internal/HqlParser.g4 @@ -0,0 +1,822 @@ +parser grammar HqlParser; + +options { + tokenVocab=HqlLexer; +} + +@header { +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +} + +@members { + protected void logUseOfReservedWordAsIdentifier(Token token) { + } +} + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Statements + +statement + : ( selectStatement | updateStatement | deleteStatement | insertStatement ) EOF + ; + +selectStatement + : querySpec + ; + +deleteStatement + : DELETE FROM? entityName identificationVariableDef? whereClause? + ; + +updateStatement + : UPDATE FROM? entityName identificationVariableDef? setClause whereClause? + ; + +setClause + : SET assignment+ + ; + +assignment + : dotIdentifierSequence EQUAL expression + ; + +insertStatement +// todo (6.0 : VERSIONED + : INSERT insertSpec querySpec + ; + +insertSpec + : intoSpec targetFieldsSpec + ; + +intoSpec + : INTO entityName + ; + +targetFieldsSpec + : + LEFT_PAREN dotIdentifierSequence (COMMA dotIdentifierSequence)* RIGHT_PAREN + ; + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// QUERY SPEC - general structure of root sqm or sub sqm + +querySpec + : selectClause? fromClause whereClause? ( groupByClause havingClause? )? orderByClause? limitClause? offsetClause? + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// FROM clause + +fromClause + : FROM fromClauseSpace (COMMA fromClauseSpace)* + ; + +fromClauseSpace + : pathRoot ( crossJoin | jpaCollectionJoin | qualifiedJoin )* + ; + +pathRoot + : entityName (identificationVariableDef)? + ; + +/** + * Rule for dotIdentifierSequence where we expect an entity-name. The extra + * "rule layer" allows the walker to specially handle such a case (to use a special + * org.hibernate.query.hql.DotIdentifierConsumer, etc) + */ +entityName + : dotIdentifierSequence + ; + +identificationVariableDef + : (AS identifier) + | IDENTIFIER + ; + +crossJoin + : CROSS JOIN pathRoot (identificationVariableDef)? + ; + +jpaCollectionJoin + : COMMA IN LEFT_PAREN path RIGHT_PAREN (identificationVariableDef)? + ; + +qualifiedJoin + : joinTypeQualifier JOIN FETCH? qualifiedJoinRhs (qualifiedJoinPredicate)? + ; + +joinTypeQualifier + : INNER? + | (LEFT|RIGHT|FULL)? OUTER? + ; + +qualifiedJoinRhs + : path (identificationVariableDef)? + ; + +qualifiedJoinPredicate + : (ON | WITH) predicate + ; + + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// SELECT clause + +selectClause + : SELECT DISTINCT? selectionList + ; + +selectionList + : selection (COMMA selection)* + ; + +selection + : selectExpression (resultIdentifier)? + ; + +selectExpression + : dynamicInstantiation + | jpaSelectObjectSyntax + | mapEntrySelection + | expression + ; + +resultIdentifier + : (AS identifier) + | IDENTIFIER + ; + + +mapEntrySelection + : ENTRY LEFT_PAREN path RIGHT_PAREN + ; + +dynamicInstantiation + : NEW dynamicInstantiationTarget LEFT_PAREN dynamicInstantiationArgs RIGHT_PAREN + ; + +dynamicInstantiationTarget + : LIST + | MAP + | dotIdentifierSequence + ; + +dynamicInstantiationArgs + : dynamicInstantiationArg ( COMMA dynamicInstantiationArg )* + ; + +dynamicInstantiationArg + : dynamicInstantiationArgExpression (AS? identifier)? + ; + +dynamicInstantiationArgExpression + : expression + | dynamicInstantiation + ; + +jpaSelectObjectSyntax + : OBJECT LEFT_PAREN identifier RIGHT_PAREN + ; + + + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Path structures + +dotIdentifierSequence + : identifier dotIdentifierSequenceContinuation* + ; + +dotIdentifierSequenceContinuation + : DOT identifier + ; + + +/** + * A path which needs to be resolved semantically. This recognizes + * any path-like structure. Generally, the path is semantically + * interpreted by the consumer of the parse-tree. However, there + * are certain cases where we can syntactically recognize a navigable + * path; see `syntacticNavigablePath` rule + */ +path + : syntacticDomainPath (pathContinuation)? + | generalPathFragment + ; + +pathContinuation + : DOT dotIdentifierSequence (DOT pathContinuation)? + ; + +/** + * Rule for cases where we syntactically know that the path is a + * "domain path" because it is one of these special cases: + * + * * TREAT( path ) + * * ELEMENTS( path ) + * * VALUE( path ) + * * KEY( path ) + * * path[ selector ] + */ +syntacticDomainPath + : treatedNavigablePath + | collectionElementNavigablePath + | mapKeyNavigablePath + | dotIdentifierSequence indexedPathAccessFragment + ; + +/** + * The main path rule. Recognition for all normal path structures including + * class, field and enum references as well as navigable paths. + * + * NOTE : this rule does *not* cover the special syntactic navigable path + * cases: TREAT, KEY, ELEMENTS, VALUES + */ +generalPathFragment + : dotIdentifierSequence (indexedPathAccessFragment)? + ; + +indexedPathAccessFragment + : LEFT_BRACKET expression RIGHT_BRACKET (DOT generalPathFragment)? + ; + +treatedNavigablePath + : TREAT LEFT_PAREN path AS dotIdentifierSequence RIGHT_PAREN (pathContinuation)? + ; + +collectionElementNavigablePath + : (VALUE | ELEMENTS) LEFT_PAREN path RIGHT_PAREN (pathContinuation)? + ; + +mapKeyNavigablePath + : KEY LEFT_PAREN path RIGHT_PAREN (pathContinuation)? + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// GROUP BY clause + +groupByClause + : GROUP BY groupingSpecification + ; + +groupingSpecification + : groupingValue ( COMMA groupingValue )* + ; + +groupingValue + : expression collationSpecification? + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +//HAVING clause + +havingClause + : HAVING predicate + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// ORDER BY clause + +orderByClause +// todo (6.0) : null precedence + : ORDER BY sortSpecification (COMMA sortSpecification)* + ; + +sortSpecification + : expression collationSpecification? orderingSpecification? + ; + +collationSpecification + : COLLATE collateName + ; + +collateName + : dotIdentifierSequence + ; + +orderingSpecification + : ASC + | DESC + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// LIMIT/OFFSET clause + +limitClause + : LIMIT parameterOrNumberLiteral + ; + +offsetClause + : OFFSET parameterOrNumberLiteral + ; + +parameterOrNumberLiteral + : parameter + | INTEGER_LITERAL + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// WHERE clause & Predicates + +whereClause + : WHERE predicate + ; + +predicate + : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate + | predicate OR predicate # OrPredicate + | predicate AND predicate # AndPredicate + | NOT predicate # NegatedPredicate + | expression IS (NOT)? NULL # IsNullPredicate + | expression IS (NOT)? EMPTY # IsEmptyPredicate + | expression EQUAL expression # EqualityPredicate + | expression NOT_EQUAL expression # InequalityPredicate + | expression GREATER expression # GreaterThanPredicate + | expression GREATER_EQUAL expression # GreaterThanOrEqualPredicate + | expression LESS expression # LessThanPredicate + | expression LESS_EQUAL expression # LessThanOrEqualPredicate + | expression (NOT)? IN inList # InPredicate + | expression (NOT)? BETWEEN expression AND expression # BetweenPredicate + | expression (NOT)? LIKE expression (likeEscape)? # LikePredicate + | MEMBER OF path # MemberOfPredicate + ; + +inList + : ELEMENTS? LEFT_PAREN dotIdentifierSequence RIGHT_PAREN # PersistentCollectionReferenceInList + | LEFT_PAREN expression (COMMA expression)* RIGHT_PAREN # ExplicitTupleInList + | expression # SubQueryInList + ; + +likeEscape + : ESCAPE expression + ; + + +// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +// Expression + +expression + : expression DOUBLE_PIPE expression # ConcatenationExpression + | expression PLUS expression # AdditionExpression + | expression MINUS expression # SubtractionExpression + | expression ASTERISK expression # MultiplicationExpression + | expression SLASH expression # DivisionExpression + | expression PERCENT expression # ModuloExpression + // todo (6.0) : should these unary plus/minus rules only apply to literals? + // if so, move the MINUS / PLUS recognition to the `literal` rule + // specificcally for numeric literals + | MINUS expression # UnaryMinusExpression + | PLUS expression # UnaryPlusExpression + | caseStatement # CaseExpression + | coalesce # CoalesceExpression + | nullIf # NullIfExpression + | literal # LiteralExpression + | parameter # ParameterExpression + | entityTypeReference # EntityTypeExpression + | path # PathExpression + | function # FunctionExpression + | LEFT_PAREN querySpec RIGHT_PAREN # SubQueryExpression + ; + +entityTypeReference + : TYPE LEFT_PAREN (path | parameter) RIGHT_PAREN + ; + +caseStatement + : simpleCaseStatement + | searchedCaseStatement + ; + +simpleCaseStatement + : CASE expression (simpleCaseWhen)+ (caseOtherwise)? END + ; + +simpleCaseWhen + : WHEN expression THEN expression + ; + +caseOtherwise + : ELSE expression + ; + +searchedCaseStatement + : CASE (searchedCaseWhen)+ (caseOtherwise)? END + ; + +searchedCaseWhen + : WHEN predicate THEN expression + ; + +coalesce + : COALESCE LEFT_PAREN expression (COMMA expression)+ RIGHT_PAREN + ; + +nullIf + : NULLIF LEFT_PAREN expression COMMA expression RIGHT_PAREN + ; + +literal + : STRING_LITERAL + | CHARACTER_LITERAL + | INTEGER_LITERAL + | LONG_LITERAL + | BIG_INTEGER_LITERAL + | FLOAT_LITERAL + | DOUBLE_LITERAL + | BIG_DECIMAL_LITERAL + | HEX_LITERAL + | OCTAL_LITERAL + | NULL + | TRUE + | FALSE + | timestampLiteral + | dateLiteral + | timeLiteral + ; + +// todo (6.0) : expand temporal literal support to Java 8 temporal types +// * Instant -> {instant '...'} +// * LocalDate -> {localDate '...'} +// * LocalDateTime -> {localDateTime '...'} +// * OffsetDateTime -> {offsetDateTime '...'} +// * OffsetTime -> {offsetTime '...'} +// * ZonedDateTime -> {localDate '...'} +// * ... +// +// Few things: +// 1) the markers above are just initial thoughts. They are obviously verbose. Maybe acronyms or shortened forms would be better +// 2) we may want to stay away from all of the timezone headaches by not supporting local, zoned and offset forms + +timestampLiteral + : TIMESTAMP_ESCAPE_START dateTimeLiteralText RIGHT_BRACE + ; + +dateLiteral + : DATE_ESCAPE_START dateTimeLiteralText RIGHT_BRACE + ; + +timeLiteral + : TIME_ESCAPE_START dateTimeLiteralText RIGHT_BRACE + ; + +dateTimeLiteralText + : STRING_LITERAL | CHARACTER_LITERAL + ; + +parameter + : COLON identifier # NamedParameter + | QUESTION_MARK INTEGER_LITERAL? # PositionalParameter + ; + +function + : standardFunction + | aggregateFunction + | jpaCollectionFunction + | hqlCollectionFunction + | jpaNonStandardFunction + | nonStandardFunction + ; + +jpaNonStandardFunction + : FUNCTION LEFT_PAREN jpaNonStandardFunctionName (COMMA nonStandardFunctionArguments)? RIGHT_PAREN + ; + +jpaNonStandardFunctionName + : STRING_LITERAL + ; + +nonStandardFunction + : nonStandardFunctionName LEFT_PAREN nonStandardFunctionArguments? RIGHT_PAREN + ; + +nonStandardFunctionName + : dotIdentifierSequence + ; + +nonStandardFunctionArguments + : expression (COMMA expression)* + ; + +jpaCollectionFunction + : SIZE LEFT_PAREN path RIGHT_PAREN # CollectionSizeFunction + | INDEX LEFT_PAREN identifier RIGHT_PAREN # CollectionIndexFunction + ; + +hqlCollectionFunction + : MAXINDEX LEFT_PAREN path RIGHT_PAREN # MaxIndexFunction + | MAXELEMENT LEFT_PAREN path RIGHT_PAREN # MaxElementFunction + | MININDEX LEFT_PAREN path RIGHT_PAREN # MinIndexFunction + | MINELEMENT LEFT_PAREN path RIGHT_PAREN # MinElementFunction + ; + +aggregateFunction + : avgFunction + | sumFunction + | minFunction + | maxFunction + | countFunction + ; + +avgFunction + : AVG LEFT_PAREN DISTINCT? expression RIGHT_PAREN + ; + +sumFunction + : SUM LEFT_PAREN DISTINCT? expression RIGHT_PAREN + ; + +minFunction + : MIN LEFT_PAREN DISTINCT? expression RIGHT_PAREN + ; + +maxFunction + : MAX LEFT_PAREN DISTINCT? expression RIGHT_PAREN + ; + +countFunction + : COUNT LEFT_PAREN DISTINCT? (expression | ASTERISK) RIGHT_PAREN + ; + +standardFunction + : castFunction + | concatFunction + | substringFunction + | trimFunction + | upperFunction + | lowerFunction + | lengthFunction + | locateFunction + | absFunction + | sqrtFunction + | modFunction + | strFunction + | currentDateFunction + | currentTimeFunction + | currentTimestampFunction + | extractFunction + | positionFunction + | charLengthFunction + | octetLengthFunction + | bitLengthFunction + ; + + +castFunction + : CAST LEFT_PAREN expression AS castTarget RIGHT_PAREN + ; + +castTarget + // todo (6.0) : should allow either + // - named cast (IDENTIFIER) + // - JavaTypeDescriptorRegistry (imported) key + // - java.sql.Types field NAME (alias for its value as a coded cast) + // - "pass through" + // - coded cast (INTEGER_LITERAL) + // - SqlTypeDescriptorRegistry key + : IDENTIFIER + ; + +concatFunction + : CONCAT LEFT_PAREN expression (COMMA expression)+ RIGHT_PAREN + ; + +substringFunction + : (SUBSTRING | SUBSTR) LEFT_PAREN expression COMMA substringFunctionStartArgument (COMMA substringFunctionLengthArgument)? RIGHT_PAREN + ; + +substringFunctionStartArgument + : expression + ; + +substringFunctionLengthArgument + : expression + ; + +trimFunction + : TRIM LEFT_PAREN trimSpecification? trimCharacter? FROM? expression RIGHT_PAREN + ; + +trimSpecification + : LEADING + | TRAILING + | BOTH + ; + +trimCharacter + : CHARACTER_LITERAL | STRING_LITERAL + ; + +upperFunction + : UPPER LEFT_PAREN expression RIGHT_PAREN + ; + +lowerFunction + : LOWER LEFT_PAREN expression RIGHT_PAREN + ; + +lengthFunction + : LENGTH LEFT_PAREN expression RIGHT_PAREN + ; + +locateFunction + : LOCATE LEFT_PAREN locateFunctionSubstrArgument COMMA locateFunctionStringArgument (COMMA locateFunctionStartArgument)? RIGHT_PAREN + ; + +locateFunctionSubstrArgument + : expression + ; + +locateFunctionStringArgument + : expression + ; + +locateFunctionStartArgument + : expression + ; + +absFunction + : ABS LEFT_PAREN expression RIGHT_PAREN + ; + +sqrtFunction + : SQRT LEFT_PAREN expression RIGHT_PAREN + ; + +modFunction + : MOD LEFT_PAREN modDividendArgument COMMA modDivisorArgument RIGHT_PAREN + ; + +strFunction + : STR LEFT_PAREN expression RIGHT_PAREN + ; + +modDividendArgument + : expression + ; + +modDivisorArgument + : expression + ; + +currentDateFunction + : CURRENT_DATE (LEFT_PAREN RIGHT_PAREN)? + ; + +currentTimeFunction + : CURRENT_TIME (LEFT_PAREN RIGHT_PAREN)? + ; + +currentTimestampFunction + : CURRENT_TIMESTAMP (LEFT_PAREN RIGHT_PAREN)? + ; + +extractFunction + : EXTRACT LEFT_PAREN extractField FROM expression RIGHT_PAREN + ; + +extractField + : datetimeField + | timeZoneField + ; + +datetimeField + : nonSecondDatetimeField + | SECOND + ; + +nonSecondDatetimeField + : YEAR + | MONTH + | DAY + | HOUR + | MINUTE + ; + +timeZoneField + : TIMEZONE_HOUR + | TIMEZONE_MINUTE + ; + +positionFunction + : POSITION LEFT_PAREN positionSubstrArgument IN positionStringArgument RIGHT_PAREN + ; + +positionSubstrArgument + : expression + ; + +positionStringArgument + : expression + ; + +charLengthFunction + : CAST LEFT_PAREN expression RIGHT_PAREN + ; + +octetLengthFunction + : OCTET_LENGTH LEFT_PAREN expression RIGHT_PAREN + ; + +bitLengthFunction + : BIT_LENGTH LEFT_PAREN expression RIGHT_PAREN + ; + +/** + * The `identifier` is used to provide "keyword as identifier" handling. + * + * The lexer hands us recognized keywords using their specific tokens. This is important + * for the recognition of sqm structure, especially in terms of performance! + * + * However we want to continue to allow users to use mopst keywords as identifiers (e.g., attribute names). + * This parser rule helps with that. Here we expect that the caller already understands their + * context enough to know that keywords-as-identifiers are allowed. + */ +identifier + : IDENTIFIER + | (ABS + | ALL + | AND + | ANY + | AS + | ASC + | AVG + | BY + | BETWEEN + | BIT_LENGTH + | BOTH + | CAST + | COALESCE + | COLLATE + | CONCAT + | COUNT + | CROSS + | DAY + | DELETE + | DESC + | DISTINCT + | ELEMENTS + | ENTRY + | FROM + | FULL + | FUNCTION + | GROUP + | HOUR + | IN + | INDEX + | INNER + | INSERT + | JOIN + | KEY + | LEADING + | LEFT + | LENGTH + | LIKE + | LIST + | LOWER + | MAP + | MAX + | MIN + | MINUTE + | MEMBER + | MONTH + | OBJECT + | ON + | OR + | ORDER + | OUTER + | POSITION + | RIGHT + | SELECT + | SECOND + | SET + | SQRT + | STR + | SUBSTRING + | SUM + | TRAILING + | TREAT + | UPDATE + | UPPER + | VALUE + | WHERE + | WITH + | YEAR) { + logUseOfReservedWordAsIdentifier(getCurrentToken()); + } + ; + diff --git a/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java b/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java new file mode 100644 index 0000000000000..495629e822eac --- /dev/null +++ b/extensions/panache/panacheql/src/test/java/io/quarkus/panacheql/LexerTest.java @@ -0,0 +1,66 @@ +package io.quarkus.panacheql; + +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import io.quarkus.panacheql.internal.HqlLexer; +import io.quarkus.panacheql.internal.HqlParser; +import io.quarkus.panacheql.internal.HqlParser.AndPredicateContext; +import io.quarkus.panacheql.internal.HqlParser.EqualityPredicateContext; +import io.quarkus.panacheql.internal.HqlParser.IsNullPredicateContext; +import io.quarkus.panacheql.internal.HqlParser.LiteralExpressionContext; +import io.quarkus.panacheql.internal.HqlParser.PathExpressionContext; +import io.quarkus.panacheql.internal.HqlParser.PredicateContext; +import io.quarkus.panacheql.internal.HqlParserBaseVisitor; + +/** + * Unit test for simple App. + */ +public class LexerTest { + + @Test + public void test() { + HqlLexer lexer = new HqlLexer(CharStreams.fromString("bar = 1 AND gee is not null")); + CommonTokenStream tokens = new CommonTokenStream(lexer); + HqlParser parser = new HqlParser(tokens); + PredicateContext predicate = parser.predicate(); + HqlParserBaseVisitor visitor = new HqlParserBaseVisitor() { + @Override + public String visitAndPredicate(AndPredicateContext ctx) { + StringBuilder sb = new StringBuilder(); + for (PredicateContext predicate : ctx.predicate()) { + if (sb.length() > 0) + sb.append(" && "); + sb.append(predicate.accept(this)); + } + return sb.toString(); + } + + @Override + public String visitIsNullPredicate(IsNullPredicateContext ctx) { + String expr = ctx.expression().accept(this); + if (ctx.NOT() != null) + return expr + " != null"; + return expr + " == null"; + } + + @Override + public String visitEqualityPredicate(EqualityPredicateContext ctx) { + return ctx.expression(0).accept(this) + " == " + ctx.expression(1).accept(this); + } + + @Override + public String visitLiteralExpression(LiteralExpressionContext ctx) { + return ctx.getText(); + } + + @Override + public String visitPathExpression(PathExpressionContext ctx) { + return ctx.getText(); + } + }; + Assertions.assertEquals("bar == 1 && gee != null", predicate.accept(visitor)); + } +} diff --git a/extensions/panache/pom.xml b/extensions/panache/pom.xml index c0ec7615555b0..b783128ac96b4 100644 --- a/extensions/panache/pom.xml +++ b/extensions/panache/pom.xml @@ -16,6 +16,8 @@ panache-common hibernate-orm-panache + mongodb-panache + panacheql diff --git a/extensions/resteasy-common/deployment/pom.xml b/extensions/resteasy-common/deployment/pom.xml index b8edfbd22fd7d..7f04f1644cfac 100644 --- a/extensions/resteasy-common/deployment/pom.xml +++ b/extensions/resteasy-common/deployment/pom.xml @@ -22,6 +22,10 @@ io.quarkus quarkus-resteasy-common + + io.quarkus + quarkus-resteasy-common-spi + io.quarkus quarkus-arc-deployment diff --git a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java index 6f3342e058066..6c123101f925a 100644 --- a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java +++ b/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyCommonProcessor.java @@ -34,6 +34,7 @@ import io.quarkus.deployment.builditem.CombinedIndexBuildItem; import io.quarkus.deployment.builditem.substrate.ReflectiveClassBuildItem; import io.quarkus.deployment.util.ServiceUtil; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; import io.quarkus.runtime.annotations.ConfigRoot; diff --git a/extensions/resteasy-common/pom.xml b/extensions/resteasy-common/pom.xml index 71a0e1387611d..6005c0e254b92 100644 --- a/extensions/resteasy-common/pom.xml +++ b/extensions/resteasy-common/pom.xml @@ -16,6 +16,7 @@ deployment runtime + spi diff --git a/extensions/resteasy-common/spi/pom.xml b/extensions/resteasy-common/spi/pom.xml new file mode 100644 index 0000000000000..fc0ce101024c4 --- /dev/null +++ b/extensions/resteasy-common/spi/pom.xml @@ -0,0 +1,23 @@ + + + + quarkus-resteasy-common-parent + io.quarkus + 999-SNAPSHOT + ../ + + 4.0.0 + + quarkus-resteasy-common-spi + Quarkus - RESTEasy - Common - SPI + + + + io.quarkus + quarkus-core-deployment + + + + \ No newline at end of file diff --git a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyJaxrsProviderBuildItem.java b/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyJaxrsProviderBuildItem.java similarity index 88% rename from extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyJaxrsProviderBuildItem.java rename to extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyJaxrsProviderBuildItem.java index cbdc3c9d6c3f2..bcb598fb6733a 100644 --- a/extensions/resteasy-common/deployment/src/main/java/io/quarkus/resteasy/common/deployment/ResteasyJaxrsProviderBuildItem.java +++ b/extensions/resteasy-common/spi/src/main/java/io/quarkus/resteasy/common/spi/ResteasyJaxrsProviderBuildItem.java @@ -1,4 +1,4 @@ -package io.quarkus.resteasy.common.deployment; +package io.quarkus.resteasy.common.spi; import io.quarkus.builder.item.MultiBuildItem; diff --git a/extensions/resteasy-jackson/deployment/src/main/java/io/quarkus/resteasy/jackson/deployment/ResteasyJacksonProcessor.java b/extensions/resteasy-jackson/deployment/src/main/java/io/quarkus/resteasy/jackson/deployment/ResteasyJacksonProcessor.java index e7c0d23065707..ec61fbbcb35d0 100644 --- a/extensions/resteasy-jackson/deployment/src/main/java/io/quarkus/resteasy/jackson/deployment/ResteasyJacksonProcessor.java +++ b/extensions/resteasy-jackson/deployment/src/main/java/io/quarkus/resteasy/jackson/deployment/ResteasyJacksonProcessor.java @@ -40,7 +40,7 @@ import io.quarkus.gizmo.MethodCreator; import io.quarkus.gizmo.MethodDescriptor; import io.quarkus.gizmo.ResultHandle; -import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.jackson.runtime.ObjectMapperProducer; public class ResteasyJacksonProcessor { @@ -50,7 +50,7 @@ public class ResteasyJacksonProcessor { private static final String QUARKUS_CONTEXT_RESOLVER_NAME = "io.quarkus.resteasy.jackson.runtime.QuarkusObjectMapperContextResolver"; - @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) + @BuildStep(providesCapabilities = { Capabilities.RESTEASY_JSON_EXTENSION, "io.quarkus.resteasy.jackson" }) void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JACKSON)); } diff --git a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java index 724cc4c022501..df3fa77b7dac1 100755 --- a/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java +++ b/extensions/resteasy-jsonb/deployment/src/main/java/io/quarkus/resteasy/jsonb/deployment/ResteasyJsonbProcessor.java @@ -7,7 +7,7 @@ public class ResteasyJsonbProcessor { - @BuildStep(providesCapabilities = Capabilities.RESTEASY_JSON_EXTENSION) + @BuildStep(providesCapabilities = { Capabilities.RESTEASY_JSON_EXTENSION, "io.quarkus.resteasy.jsonb" }) void build(BuildProducer feature) { feature.produce(new FeatureBuildItem(FeatureBuildItem.RESTEASY_JSONB)); } diff --git a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java index e5a65e31d36b0..c93f2a0b09abc 100644 --- a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java +++ b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyBuiltinsProcessor.java @@ -9,7 +9,7 @@ import io.quarkus.deployment.annotations.BuildProducer; import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.annotations.Record; -import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.runtime.ExceptionMapperRecorder; import io.quarkus.resteasy.runtime.NotFoundExceptionMapper; import io.quarkus.resteasy.runtime.RolesFilterRegistrar; diff --git a/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java b/extensions/resteasy/deployment/src/main/java/io/quarkus/resteasy/deployment/ResteasyServletProcessor.java old mode 100755 new mode 100644 diff --git a/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java b/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java index 6febabe68d625..6708816411164 100644 --- a/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java +++ b/extensions/smallrye-opentracing/deployment/src/main/java/io/quarkus/smallrye/opentracing/deployment/SmallRyeOpenTracingProcessor.java @@ -12,7 +12,7 @@ import io.quarkus.deployment.annotations.BuildStep; import io.quarkus.deployment.builditem.FeatureBuildItem; import io.quarkus.deployment.builditem.substrate.ReflectiveMethodBuildItem; -import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.smallrye.opentracing.runtime.QuarkusSmallRyeTracingDynamicFeature; import io.quarkus.smallrye.opentracing.runtime.TracerProducer; import io.quarkus.undertow.deployment.FilterBuildItem; diff --git a/extensions/spring-web/deployment/pom.xml b/extensions/spring-web/deployment/pom.xml index 8a706bf36306d..050a0525bf058 100644 --- a/extensions/spring-web/deployment/pom.xml +++ b/extensions/spring-web/deployment/pom.xml @@ -27,6 +27,10 @@ io.quarkus quarkus-resteasy-jackson-deployment + + io.quarkus + quarkus-resteasy-common-spi + io.quarkus quarkus-undertow-deployment diff --git a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java index cddd1c4f3e940..5aa359f549a38 100644 --- a/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java +++ b/extensions/spring-web/deployment/src/main/java/io/quarkus/spring/web/deployment/SpringWebProcessor.java @@ -37,7 +37,7 @@ import io.quarkus.deployment.util.ServiceUtil; import io.quarkus.gizmo.ClassOutput; import io.quarkus.resteasy.common.deployment.ResteasyCommonProcessor; -import io.quarkus.resteasy.common.deployment.ResteasyJaxrsProviderBuildItem; +import io.quarkus.resteasy.common.spi.ResteasyJaxrsProviderBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceDefiningAnnotationBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodAnnotationsBuildItem; import io.quarkus.resteasy.server.common.spi.AdditionalJaxRsResourceMethodParamAnnotations; diff --git a/integration-tests/mongodb-panache/pom.xml b/integration-tests/mongodb-panache/pom.xml new file mode 100755 index 0000000000000..6829b18b31559 --- /dev/null +++ b/integration-tests/mongodb-panache/pom.xml @@ -0,0 +1,146 @@ + + + 4.0.0 + + + io.quarkus + quarkus-integration-tests-parent + 999-SNAPSHOT + + + quarkus-integration-test-mongodb-panache + + Quarkus - Integration Tests - MongoDB Panache + + + + io.quarkus + quarkus-resteasy + + + io.quarkus + quarkus-resteasy-jsonb + + + io.quarkus + quarkus-mongodb-panache + + + io.quarkus + quarkus-mongodb-client + + + + io.quarkus + quarkus-junit5 + test + + + io.rest-assured + rest-assured + test + + + de.flapdoodle.embed + de.flapdoodle.embed.mongo + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jdk8 + test + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + test + + + + + + + src/main/resources + true + + + + + maven-failsafe-plugin + + true + + + + io.quarkus + quarkus-maven-plugin + ${project.version} + + + + build + + + + + + + + + + native-image + + + native + + + + + + org.apache.maven.plugins + maven-failsafe-plugin + + + + integration-test + verify + + + + + ${project.build.directory}/${project.build.finalName}-runner + + + + + + + false + + + + ${project.groupId} + quarkus-maven-plugin + ${project.version} + + + native-image + + native-image + + + true + true + ${graalvmHome} + + + + + + + + + + diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/Book.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/Book.java new file mode 100644 index 0000000000000..653426b12d58b --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/Book.java @@ -0,0 +1,90 @@ +package io.quarkus.it.mongodb.panache.book; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import javax.json.bind.annotation.JsonbDateFormat; + +import org.bson.codecs.pojo.annotations.BsonIgnore; +import org.bson.codecs.pojo.annotations.BsonProperty; +import org.bson.types.ObjectId; + +import io.quarkus.mongodb.panache.MongoEntity; + +@MongoEntity(collection = "TheBook") +public class Book { + @BsonProperty("bookTitle") + private String title; + private String author; + private ObjectId id; + @BsonIgnore + private String transientDescription; + @JsonbDateFormat("yyyy-MM-dd") + private LocalDate creationDate; + + private List categories = new ArrayList<>(); + + private BookDetail details; + + public ObjectId getId() { + return id; + } + + public void setId(ObjectId id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public Book setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public Book setAuthor(String author) { + this.author = author; + return this; + } + + public List getCategories() { + return categories; + } + + public Book setCategories(List categories) { + this.categories = categories; + return this; + } + + public BookDetail getDetails() { + return details; + } + + public Book setDetails(BookDetail details) { + this.details = details; + return this; + } + + public String getTransientDescription() { + return transientDescription; + } + + public Book setTransientDescription(String transientDescription) { + this.transientDescription = transientDescription; + return this; + } + + public LocalDate getCreationDate() { + return creationDate; + } + + public void setCreationDate(LocalDate creationDate) { + this.creationDate = creationDate; + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookDetail.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookDetail.java new file mode 100644 index 0000000000000..691985723eb3d --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookDetail.java @@ -0,0 +1,26 @@ +package io.quarkus.it.mongodb.panache.book; + +public class BookDetail { + + private String summary; + + private int rating; + + public String getSummary() { + return summary; + } + + public BookDetail setSummary(String summary) { + this.summary = summary; + return this; + } + + public int getRating() { + return rating; + } + + public BookDetail setRating(int rating) { + this.rating = rating; + return this; + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntity.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntity.java new file mode 100644 index 0000000000000..b8958b4f3f986 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntity.java @@ -0,0 +1,80 @@ +package io.quarkus.it.mongodb.panache.book; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; + +import javax.json.bind.annotation.JsonbDateFormat; + +import org.bson.codecs.pojo.annotations.BsonIgnore; +import org.bson.codecs.pojo.annotations.BsonProperty; + +import io.quarkus.mongodb.panache.MongoEntity; +import io.quarkus.mongodb.panache.PanacheMongoEntity; + +@MongoEntity(collection = "TheBookEntity") +public class BookEntity extends PanacheMongoEntity { + @BsonProperty("bookTitle") + private String title; + private String author; + @BsonIgnore + private String transientDescription; + @JsonbDateFormat("yyyy-MM-dd") + private LocalDate creationDate; + + private List categories = new ArrayList<>(); + + private BookDetail details; + + public String getTitle() { + return title; + } + + public BookEntity setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public BookEntity setAuthor(String author) { + this.author = author; + return this; + } + + public List getCategories() { + return categories; + } + + public BookEntity setCategories(List categories) { + this.categories = categories; + return this; + } + + public BookDetail getDetails() { + return details; + } + + public BookEntity setDetails(BookDetail details) { + this.details = details; + return this; + } + + public String getTransientDescription() { + return transientDescription; + } + + public void setTransientDescription(String transientDescription) { + this.transientDescription = transientDescription; + } + + public LocalDate getCreationDate() { + return creationDate; + } + + public void setCreationDate(LocalDate creationDate) { + this.creationDate = creationDate; + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntityResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntityResource.java new file mode 100644 index 0000000000000..9bd49d3544486 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookEntityResource.java @@ -0,0 +1,105 @@ +package io.quarkus.it.mongodb.panache.book; + +import java.net.URI; +import java.time.LocalDate; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.bson.types.ObjectId; +import org.jboss.logging.Logger; + +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +@Path("/books/entity") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class BookEntityResource { + private static final Logger LOGGER = Logger.getLogger(BookEntityResource.class); + + @PostConstruct + void init() { + String databaseName = BookEntity.mongoDatabase().getName(); + String collectionName = BookEntity.mongoCollection().getNamespace().getCollectionName(); + LOGGER.infov("Using BookEntity[database={0}, collection={1}]", databaseName, collectionName); + } + + @GET + public List getBooks(@QueryParam("sort") String sort) { + if (sort != null) { + return BookEntity.listAll(Sort.ascending(sort)); + } + return BookEntity.listAll(); + } + + @POST + public Response addBook(BookEntity book) { + book.persist(); + String id = book.id.toString(); + return Response.created(URI.create("/books/entity/" + id)).build(); + } + + @PUT + public Response updateBook(BookEntity book) { + book.update(); + return Response.accepted().build(); + } + + // PATCH is not correct here but it allows to test persistOrUpdate without a specific subpath + @PATCH + public Response upsertBook(BookEntity book) { + book.persistOrUpdate(); + return Response.accepted().build(); + } + + @DELETE + @Path("/{id}") + public void deleteBook(@PathParam("id") String id) { + BookEntity theBook = BookEntity.findById(new ObjectId(id)); + theBook.delete(); + } + + @GET + @Path("/{id}") + public BookEntity getBook(@PathParam("id") String id) { + return BookEntity.findById(new ObjectId(id)); + } + + @GET + @Path("/search/{author}") + public List getBooksByAuthor(@PathParam("author") String author) { + return BookEntity.list("author", author); + } + + @GET + @Path("/search") + public BookEntity search(@QueryParam("author") String author, @QueryParam("title") String title, + @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo) { + if (author != null) { + return BookEntity.find("{'author': ?1,'bookTitle': ?2}", author, title).firstResult(); + } + + return BookEntity + .find("{'creationDate': {$gte: ?1}, 'creationDate': {$lte: ?2}}", LocalDate.parse(dateFrom), + LocalDate.parse(dateTo)) + .firstResult(); + } + + @GET + @Path("/search2") + public BookEntity search2(@QueryParam("author") String author, @QueryParam("title") String title, + @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo) { + if (author != null) { + return BookEntity.find("{'author': :author,'bookTitle': :title}", + Parameters.with("author", author).and("title", title)).firstResult(); + } + + return BookEntity.find("{'creationDate': {$gte: :dateFrom}, 'creationDate': {$lte: :dateTo}}", + Parameters.with("dateFrom", LocalDate.parse(dateFrom)).and("dateTo", LocalDate.parse(dateTo))).firstResult(); + } + +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepository.java new file mode 100644 index 0000000000000..d7f35d2a2b25b --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepository.java @@ -0,0 +1,9 @@ +package io.quarkus.it.mongodb.panache.book; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepository; + +@ApplicationScoped +public class BookRepository implements PanacheMongoRepository { +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepositoryResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepositoryResource.java new file mode 100644 index 0000000000000..8f042e1358fc4 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/book/BookRepositoryResource.java @@ -0,0 +1,108 @@ +package io.quarkus.it.mongodb.panache.book; + +import java.net.URI; +import java.time.LocalDate; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import org.bson.types.ObjectId; +import org.jboss.logging.Logger; + +import io.quarkus.panache.common.Parameters; +import io.quarkus.panache.common.Sort; + +@Path("/books/repository") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class BookRepositoryResource { + private static final Logger LOGGER = Logger.getLogger(BookRepositoryResource.class); + @Inject + BookRepository bookRepository; + + @PostConstruct + void init() { + String databaseName = bookRepository.mongoDatabase().getName(); + String collectionName = bookRepository.mongoCollection().getNamespace().getCollectionName(); + LOGGER.infov("Using BookRepository[database={0}, collection={1}]", databaseName, collectionName); + } + + @GET + public List getBooks(@QueryParam("sort") String sort) { + if (sort != null) { + return bookRepository.listAll(Sort.ascending(sort)); + } + return bookRepository.listAll(); + } + + @POST + public Response addBook(Book book) { + bookRepository.persist(book); + String id = book.getId().toString(); + return Response.created(URI.create("/books/entity" + id)).build(); + } + + @PUT + public Response updateBook(Book book) { + bookRepository.update(book); + return Response.accepted().build(); + } + + // PATCH is not correct here but it allows to test persistOrUpdate without a specific subpath + @PATCH + public Response upsertBook(Book book) { + bookRepository.persistOrUpdate(book); + return Response.accepted().build(); + } + + @DELETE + @Path("/{id}") + public void deleteBook(@PathParam("id") String id) { + Book theBook = bookRepository.findById(new ObjectId(id)); + bookRepository.delete(theBook); + } + + @GET + @Path("/{id}") + public Book getBook(@PathParam("id") String id) { + return bookRepository.findById(new ObjectId(id)); + } + + @GET + @Path("/search/{author}") + public List getBooksByAuthor(@PathParam("author") String author) { + return bookRepository.list("author", author); + } + + @GET + @Path("/search") + public Book search(@QueryParam("author") String author, @QueryParam("title") String title, + @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo) { + if (author != null) { + return bookRepository.find("{'author': ?1,'bookTitle': ?2}", author, title).firstResult(); + } + + return bookRepository + .find("{'creationDate': {$gte: ?1}, 'creationDate': {$lte: ?2}}", LocalDate.parse(dateFrom), + LocalDate.parse(dateTo)) + .firstResult(); + } + + @GET + @Path("/search2") + public Book search2(@QueryParam("author") String author, @QueryParam("title") String title, + @QueryParam("dateFrom") String dateFrom, @QueryParam("dateTo") String dateTo) { + if (author != null) { + return bookRepository.find("{'author': :author,'bookTitle': :title}", + Parameters.with("author", author).and("title", title)).firstResult(); + } + + return bookRepository.find("{'creationDate': {$gte: :dateFrom}, 'creationDate': {$lte: :dateTo}}", + Parameters.with("dateFrom", LocalDate.parse(dateFrom)).and("dateTo", LocalDate.parse(dateTo))).firstResult(); + } + +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java new file mode 100644 index 0000000000000..49f28cd323000 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/Person.java @@ -0,0 +1,10 @@ +package io.quarkus.it.mongodb.panache.person; + +import org.bson.codecs.pojo.annotations.BsonId; + +public class Person { + @BsonId + public Long id; + public String firstname; + public String lastname; +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java new file mode 100644 index 0000000000000..4a146d9d9c1a5 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntity.java @@ -0,0 +1,12 @@ +package io.quarkus.it.mongodb.panache.person; + +import org.bson.codecs.pojo.annotations.BsonId; + +import io.quarkus.mongodb.panache.PanacheMongoEntityBase; + +public class PersonEntity extends PanacheMongoEntityBase { + @BsonId + public Long id; + public String firstname; + public String lastname; +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntityResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntityResource.java new file mode 100644 index 0000000000000..4c5dc3d7072b9 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonEntityResource.java @@ -0,0 +1,73 @@ +package io.quarkus.it.mongodb.panache.person; + +import java.net.URI; +import java.util.List; + +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.quarkus.panache.common.Sort; + +@Path("/persons/entity") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class PersonEntityResource { + @GET + public List getPersons(@QueryParam("sort") String sort) { + if (sort != null) { + return PersonEntity.listAll(Sort.ascending(sort)); + } + return PersonEntity.listAll(); + } + + @POST + public Response addPerson(PersonEntity person) { + person.persist(); + String id = person.id.toString(); + return Response.created(URI.create("/persons/entity/" + id)).build(); + } + + @POST + @Path("/multiple") + public void addPersons(List persons) { + PersonEntity.persist(persons); + } + + @PUT + public Response updatePerson(PersonEntity person) { + person.update(); + return Response.accepted().build(); + } + + // PATCH is not correct here but it allows to test persistOrUpdate without a specific subpath + @PATCH + public Response upsertPerson(PersonEntity person) { + person.persistOrUpdate(); + return Response.accepted().build(); + } + + @DELETE + @Path("/{id}") + public void deletePerson(@PathParam("id") String id) { + PersonEntity person = PersonEntity.findById(Long.parseLong(id)); + person.delete(); + } + + @GET + @Path("/{id}") + public PersonEntity getPerson(@PathParam("id") String id) { + return PersonEntity.findById(Long.parseLong(id)); + } + + @GET + @Path("/count") + public long countAll() { + return PersonEntity.count(); + } + + @DELETE + public void deleteAll() { + PersonEntity.deleteAll(); + } +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepository.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepository.java new file mode 100644 index 0000000000000..be813d2256439 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepository.java @@ -0,0 +1,9 @@ +package io.quarkus.it.mongodb.panache.person; + +import javax.enterprise.context.ApplicationScoped; + +import io.quarkus.mongodb.panache.PanacheMongoRepositoryBase; + +@ApplicationScoped +public class PersonRepository implements PanacheMongoRepositoryBase { +} diff --git a/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java new file mode 100644 index 0000000000000..9e799d05eebb2 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/java/io/quarkus/it/mongodb/panache/person/PersonRepositoryResource.java @@ -0,0 +1,78 @@ +package io.quarkus.it.mongodb.panache.person; + +import java.net.URI; +import java.util.List; + +import javax.inject.Inject; +import javax.ws.rs.*; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import io.quarkus.panache.common.Sort; + +@Path("/persons/repository") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class PersonRepositoryResource { + + @Inject + PersonRepository personRepository; + + @GET + public List getPersons(@QueryParam("sort") String sort) { + if (sort != null) { + return personRepository.listAll(Sort.ascending(sort)); + } + return personRepository.listAll(); + } + + @POST + public Response addPerson(Person person) { + personRepository.persist(person); + String id = person.id.toString(); + return Response.created(URI.create("/persons/repository/" + id)).build(); + } + + @POST + @Path("/multiple") + public void addPersons(List persons) { + personRepository.persist(persons); + } + + @PUT + public Response updatePerson(Person person) { + personRepository.update(person); + return Response.accepted().build(); + } + + // PATCH is not correct here but it allows to test persistOrUpdate without a specific subpath + @PATCH + public Response upsertPerson(Person person) { + personRepository.persistOrUpdate(person); + return Response.accepted().build(); + } + + @DELETE + @Path("/{id}") + public void deletePerson(@PathParam("id") String id) { + Person person = personRepository.findById(Long.parseLong(id)); + personRepository.delete(person); + } + + @GET + @Path("/{id}") + public Person getPerson(@PathParam("id") String id) { + return personRepository.findById(Long.parseLong(id)); + } + + @GET + @Path("/count") + public long countAll() { + return personRepository.count(); + } + + @DELETE + public void deleteAll() { + personRepository.deleteAll(); + } +} diff --git a/integration-tests/mongodb-panache/src/main/resources/application.properties b/integration-tests/mongodb-panache/src/main/resources/application.properties new file mode 100644 index 0000000000000..4b70d08632241 --- /dev/null +++ b/integration-tests/mongodb-panache/src/main/resources/application.properties @@ -0,0 +1,5 @@ +quarkus.mongodb.connection-string=mongodb://localhost:27018 +quarkus.mongodb.write-concern.journal=false +quarkus.mongodb.database=books + +quarkus.log.category."io.quarkus.mongodb.panache.runtime".level=DEBUG \ No newline at end of file diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookDTO.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookDTO.java new file mode 100644 index 0000000000000..8b7134e80a81f --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookDTO.java @@ -0,0 +1,100 @@ +package io.quarkus.it.mongodb.panache; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonFormat; + +import io.quarkus.it.mongodb.panache.book.BookDetail; + +/** + * The IT uses a DTO and not directly the Book object because it should avoid the usage of ObjectId. + */ +public class BookDTO { + private String title; + private String author; + private String id; + private String transientDescription; + @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd") + private Date creationDate; + + private List categories = new ArrayList<>(); + + private BookDetail details; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public BookDTO setTitle(String title) { + this.title = title; + return this; + } + + public String getAuthor() { + return author; + } + + public BookDTO setAuthor(String author) { + this.author = author; + return this; + } + + public List getCategories() { + return categories; + } + + public BookDTO setCategories(List categories) { + this.categories = categories; + return this; + } + + public BookDetail getDetails() { + return details; + } + + public BookDTO setDetails(BookDetail details) { + this.details = details; + return this; + } + + public String getTransientDescription() { + return transientDescription; + } + + public BookDTO setTransientDescription(String transientDescription) { + this.transientDescription = transientDescription; + return this; + } + + public Date getCreationDate() { + return creationDate; + } + + public BookDTO setCreationDate(Date creationDate) { + this.creationDate = creationDate; + return this; + } + + @Override + public String toString() { + return "BookDTO{" + + "title='" + title + '\'' + + ", author='" + author + '\'' + + ", id='" + id + '\'' + + ", transientDescription='" + transientDescription + '\'' + + ", creationDate='" + creationDate + '\'' + + ", categories=" + categories + + ", details=" + details + + '}'; + } +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookResourceTest.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookResourceTest.java new file mode 100644 index 0000000000000..b2427d1ad1657 --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/BookResourceTest.java @@ -0,0 +1,327 @@ +package io.quarkus.it.mongodb.panache; + +import static io.restassured.RestAssured.get; + +import java.io.IOException; +import java.time.LocalDate; +import java.time.ZoneOffset; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jdk8.Jdk8Module; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +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.it.mongodb.panache.book.BookDetail; +import io.quarkus.it.mongodb.panache.person.Person; +import io.quarkus.test.junit.QuarkusTest; +import io.restassured.RestAssured; +import io.restassured.common.mapper.TypeRef; +import io.restassured.config.ObjectMapperConfig; +import io.restassured.parsing.Parser; +import io.restassured.response.Response; + +@QuarkusTest +class BookResourceTest { + private static final Logger LOGGER = LoggerFactory.getLogger(BookResourceTest.class); + private static final TypeRef> LIST_OF_BOOK_TYPE_REF = new TypeRef>() { + }; + private static final TypeRef> LIST_OF_PERSON_TYPE_REF = new TypeRef>() { + }; + + private static MongodExecutable MONGO; + + @BeforeAll + public static void startMongoDatabase() throws IOException { + Version.Main version = Version.Main.V4_0; + int port = 27018; + LOGGER.info("Starting Mongo {} on port {}", version, port); + IMongodConfig config = new MongodConfigBuilder() + .version(version) + .net(new Net(port, Network.localhostIsIPv6())) + .build(); + MONGO = MongodStarter.getDefaultInstance().prepare(config); + MONGO.start(); + } + + @AfterAll + public static void stopMongoDatabase() { + if (MONGO != null) { + MONGO.stop(); + } + } + + @Test + public void testBookEntity() { + callBookEndpoint("/books/entity"); + } + + @Test + public void testBookRepository() { + callBookEndpoint("/books/repository"); + } + + @Test + public void testPersonEntity() { + callPersonEndpoint("/persons/entity"); + } + + @Test + public void testPersonRepository() { + callPersonEndpoint("/persons/repository"); + } + + private void callBookEndpoint(String endpoint) { + RestAssured.defaultParser = Parser.JSON; + RestAssured.config + .objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory((type, s) -> new ObjectMapper() + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS))); + + List list = get(endpoint).as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(0, list.size()); + + BookDTO book1 = new BookDTO().setAuthor("Victor Hugo").setTitle("Les Misérables") + .setCreationDate(yearToDate(1886)) + .setCategories(Arrays.asList("long", "very long")) + .setDetails(new BookDetail().setRating(3).setSummary("A very long book")); + Response response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(book1) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + Assertions.assertTrue(response.header("Location").length() > 20);//Assert that id has been populated + + BookDTO book2 = new BookDTO().setAuthor("Victor Hugo").setTitle("Notre-Dame de Paris") + .setCreationDate(yearToDate(1831)) + .setCategories(Arrays.asList("long", "quasimodo")) + .setDetails(new BookDetail().setRating(4).setSummary("quasimodo and esmeralda")); + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(book2) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + + list = get(endpoint).as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(2, list.size()); + + BookDTO book3 = new BookDTO().setAuthor("Charles Baudelaire").setTitle("Les fleurs du mal") + .setCreationDate(yearToDate(1857)) + .setCategories(Collections.singletonList("poem")) + .setDetails(new BookDetail().setRating(2).setSummary("Les Fleurs du mal is a volume of poetry.")); + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(book3) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + + BookDTO book4 = new BookDTO().setAuthor("Charles Baudelaire").setTitle("Le Spleen de Paris") + .setCreationDate(yearToDate(1869)) + .setCategories(Collections.singletonList("poem")) + .setDetails(new BookDetail().setRating(2) + .setSummary("Le Spleen de Paris is a collection of 50 short prose poems.")); + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(book4) + .patch(endpoint) + .andReturn(); + Assertions.assertEquals(202, response.statusCode()); + + list = get(endpoint).as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(4, list.size()); + + //with sort + list = get(endpoint + "?sort=author").as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(4, list.size()); + + // magic query find("author", author) + list = get(endpoint + "/search/Victor Hugo").as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(2, list.size()); + + // magic query find("{'author':?1,'title':?1}", author, title) + BookDTO book = get(endpoint + "/search?author=Victor Hugo&title=Notre-Dame de Paris").as(BookDTO.class); + Assertions.assertNotNull(book); + + // date + book = get(endpoint + "/search?dateFrom=1885-01-01&dateTo=1887-01-01").as(BookDTO.class); + Assertions.assertNotNull(book); + + book = get(endpoint + "/search2?dateFrom=1885-01-01&dateTo=1887-01-01").as(BookDTO.class); + Assertions.assertNotNull(book); + + // magic query find("{'author'::author,'title'::title}", Parameters.with("author", author).and("title", title)) + book = get(endpoint + "/search2?author=Victor Hugo&title=Notre-Dame de Paris").as(BookDTO.class); + Assertions.assertNotNull(book); + Assertions.assertNotNull(book.getId()); + Assertions.assertNotNull(book.getDetails()); + + //update a book + book.setTitle("Notre-Dame de Paris 2").setTransientDescription("should not be persisted"); + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(book) + .put(endpoint) + .andReturn(); + Assertions.assertEquals(202, response.statusCode()); + + //check that the title has been updated and the transient description ignored + book = get(endpoint + "/" + book.getId().toString()).as(BookDTO.class); + Assertions.assertEquals("Notre-Dame de Paris 2", book.getTitle()); + Assertions.assertNull(book.getTransientDescription()); + + //delete a book + response = RestAssured + .given() + .delete(endpoint + "/" + book.getId().toString()) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + list = get(endpoint).as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(3, list.size()); + + //test some special characters + list = get(endpoint + "/search/Victor'\\ Hugo").as(LIST_OF_BOOK_TYPE_REF); + Assertions.assertEquals(0, list.size()); + } + + private void callPersonEndpoint(String endpoint) { + RestAssured.defaultParser = Parser.JSON; + RestAssured.config + .objectMapperConfig(new ObjectMapperConfig().jackson2ObjectMapperFactory((type, s) -> new ObjectMapper() + .registerModule(new Jdk8Module()) + .registerModule(new JavaTimeModule()) + .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS))); + + List list = get(endpoint).as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(0, list.size()); + + Person person1 = new Person(); + person1.id = 1L; + person1.firstname = "John"; + person1.lastname = "Doe"; + Response response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person1) + .post(endpoint) + .andReturn(); + Assertions.assertEquals(201, response.statusCode()); + + Person person2 = new Person(); + person2.id = 2L; + person2.firstname = "Jane"; + person2.lastname = "Doe"; + Person person3 = new Person(); + person3.id = 3L; + person3.firstname = "Victor"; + person3.lastname = "Hugo"; + List persons = new ArrayList<>(); + persons.add(person2); + persons.add(person3); + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(persons) + .post(endpoint + "/multiple") + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + Person person4 = new Person(); + person1.id = 4L; + person1.firstname = "Charles"; + person1.lastname = "Baudelaire"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person1) + .patch(endpoint) + .andReturn(); + Assertions.assertEquals(202, response.statusCode()); + + list = get(endpoint).as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(4, list.size()); + + //with sort + list = get(endpoint + "?sort=firstname").as(LIST_OF_PERSON_TYPE_REF); + Assertions.assertEquals(4, list.size()); + + //count + Long count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(4, count); + + //update a person + person3.lastname = "Webster"; + response = RestAssured + .given() + .header("Content-Type", "application/json") + .body(person3) + .put(endpoint) + .andReturn(); + Assertions.assertEquals(202, response.statusCode()); + + //check that the title has been updated + person3 = get(endpoint + "/" + person3.id.toString()).as(Person.class); + Assertions.assertEquals(3L, person3.id); + Assertions.assertEquals("Webster", person3.lastname); + + //delete a person + response = RestAssured + .given() + .delete(endpoint + "/" + person3.id.toString()) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(3, count); + + //delete all + response = RestAssured + .given() + .delete(endpoint) + .andReturn(); + Assertions.assertEquals(204, response.statusCode()); + + count = get(endpoint + "/count").as(Long.class); + Assertions.assertEquals(0, count); + } + + private Date yearToDate(int year) { + Calendar cal = new GregorianCalendar(); + cal.set(year, 1, 1); + return cal.getTime(); + } + + private Date fromYear(int year) { + return Date.from(LocalDate.of(year, 1, 1).atStartOfDay().toInstant(ZoneOffset.UTC)); + } + +} diff --git a/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/NativeBookResourceIT.java b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/NativeBookResourceIT.java new file mode 100644 index 0000000000000..77726773e13cc --- /dev/null +++ b/integration-tests/mongodb-panache/src/test/java/io/quarkus/it/mongodb/panache/NativeBookResourceIT.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mongodb.panache; + +import io.quarkus.test.junit.SubstrateTest; + +@SubstrateTest +class NativeBookResourceIT extends BookResourceTest { + +} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index a75e03ceb5113..59c0be4e5fb05 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -62,8 +62,9 @@ artemis-core artemis-jms maven - kotlin scala + kotlin + mongodb-panache