Handle MongoDB BsonDiscriminator

Fixes #6292
This commit is contained in:
Loïc Mathieu
2020-02-10 09:50:16 +01:00
parent 72e8580cff
commit 9ee1c84725
12 changed files with 226 additions and 8 deletions

View File

@@ -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.

View File

@@ -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<String> bsonDisciminatorClassNames;
public BsonDiscriminatorBuildItem(List<String> bsonDisciminatorClassNames) {
this.bsonDisciminatorClassNames = bsonDisciminatorClassNames;
}
public List<String> getBsonDisciminatorClassNames() {
return bsonDisciminatorClassNames;
}
}

View File

@@ -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<ReflectiveClassBuildItem> addCodecsToNative(CodecProviderBuildItem providers) {
return providers.getCodecProviderClassNames().stream()
BsonDiscriminatorBuildItem collectBsonDiscriminators(CombinedIndexBuildItem indexBuildItem) {
List<String> 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<ReflectiveClassBuildItem> addCodecsAndDiscriminatorsToNative(CodecProviderBuildItem codecProviders,
BsonDiscriminatorBuildItem bsonDiscriminators) {
List<String> 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());
}

View File

@@ -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<String> codecProviders;
private List<String> bsonDiscriminators;
private Map<String, MongoClient> mongoclients = new HashMap<>();
private Map<String, ReactiveMongoClient> 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<String> bsonDiscriminators) {
this.bsonDiscriminators = bsonDiscriminators;
}
public void disableSslSupport() {
this.disableSslSupport = true;
}

View File

@@ -30,7 +30,7 @@ public class MongoClientRecorder {
};
}
public void configureRuntimeProperties(List<String> codecs, MongodbConfig config) {
public void configureRuntimeProperties(List<String> codecs, List<String> 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);
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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<Vehicle> collection;
@PostConstruct
public void init() {
MongoDatabase database = client.getDatabase("books");
collection = database.getCollection("vehicle", Vehicle.class);
}
@GET
public List<Vehicle> getVehicles() {
FindIterable<Vehicle> iterable = collection.find();
List<Vehicle> 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();
}
}

View File

@@ -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());
}
}

View File

@@ -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<Vehicle> {
@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");
}
}
}

View File

@@ -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));
}
}