From 9ee1c84725054f8a7a8036e4c4e22ae9badeffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Mathieu?= Date: Mon, 10 Feb 2020 09:50:16 +0100 Subject: [PATCH] Handle MongoDB BsonDiscriminator Fixes #6292 --- docs/src/main/asciidoc/mongodb.adoc | 10 ++++ .../BsonDiscriminatorBuildItem.java | 18 ++++++ .../deployment/MongoClientProcessor.java | 25 +++++++- .../runtime/AbstractMongoClientProducer.java | 24 ++++++-- .../mongodb/runtime/MongoClientRecorder.java | 3 +- .../quarkus/it/mongodb/discriminator/Car.java | 8 +++ .../it/mongodb/discriminator/Moto.java | 8 +++ .../it/mongodb/discriminator/Vehicle.java | 9 +++ .../discriminator/VehicleResource.java | 57 +++++++++++++++++++ .../jsonb/VehicleCustomizer.java | 14 +++++ .../jsonb/VehicleDeserializer.java | 36 ++++++++++++ .../quarkus/it/mongodb/BookResourceTest.java | 22 +++++++ 12 files changed, 226 insertions(+), 8 deletions(-) create mode 100644 extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/BsonDiscriminatorBuildItem.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Car.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Moto.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Vehicle.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/VehicleResource.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleCustomizer.java create mode 100644 integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleDeserializer.java diff --git a/docs/src/main/asciidoc/mongodb.adoc b/docs/src/main/asciidoc/mongodb.adoc index a1cde7eec..8e49e7629 100644 --- a/docs/src/main/asciidoc/mongodb.adoc +++ b/docs/src/main/asciidoc/mongodb.adoc @@ -510,6 +510,16 @@ public class CodecFruitService { } ---- +== The POJO Codec + +The link:http://mongodb.github.io/mongo-java-driver/3.12/bson/pojos[POJO Codec] provides a set of annotations that enable the customization of +the way a POJO is mapped to a MongoDB collection and this codec is initialized automatically by Quarkus + +One of these annotations is the `@BsonDiscriminator` annotation that allows to storage multiple Java types in a single MongoDB collection by adding +a discriminator field inside the document. It can be useful when working with abstract types or interfaces. + +Quarkus will automatically register all the classes annotated with `@BsonDiscriminator` with the POJO codec. + == Simplifying MongoDB with Panache The link:mongodb-panache[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.html[Hibernate ORM with Panache] and focuses on making your entities trivial and fun to write in Quarkus. diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/BsonDiscriminatorBuildItem.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/BsonDiscriminatorBuildItem.java new file mode 100644 index 000000000..d71fe8868 --- /dev/null +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/BsonDiscriminatorBuildItem.java @@ -0,0 +1,18 @@ +package io.quarkus.mongodb.deployment; + +import java.util.List; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class BsonDiscriminatorBuildItem extends SimpleBuildItem { + + private List bsonDisciminatorClassNames; + + public BsonDiscriminatorBuildItem(List bsonDisciminatorClassNames) { + this.bsonDisciminatorClassNames = bsonDisciminatorClassNames; + } + + public List getBsonDisciminatorClassNames() { + return bsonDisciminatorClassNames; + } +} diff --git a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java index 3f1a177e4..bb1e2952b 100644 --- a/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java +++ b/extensions/mongodb-client/deployment/src/main/java/io/quarkus/mongodb/deployment/MongoClientProcessor.java @@ -3,6 +3,7 @@ package io.quarkus.mongodb.deployment; import static io.quarkus.deployment.annotations.ExecutionTime.RUNTIME_INIT; import static io.quarkus.deployment.annotations.ExecutionTime.STATIC_INIT; +import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.List; @@ -15,6 +16,7 @@ import javax.enterprise.inject.Default; import javax.enterprise.inject.Produces; import org.bson.codecs.configuration.CodecProvider; +import org.bson.codecs.pojo.annotations.BsonDiscriminator; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationValue; import org.jboss.jandex.ClassInfo; @@ -72,8 +74,10 @@ public class MongoClientProcessor { @BuildStep void configureRuntimeProperties(MongoClientRecorder recorder, CodecProviderBuildItem codecProvider, + BsonDiscriminatorBuildItem bsonDiscriminator, MongodbConfig config) { - recorder.configureRuntimeProperties(codecProvider.getCodecProviderClassNames(), config); + recorder.configureRuntimeProperties(codecProvider.getCodecProviderClassNames(), + bsonDiscriminator.getBsonDisciminatorClassNames(), config); } @BuildStep @@ -85,8 +89,23 @@ public class MongoClientProcessor { } @BuildStep - List addCodecsToNative(CodecProviderBuildItem providers) { - return providers.getCodecProviderClassNames().stream() + BsonDiscriminatorBuildItem collectBsonDiscriminators(CombinedIndexBuildItem indexBuildItem) { + List names = new ArrayList<>(); + DotName bsonDiscriminatorName = DotName.createSimple(BsonDiscriminator.class.getName()); + for (AnnotationInstance annotationInstance : indexBuildItem.getIndex().getAnnotations(bsonDiscriminatorName)) { + names.add(annotationInstance.target().asClass().name().toString()); + } + return new BsonDiscriminatorBuildItem(names); + } + + @BuildStep + List addCodecsAndDiscriminatorsToNative(CodecProviderBuildItem codecProviders, + BsonDiscriminatorBuildItem bsonDiscriminators) { + List reflectiveClassNames = new ArrayList<>(); + reflectiveClassNames.addAll(codecProviders.getCodecProviderClassNames()); + reflectiveClassNames.addAll(bsonDiscriminators.getBsonDisciminatorClassNames()); + + return reflectiveClassNames.stream() .map(s -> new ReflectiveClassBuildItem(true, true, false, s)) .collect(Collectors.toList()); } diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/AbstractMongoClientProducer.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/AbstractMongoClientProducer.java index 6ed90fe13..d18bc6c83 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/AbstractMongoClientProducer.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/AbstractMongoClientProducer.java @@ -22,6 +22,7 @@ import javax.annotation.PreDestroy; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistries; import org.bson.codecs.configuration.CodecRegistry; +import org.bson.codecs.pojo.ClassModel; import org.bson.codecs.pojo.Conventions; import org.bson.codecs.pojo.PojoCodecProvider; import org.jboss.logging.Logger; @@ -54,6 +55,7 @@ public abstract class AbstractMongoClientProducer { private MongodbConfig mongodbConfig; private boolean disableSslSupport = false; private List codecProviders; + private List bsonDiscriminators; private Map mongoclients = new HashMap<>(); private Map reactiveMongoClients = new HashMap<>(); @@ -217,11 +219,21 @@ public abstract class AbstractMongoClientProducer { } // add pojo codec provider with automatic capabilities // it always needs to be the last codec provided - CodecProvider pojoCodecProvider = PojoCodecProvider.builder() + PojoCodecProvider.Builder pojoCodecProviderBuilder = PojoCodecProvider.builder() .automatic(true) - .conventions(Conventions.DEFAULT_CONVENTIONS) - .build(); - providers.add(pojoCodecProvider); + .conventions(Conventions.DEFAULT_CONVENTIONS); + // register bson discriminators + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + for (String bsonDiscriminator : bsonDiscriminators) { + try { + pojoCodecProviderBuilder + .register(ClassModel.builder(Class.forName(bsonDiscriminator, true, classLoader)) + .enableDiscriminator(true).build()); + } catch (ClassNotFoundException e) { + // Ignore + } + } + providers.add(pojoCodecProviderBuilder.build()); CodecRegistry registry = CodecRegistries.fromRegistries(defaultCodecRegistry, CodecRegistries.fromProviders(providers)); settings.codecRegistry(registry); @@ -367,6 +379,10 @@ public abstract class AbstractMongoClientProducer { this.codecProviders = codecs; } + public void setBsonDiscriminators(List bsonDiscriminators) { + this.bsonDiscriminators = bsonDiscriminators; + } + public void disableSslSupport() { this.disableSslSupport = true; } diff --git a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java index ec9d91286..d351a1f82 100644 --- a/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java +++ b/extensions/mongodb-client/runtime/src/main/java/io/quarkus/mongodb/runtime/MongoClientRecorder.java @@ -30,7 +30,7 @@ public class MongoClientRecorder { }; } - public void configureRuntimeProperties(List codecs, MongodbConfig config) { + public void configureRuntimeProperties(List codecs, List bsonDiscriminators, MongodbConfig config) { // TODO @dmlloyd // Same here, the map is entirely empty (obviously, I didn't expect the values // that were not properly injected but at least the config objects present in @@ -38,6 +38,7 @@ public class MongoClientRecorder { // The elements from the default mongoClient are there AbstractMongoClientProducer producer = Arc.container().instance(AbstractMongoClientProducer.class).get(); producer.setCodecs(codecs); + producer.setBsonDiscriminators(bsonDiscriminators); producer.setConfig(config); } diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Car.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Car.java new file mode 100644 index 000000000..72c4a8481 --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Car.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mongodb.discriminator; + +import org.bson.codecs.pojo.annotations.BsonDiscriminator; + +@BsonDiscriminator(key = "type", value = "CAR") +public class Car extends Vehicle { + public int seatNumber; +} diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Moto.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Moto.java new file mode 100644 index 000000000..4ec697232 --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Moto.java @@ -0,0 +1,8 @@ +package io.quarkus.it.mongodb.discriminator; + +import org.bson.codecs.pojo.annotations.BsonDiscriminator; + +@BsonDiscriminator(key = "type", value = "MOTO") +public class Moto extends Vehicle { + public boolean sideCar; +} diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Vehicle.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Vehicle.java new file mode 100644 index 000000000..2685992af --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/Vehicle.java @@ -0,0 +1,9 @@ +package io.quarkus.it.mongodb.discriminator; + +import org.bson.codecs.pojo.annotations.BsonDiscriminator; + +@BsonDiscriminator(key = "type") +public abstract class Vehicle { + public String type; + public String name; +} diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/VehicleResource.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/VehicleResource.java new file mode 100644 index 000000000..c2b7576c3 --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/VehicleResource.java @@ -0,0 +1,57 @@ +package io.quarkus.it.mongodb.discriminator; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +import javax.annotation.PostConstruct; +import javax.inject.Inject; +import javax.ws.rs.Consumes; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; + +import com.mongodb.client.FindIterable; +import com.mongodb.client.MongoClient; +import com.mongodb.client.MongoCollection; +import com.mongodb.client.MongoDatabase; + +@Path("/vehicles") +@Produces(MediaType.APPLICATION_JSON) +@Consumes(MediaType.APPLICATION_JSON) +public class VehicleResource { + @Inject + MongoClient client; + + private MongoCollection collection; + + @PostConstruct + public void init() { + MongoDatabase database = client.getDatabase("books"); + collection = database.getCollection("vehicle", Vehicle.class); + + } + + @GET + public List getVehicles() { + FindIterable iterable = collection.find(); + List vehicles = new ArrayList<>(); + for (Vehicle doc : iterable) { + vehicles.add(doc); + } + return vehicles; + } + + @POST + public Response addVehicle(Vehicle vehicle) throws UnsupportedEncodingException { + collection.insertOne(vehicle); + return Response.created(URI.create("/vehicle/" + URLEncoder.encode(vehicle.name, StandardCharsets.UTF_8.toString()))) + .build(); + } +} diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleCustomizer.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleCustomizer.java new file mode 100644 index 000000000..109405396 --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleCustomizer.java @@ -0,0 +1,14 @@ +package io.quarkus.it.mongodb.discriminator.jsonb; + +import javax.inject.Singleton; +import javax.json.bind.JsonbConfig; + +import io.quarkus.jsonb.JsonbConfigCustomizer; + +@Singleton +public class VehicleCustomizer implements JsonbConfigCustomizer { + @Override + public void customize(JsonbConfig jsonbConfig) { + jsonbConfig.withDeserializers(new VehicleDeserializer()); + } +} diff --git a/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleDeserializer.java b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleDeserializer.java new file mode 100644 index 000000000..51b063530 --- /dev/null +++ b/integration-tests/mongodb-client/src/main/java/io/quarkus/it/mongodb/discriminator/jsonb/VehicleDeserializer.java @@ -0,0 +1,36 @@ +package io.quarkus.it.mongodb.discriminator.jsonb; + +import java.lang.reflect.Type; + +import javax.json.JsonObject; +import javax.json.bind.serializer.DeserializationContext; +import javax.json.bind.serializer.JsonbDeserializer; +import javax.json.stream.JsonParser; + +import io.quarkus.it.mongodb.discriminator.Car; +import io.quarkus.it.mongodb.discriminator.Moto; +import io.quarkus.it.mongodb.discriminator.Vehicle; + +public class VehicleDeserializer implements JsonbDeserializer { + @Override + public Vehicle deserialize(JsonParser parser, DeserializationContext ctx, Type rtType) { + JsonObject json = parser.getObject(); + String type = json.getString("type"); + switch (type) { + case "CAR": + Car car = new Car(); + car.type = type; + car.seatNumber = json.getInt("seatNumber"); + car.name = json.getString("name"); + return car; + case "MOTO": + Moto moto = new Moto(); + moto.type = type; + moto.name = json.getString("name"); + moto.sideCar = json.getBoolean("sideCar"); + return moto; + default: + throw new RuntimeException("Type " + type + "not managed"); + } + } +} diff --git a/integration-tests/mongodb-client/src/test/java/io/quarkus/it/mongodb/BookResourceTest.java b/integration-tests/mongodb-client/src/test/java/io/quarkus/it/mongodb/BookResourceTest.java index 51d36a154..b1d6d1971 100644 --- a/integration-tests/mongodb-client/src/test/java/io/quarkus/it/mongodb/BookResourceTest.java +++ b/integration-tests/mongodb-client/src/test/java/io/quarkus/it/mongodb/BookResourceTest.java @@ -16,6 +16,8 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import io.quarkus.it.mongodb.discriminator.Car; +import io.quarkus.it.mongodb.discriminator.Moto; import io.quarkus.test.common.QuarkusTestResource; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; @@ -123,4 +125,24 @@ public class BookResourceTest { "checks.name", containsInAnyOrder("MongoDB connection health check")); } + @Test + public void testVehicleEndpoint() { + Car car = new Car(); + car.name = "Renault Clio"; + car.type = "CAR"; + car.seatNumber = 5; + RestAssured.given().header("Content-Type", "application/json").body(car) + .when().post("/vehicles") + .then().statusCode(201); + + Moto moto = new Moto(); + moto.name = "Harley Davidson Sportster"; + moto.type = "MOTO"; + RestAssured.given().header("Content-Type", "application/json").body(moto) + .when().post("/vehicles") + .then().statusCode(201); + + get("/vehicles").then().statusCode(200).body("size()", is(2)); + } + }