From 77b549d62c0c9ab2787afe6078ddbc40dbc5a95a Mon Sep 17 00:00:00 2001 From: Valentin Kovalenko Date: Thu, 1 Jun 2023 18:04:49 -0600 Subject: [PATCH] Deprecate `Parameterizable`, introduce default `CodecProvider.get(Class, List, CodecRegistry)` instead (#1115) JAVA-4967 --- .../org/bson/codecs/kotlin/DataClassCodec.kt | 58 ++++--------------- .../codecs/kotlin/DataClassCodecProvider.kt | 6 +- .../kotlin/DataClassCodecProviderTest.kt | 37 ++---------- .../org/bson/codecs/record/RecordCodec.java | 40 +------------ .../codecs/record/RecordCodecProvider.java | 18 ++++-- .../bson/codecs/record/RecordCodecTest.java | 49 +++++++++------- .../main/org/bson/codecs/CollectionCodec.java | 18 +----- .../bson/codecs/CollectionCodecProvider.java | 29 +++++++++- .../org/bson/codecs/MapCodecProvider.java | 33 ++++++++++- bson/src/main/org/bson/codecs/MapCodecV2.java | 22 +------ .../main/org/bson/codecs/Parameterizable.java | 7 +++ .../codecs/configuration/CodecProvider.java | 29 ++++++++++ ...idableUuidRepresentationCodecProvider.java | 17 +++++- .../org/bson/internal/ChildCodecRegistry.java | 9 ++- .../bson/internal/ProvidersCodecRegistry.java | 33 ++++++++--- .../codecs/CollectionCodecProviderTest.java | 52 +++++++++++++++++ .../CollectionCodecSpecification.groovy | 7 ++- .../org/bson/codecs/MapCodecProviderTest.java | 53 +++++++++++++++++ .../codecs/MapCodecV2Specification.groovy | 7 ++- .../CodeRegistriesSpecification.groovy | 58 +++++++++++++++++++ .../mongodb/Jep395RecordCodecProvider.java | 22 +++++-- .../main/com/mongodb/KotlinCodecProvider.java | 23 ++++++-- 22 files changed, 416 insertions(+), 211 deletions(-) create mode 100644 bson/src/test/unit/org/bson/codecs/CollectionCodecProviderTest.java create mode 100644 bson/src/test/unit/org/bson/codecs/MapCodecProviderTest.java diff --git a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt index 42e493b9706..da84033c521 100644 --- a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt +++ b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt @@ -35,7 +35,6 @@ import org.bson.BsonWriter import org.bson.codecs.Codec import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext -import org.bson.codecs.Parameterizable import org.bson.codecs.RepresentationConfigurable import org.bson.codecs.configuration.CodecConfigurationException import org.bson.codecs.configuration.CodecRegistry @@ -136,31 +135,22 @@ internal data class DataClassCodec( ): Codec? { return if (!kClass.isData) { null - } else if (kClass.typeParameters.isNotEmpty()) { - RawDataClassCodec(kClass) } else { - createDataClassCodec(kClass, codecRegistry, types) + validateAnnotations(kClass) + val primaryConstructor = + kClass.primaryConstructor ?: throw CodecConfigurationException("No primary constructor for $kClass") + val typeMap = types.mapIndexed { i, k -> primaryConstructor.typeParameters[i].createType() to k } + .toMap() + + val propertyModels = + primaryConstructor.parameters.map { kParameter -> + PropertyModel( + kParameter, computeFieldName(kParameter), getCodec(kParameter, typeMap, codecRegistry)) + } + return DataClassCodec(kClass, primaryConstructor, propertyModels) } } - private fun createDataClassCodec( - kClass: KClass, - codecRegistry: CodecRegistry, - types: List = emptyList() - ): Codec { - validateAnnotations(kClass) - val primaryConstructor = - kClass.primaryConstructor ?: throw CodecConfigurationException("No primary constructor for $kClass") - val typeMap = types.mapIndexed { i, k -> primaryConstructor.typeParameters[i].createType() to k }.toMap() - - val propertyModels = - primaryConstructor.parameters.map { kParameter -> - PropertyModel( - kParameter, computeFieldName(kParameter), getCodec(kParameter, typeMap, codecRegistry)) - } - return DataClassCodec(kClass, primaryConstructor, propertyModels) - } - private fun validateAnnotations(kClass: KClass) { codecConfigurationRequires(kClass.findAnnotation() == null) { """Annotation 'BsonDiscriminator' is not supported on kotlin data classes, @@ -251,29 +241,5 @@ internal data class DataClassCodec( throw CodecConfigurationException(lazyMessage.invoke()) } } - - /** - * A Raw unparameterized data class - * - * It cannot encode or decode it just can create parameterized DataClassCodecs - */ - internal data class RawDataClassCodec(private val kClass: KClass) : Codec, Parameterizable { - - override fun getEncoderClass(): Class = kClass.java - - override fun parameterize(codecRegistry: CodecRegistry, types: List): Codec<*> { - return createDataClassCodec(kClass, codecRegistry, types) - } - - override fun decode(reader: BsonReader?, decoderContext: DecoderContext?): T { - throw CodecConfigurationException( - "Can not decode to ${kClass.simpleName} as it has type parameters and has not been parameterized.") - } - - override fun encode(writer: BsonWriter?, value: T, encoderContext: EncoderContext?) { - throw CodecConfigurationException( - "Can not encode to ${kClass.simpleName} as it has type parameters and has not been parameterized.") - } - } } } diff --git a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt index 8053529002b..e13d74116c7 100644 --- a/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt +++ b/bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodecProvider.kt @@ -18,9 +18,13 @@ package org.bson.codecs.kotlin import org.bson.codecs.Codec import org.bson.codecs.configuration.CodecProvider import org.bson.codecs.configuration.CodecRegistry +import java.lang.reflect.Type /** A Kotlin reflection based Codec Provider for data classes */ public class DataClassCodecProvider : CodecProvider { override fun get(clazz: Class, registry: CodecRegistry): Codec? = - DataClassCodec.create(clazz.kotlin, registry) + get(clazz, emptyList(), registry) + + override fun get(clazz: Class, typeArguments: List, registry: CodecRegistry): Codec? = + DataClassCodec.create(clazz.kotlin, registry, typeArguments) } diff --git a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt index 7de4bfca944..b36ada9622a 100644 --- a/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt +++ b/bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecProviderTest.kt @@ -16,23 +16,16 @@ package org.bson.codecs.kotlin import com.mongodb.MongoClientSettings -import kotlin.test.assertEquals -import kotlin.test.assertNotNull -import kotlin.test.assertNull -import kotlin.test.assertTrue -import org.bson.BsonDocument -import org.bson.BsonDocumentReader -import org.bson.BsonDocumentWriter -import org.bson.codecs.DecoderContext -import org.bson.codecs.EncoderContext import org.bson.codecs.configuration.CodecConfigurationException -import org.bson.codecs.kotlin.samples.DataClassEmbedded import org.bson.codecs.kotlin.samples.DataClassParameterized -import org.bson.codecs.kotlin.samples.DataClassWithParameterizedDataClass import org.bson.codecs.kotlin.samples.DataClassWithSimpleValues import org.bson.conversions.Bson import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue class DataClassCodecProviderTest { @@ -52,27 +45,9 @@ class DataClassCodecProviderTest { } @Test - fun shouldReturnRawDataClassCodecForParameterizedDataClass() { - val provider = DataClassCodecProvider() - val codec = provider.get(DataClassParameterized::class.java, Bson.DEFAULT_CODEC_REGISTRY) - - assertNotNull(codec) - assertTrue { codec is DataClassCodec.Companion.RawDataClassCodec } - assertEquals(DataClassParameterized::class.java, codec.encoderClass) - - assertThrows { - val writer = BsonDocumentWriter(BsonDocument()) - val dataClass = - DataClassWithParameterizedDataClass( - "myId", DataClassParameterized(2.0, "myString", listOf(DataClassEmbedded("embedded1")))) - codec.encode(writer, dataClass.parameterizedDataClass, EncoderContext.builder().build()) - } - + fun shouldRequireTypeArgumentsForDataClassParameterized() { assertThrows { - val value = - BsonDocument.parse( - """{"number": 2.0, "string": "myString", "parameterizedList": [{"name": "embedded1"}]}""") - codec.decode(BsonDocumentReader(value), DecoderContext.builder().build()) + DataClassCodecProvider().get(DataClassParameterized::class.java, Bson.DEFAULT_CODEC_REGISTRY) } } diff --git a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java index 47385c7b7af..8a304760f31 100644 --- a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java +++ b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodec.java @@ -22,7 +22,6 @@ import org.bson.codecs.Codec; import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; -import org.bson.codecs.Parameterizable; import org.bson.codecs.RepresentationConfigurable; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; @@ -50,10 +49,9 @@ import static java.lang.String.format; import static org.bson.assertions.Assertions.notNull; -final class RecordCodec implements Codec, Parameterizable { +final class RecordCodec implements Codec { private static final Logger LOGGER = Loggers.getLogger("RecordCodec"); private final Class clazz; - private final boolean requiresParameterization; private final Constructor canonicalConstructor; private final List componentModels; private final ComponentModel componentModelForId; @@ -251,30 +249,11 @@ private static void validateAnnotationOnlyOnField(final R } } - RecordCodec(final Class clazz, final CodecRegistry codecRegistry) { - this.clazz = notNull("class", clazz); - if (clazz.getTypeParameters().length > 0) { - requiresParameterization = true; - canonicalConstructor = null; - componentModels = null; - fieldNameToComponentModel = null; - componentModelForId = null; - } else { - requiresParameterization = false; - canonicalConstructor = notNull("canonicalConstructor", getCanonicalConstructor(clazz)); - componentModels = getComponentModels(clazz, codecRegistry, List.of()); - fieldNameToComponentModel = componentModels.stream() - .collect(Collectors.toMap(ComponentModel::getFieldName, Function.identity())); - componentModelForId = getComponentModelForId(clazz, componentModels); - } - } - - RecordCodec(final Class clazz, final CodecRegistry codecRegistry, final List types) { - if (types.size() != clazz.getTypeParameters().length || types.isEmpty()) { + RecordCodec(final Class clazz, final List types, final CodecRegistry codecRegistry) { + if (types.size() != clazz.getTypeParameters().length) { throw new CodecConfigurationException("Unexpected number of type parameters for record class " + clazz); } this.clazz = notNull("class", clazz); - requiresParameterization = false; canonicalConstructor = notNull("canonicalConstructor", getCanonicalConstructor(clazz)); componentModels = getComponentModels(clazz, codecRegistry, types); fieldNameToComponentModel = componentModels.stream() @@ -282,18 +261,9 @@ private static void validateAnnotationOnlyOnField(final R componentModelForId = getComponentModelForId(clazz, componentModels); } - @Override - public Codec parameterize(final CodecRegistry codecRegistry, final List types) { - return new RecordCodec<>(clazz, codecRegistry, types); - } - @SuppressWarnings("unchecked") @Override public T decode(final BsonReader reader, final DecoderContext decoderContext) { - if (requiresParameterization) { - throw new CodecConfigurationException("Can not decode to a record with type parameters that has not been parameterized"); - } - reader.readStartDocument(); Object[] constructorArguments = new Object[componentModels.size()]; @@ -320,10 +290,6 @@ public T decode(final BsonReader reader, final DecoderContext decoderContext) { @Override public void encode(final BsonWriter writer, final T record, final EncoderContext encoderContext) { - if (requiresParameterization) { - throw new CodecConfigurationException("Can not decode to a record with type parameters that has not been parameterized"); - } - writer.writeStartDocument(); if (componentModelForId != null) { writeComponent(writer, record, componentModelForId); diff --git a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodecProvider.java b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodecProvider.java index 74624d8e0ac..e41f1fb2bb1 100644 --- a/bson-record-codec/src/main/org/bson/codecs/record/RecordCodecProvider.java +++ b/bson-record-codec/src/main/org/bson/codecs/record/RecordCodecProvider.java @@ -20,6 +20,11 @@ import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; +import java.lang.reflect.Type; +import java.util.List; + +import static org.bson.assertions.Assertions.assertNotNull; + /** * Provides Codec instances for Java records. * @@ -27,13 +32,18 @@ * @see Record */ public final class RecordCodecProvider implements CodecProvider { - @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Codec get(final Class clazz, final CodecRegistry registry) { - if (!clazz.isRecord()) { + return get(clazz, List.of(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { + if (!assertNotNull(clazz).isRecord()) { return null; } - - return (Codec) new RecordCodec(clazz, registry); + @SuppressWarnings({"unchecked", "rawtypes"}) + Codec result = new RecordCodec(clazz, assertNotNull(typeArguments), registry); + return result; } } diff --git a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java index a21fbd769ad..606bc68e59a 100644 --- a/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java +++ b/bson-record-codec/src/test/unit/org/bson/codecs/record/RecordCodecTest.java @@ -27,6 +27,7 @@ import org.bson.codecs.DecoderContext; import org.bson.codecs.EncoderContext; import org.bson.codecs.configuration.CodecConfigurationException; +import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.record.samples.TestRecordEmbedded; import org.bson.codecs.record.samples.TestRecordParameterized; import org.bson.codecs.record.samples.TestRecordWithDeprecatedAnnotations; @@ -67,7 +68,7 @@ public class RecordCodecTest { @Test public void testRecordWithDeprecatedAnnotations() { - var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); + var codec = createRecordCodec(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); var identifier = new ObjectId(); var testRecord = new TestRecordWithDeprecatedAnnotations("Lucas", 14, List.of("soccer", "basketball"), identifier.toHexString()); @@ -95,7 +96,7 @@ public void testRecordWithDeprecatedAnnotations() { @Test public void testRecordWithPojoAnnotations() { - var codec = new RecordCodec<>(TestRecordWithPojoAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); + var codec = createRecordCodec(TestRecordWithPojoAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); var identifier = new ObjectId(); var testRecord = new TestRecordWithPojoAnnotations("Lucas", 14, List.of("soccer", "basketball"), identifier.toHexString()); @@ -123,7 +124,7 @@ public void testRecordWithPojoAnnotations() { @Test public void testRecordWithNestedListOfRecords() { - var codec = new RecordCodec<>(TestRecordWithListOfRecords.class, + var codec = createRecordCodec(TestRecordWithListOfRecords.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithListOfRecords(identifier, List.of(new TestRecordEmbedded("embedded"))); @@ -150,7 +151,7 @@ public void testRecordWithNestedListOfRecords() { @Test public void testRecordWithNestedListOfListOfRecords() { - var codec = new RecordCodec<>(TestRecordWithListOfListOfRecords.class, + var codec = createRecordCodec(TestRecordWithListOfListOfRecords.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithListOfListOfRecords(identifier, List.of(List.of(new TestRecordEmbedded("embedded")))); @@ -178,7 +179,7 @@ public void testRecordWithNestedListOfListOfRecords() { @Test public void testRecordWithNestedMapOfRecords() { - var codec = new RecordCodec<>(TestRecordWithMapOfRecords.class, + var codec = createRecordCodec(TestRecordWithMapOfRecords.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithMapOfRecords(identifier, @@ -206,7 +207,7 @@ public void testRecordWithNestedMapOfRecords() { @Test public void testRecordWithNestedMapOfListRecords() { - var codec = new RecordCodec<>(TestRecordWithMapOfListOfRecords.class, + var codec = createRecordCodec(TestRecordWithMapOfListOfRecords.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithMapOfListOfRecords(identifier, @@ -236,7 +237,7 @@ public void testRecordWithNestedMapOfListRecords() { @Test public void testRecordWithNestedParameterizedRecord() { - var codec = new RecordCodec<>(TestRecordWithParameterizedRecord.class, + var codec = createRecordCodec(TestRecordWithParameterizedRecord.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithParameterizedRecord(identifier, @@ -267,7 +268,7 @@ public void testRecordWithNestedParameterizedRecord() { @Test public void testRecordWithNestedParameterizedRecordWithDifferentlyOrderedTypeParameters() { - var codec = new RecordCodec<>(TestRecordWithNestedParameterizedRecord.class, + var codec = createRecordCodec(TestRecordWithNestedParameterizedRecord.class, fromProviders(new RecordCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)); var identifier = new ObjectId(); var testRecord = new TestRecordWithNestedParameterizedRecord(identifier, @@ -301,7 +302,7 @@ public void testRecordWithNestedParameterizedRecordWithDifferentlyOrderedTypePar @Test public void testRecordWithNulls() { - var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); + var codec = createRecordCodec(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); var identifier = new ObjectId(); var testRecord = new TestRecordWithDeprecatedAnnotations(null, 14, null, identifier.toHexString()); @@ -326,7 +327,7 @@ public void testRecordWithNulls() { @Test public void testRecordWithExtraData() { - var codec = new RecordCodec<>(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); + var codec = createRecordCodec(TestRecordWithDeprecatedAnnotations.class, Bson.DEFAULT_CODEC_REGISTRY); var identifier = new ObjectId(); var testRecord = new TestRecordWithDeprecatedAnnotations("Felix", 13, List.of("rugby", "badminton"), identifier.toHexString()); @@ -376,36 +377,40 @@ public void testSelfReferentialRecords() { @Test public void testExceptionsForAnnotationsNotOnRecordComponent() { assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonIdOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonIdOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonIdOnCanonicalConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonIdOnCanonicalConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonPropertyOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonPropertyOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonPropertyOnCanonicalConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonPropertyOnCanonicalConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonRepresentationOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonRepresentationOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); } @Test public void testExceptionsForUnsupportedAnnotations() { assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonDiscriminatorOnRecord.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonDiscriminatorOnRecord.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonCreatorOnConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonCreatorOnConstructor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonCreatorOnMethod.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonCreatorOnMethod.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonIgnoreOnComponent.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonIgnoreOnComponent.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonIgnoreOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonIgnoreOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonExtraElementsOnComponent.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonExtraElementsOnComponent.class, Bson.DEFAULT_CODEC_REGISTRY)); assertThrows(CodecConfigurationException.class, () -> - new RecordCodec<>(TestRecordWithIllegalBsonExtraElementsOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + createRecordCodec(TestRecordWithIllegalBsonExtraElementsOnAccessor.class, Bson.DEFAULT_CODEC_REGISTRY)); + } + + private static RecordCodec createRecordCodec(final Class clazz, final CodecRegistry registry) { + return new RecordCodec<>(clazz, List.of(), registry); } } diff --git a/bson/src/main/org/bson/codecs/CollectionCodec.java b/bson/src/main/org/bson/codecs/CollectionCodec.java index f3f18b41c7e..d53ab4a937e 100644 --- a/bson/src/main/org/bson/codecs/CollectionCodec.java +++ b/bson/src/main/org/bson/codecs/CollectionCodec.java @@ -20,18 +20,15 @@ import org.bson.BsonWriter; import org.bson.Transformer; import org.bson.UuidRepresentation; -import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; -import java.lang.reflect.Type; import java.util.Collection; import java.util.List; import static org.bson.assertions.Assertions.notNull; -import static org.bson.codecs.ContainerCodecHelper.getCodec; /** - * A parameterized Codec for {@code Collection}. + * A codec for {@code Collection}. * *

Supports {@link Collection}, {@link List}, {@link java.util.AbstractCollection}, {@link java.util.AbstractList}, * {@link java.util.Set}, {@link java.util.NavigableSet}, {@link java.util.SortedSet}, {@link java.util.AbstractSet} or any @@ -47,7 +44,7 @@ */ @SuppressWarnings("rawtypes") final class CollectionCodec> extends AbstractCollectionCodec - implements OverridableUuidRepresentationCodec, Parameterizable { + implements OverridableUuidRepresentationCodec { private final CodecRegistry registry; private final BsonTypeCodecMap bsonTypeCodecMap; @@ -76,17 +73,6 @@ private CollectionCodec(final CodecRegistry registry, final BsonTypeCodecMap bso this.uuidRepresentation = uuidRepresentation; } - - @SuppressWarnings("unchecked") - @Override - public Codec parameterize(final CodecRegistry codecRegistry, final List types) { - if (types.size() != 1) { - throw new CodecConfigurationException("Expected only one parameterized type for an Iterable, but found " + types.size()); - } - - return new ParameterizedCollectionCodec(getCodec(codecRegistry, types.get(0)), getEncoderClass()); - } - @Override public Codec withUuidRepresentation(final UuidRepresentation uuidRepresentation) { if (this.uuidRepresentation.equals(uuidRepresentation)) { diff --git a/bson/src/main/org/bson/codecs/CollectionCodecProvider.java b/bson/src/main/org/bson/codecs/CollectionCodecProvider.java index 74dd6b6ae21..c4c447e87bd 100644 --- a/bson/src/main/org/bson/codecs/CollectionCodecProvider.java +++ b/bson/src/main/org/bson/codecs/CollectionCodecProvider.java @@ -17,14 +17,19 @@ package org.bson.codecs; import org.bson.Transformer; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; +import java.lang.reflect.Type; import java.util.Collection; +import java.util.Collections; +import java.util.List; import java.util.Objects; import static org.bson.assertions.Assertions.notNull; import static org.bson.codecs.BsonTypeClassMap.DEFAULT_BSON_TYPE_CLASS_MAP; +import static org.bson.codecs.ContainerCodecHelper.getCodec; /** * A {@code CodecProvider} for classes than implement the {@code Collection} interface. @@ -75,12 +80,30 @@ public CollectionCodecProvider(final BsonTypeClassMap bsonTypeClassMap, final Tr } @Override - @SuppressWarnings({"unchecked", "rawtypes"}) public Codec get(final Class clazz, final CodecRegistry registry) { + return get(clazz, Collections.emptyList(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { if (Collection.class.isAssignableFrom(clazz)) { - return new CollectionCodec(registry, bsonTypeClassMap, valueTransformer, clazz); + int typeArgumentsSize = typeArguments.size(); + switch (typeArgumentsSize) { + case 0: { + @SuppressWarnings({"unchecked", "rawtypes"}) + Codec result = new CollectionCodec(registry, bsonTypeClassMap, valueTransformer, clazz); + return result; + } + case 1: { + @SuppressWarnings({"unchecked", "rawtypes"}) + Codec result = new ParameterizedCollectionCodec(getCodec(registry, typeArguments.get(0)), clazz); + return result; + } + default: { + throw new CodecConfigurationException("Expected only one type argument for a Collection, but found " + typeArgumentsSize); + } + } } - return null; } diff --git a/bson/src/main/org/bson/codecs/MapCodecProvider.java b/bson/src/main/org/bson/codecs/MapCodecProvider.java index ba16fa83f73..0db9ee975f9 100644 --- a/bson/src/main/org/bson/codecs/MapCodecProvider.java +++ b/bson/src/main/org/bson/codecs/MapCodecProvider.java @@ -17,14 +17,19 @@ package org.bson.codecs; import org.bson.Transformer; +import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; import java.util.Map; import java.util.Objects; import static org.bson.assertions.Assertions.notNull; import static org.bson.codecs.BsonTypeClassMap.DEFAULT_BSON_TYPE_CLASS_MAP; +import static org.bson.codecs.ContainerCodecHelper.getCodec; /** * A {@code CodecProvider} for the Map class and all the default Codec implementations on which it depends. @@ -74,12 +79,34 @@ public MapCodecProvider(final BsonTypeClassMap bsonTypeClassMap, final Transform } @Override - @SuppressWarnings({"rawtypes", "unchecked"}) public Codec get(final Class clazz, final CodecRegistry registry) { + return get(clazz, Collections.emptyList(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { if (Map.class.isAssignableFrom(clazz)) { - return new MapCodecV2(registry, bsonTypeClassMap, valueTransformer, clazz); + int typeArgumentsSize = typeArguments.size(); + switch (typeArgumentsSize) { + case 0: { + @SuppressWarnings({"unchecked", "rawtypes"}) + Codec result = new MapCodecV2(registry, bsonTypeClassMap, valueTransformer, clazz); + return result; + } + case 2: { + Type genericTypeOfMapKey = typeArguments.get(0); + if (!genericTypeOfMapKey.getTypeName().equals("java.lang.String")) { + throw new CodecConfigurationException("Unsupported key type for Map: " + genericTypeOfMapKey.getTypeName()); + } + @SuppressWarnings({"unchecked", "rawtypes"}) + Codec result = new ParameterizedMapCodec(getCodec(registry, typeArguments.get(1)), clazz); + return result; + } + default: { + throw new CodecConfigurationException("Expected two parameterized type for an Iterable, but found " + typeArgumentsSize); + } + } } - return null; } diff --git a/bson/src/main/org/bson/codecs/MapCodecV2.java b/bson/src/main/org/bson/codecs/MapCodecV2.java index 2aa699a22cb..d7f2351f7f5 100644 --- a/bson/src/main/org/bson/codecs/MapCodecV2.java +++ b/bson/src/main/org/bson/codecs/MapCodecV2.java @@ -20,18 +20,14 @@ import org.bson.BsonWriter; import org.bson.Transformer; import org.bson.UuidRepresentation; -import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecRegistry; -import java.lang.reflect.Type; -import java.util.List; import java.util.Map; import static org.bson.assertions.Assertions.notNull; -import static org.bson.codecs.ContainerCodecHelper.getCodec; /** - * A parameterized Codec for {@code Map}. + * A codec for {@code Map}. * *

Supports {@link Map}, {@link java.util.NavigableMap}, {@link java.util.AbstractMap} or any concrete class that implements {@code * Map} and has a public no-args constructor. If the type argument is {@code Map}, it constructs @@ -44,7 +40,7 @@ */ @SuppressWarnings("rawtypes") final class MapCodecV2> extends AbstractMapCodec - implements OverridableUuidRepresentationCodec, Parameterizable { + implements OverridableUuidRepresentationCodec { private final BsonTypeCodecMap bsonTypeCodecMap; private final CodecRegistry registry; @@ -85,20 +81,6 @@ public Codec withUuidRepresentation(final UuidRepresentation uuidRepresentati return new MapCodecV2<>(registry, bsonTypeCodecMap, valueTransformer, uuidRepresentation, getEncoderClass()); } - @SuppressWarnings("unchecked") - @Override - public Codec parameterize(final CodecRegistry codecRegistry, final List types) { - if (types.size() != 2) { - throw new CodecConfigurationException("Expected two parameterized type for an Iterable, but found " - + types.size()); - } - Type genericTypeOfMapKey = types.get(0); - if (!genericTypeOfMapKey.getTypeName().equals("java.lang.String")) { - throw new CodecConfigurationException("Unsupported key type for Map: " + genericTypeOfMapKey.getTypeName()); - } - return new ParameterizedMapCodec(getCodec(codecRegistry, types.get(1)), getEncoderClass()); - } - @Override Object readValue(final BsonReader reader, final DecoderContext decoderContext) { return ContainerCodecHelper.readValue(reader, decoderContext, bsonTypeCodecMap, uuidRepresentation, registry, valueTransformer); diff --git a/bson/src/main/org/bson/codecs/Parameterizable.java b/bson/src/main/org/bson/codecs/Parameterizable.java index 917801b891c..479ab205524 100644 --- a/bson/src/main/org/bson/codecs/Parameterizable.java +++ b/bson/src/main/org/bson/codecs/Parameterizable.java @@ -16,6 +16,7 @@ package org.bson.codecs; +import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; import java.lang.reflect.Type; @@ -26,7 +27,13 @@ * An interface indicating that a Codec is for a type that can be parameterized by generic types. * * @since 4.8 + * @deprecated Since 4.10. Instead of implementing {@link Parameterizable} for a custom {@link Codec}, + * users should implement {@link CodecProvider#get(Class, List, CodecRegistry)} for a custom {@link CodecProvider}. */ +@Deprecated +// After releasing this interface, we realized that our implementations of `Parameterizable.parameterize` were doing what +// `CodecProvider.get` is supposed to be doing. As a result, we introduced a new default method to `CodecProvider`, +// and deprecated `Parameterizable`. public interface Parameterizable { /** * Recursively parameterize the codec with the given registry and generic type arguments. diff --git a/bson/src/main/org/bson/codecs/configuration/CodecProvider.java b/bson/src/main/org/bson/codecs/configuration/CodecProvider.java index b667067d4ff..8f01c60b551 100644 --- a/bson/src/main/org/bson/codecs/configuration/CodecProvider.java +++ b/bson/src/main/org/bson/codecs/configuration/CodecProvider.java @@ -18,6 +18,10 @@ import org.bson.codecs.Codec; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.List; + /** * A provider of {@code Codec} instances. Typically, an instance of a class implementing this interface would be used to construct a * {@code CodecRegistry}. @@ -34,10 +38,35 @@ public interface CodecProvider { /** * Get a {@code Codec} using the given context, which includes, most importantly, the Class for which a {@code Codec} is required. * + *

This method is called by the driver only if {@link #get(Class, List, CodecRegistry)} is not overridden, + * or is overridden such that it calls this method.

+ * * @param clazz the Class for which to get a Codec * @param registry the registry to use for resolving dependent Codec instances * @param the type of the class for which a Codec is required * @return the Codec instance, which may be null, if this source is unable to provide one for the requested Class */ Codec get(Class clazz, CodecRegistry registry); + + /** + * Get a {@code Codec} using the given context, which includes, most importantly, the Class for which a {@code Codec} is required. + * + *

The default implementation delegates to {@link #get(Class, CodecRegistry)}, thus not propagating {@code typeArguments} + * when it uses the {@code registry}.

+ * + * @param clazz the Class for which to get a Codec + * @param typeArguments The type arguments for the {@code clazz}. The size of the list is either equal to the + * number of type parameters of the {@code clazz}, or is zero. + * For example, if {@code clazz} is {@link Collection}{@code .class}, then the size of {@code typeArguments} is one, + * since {@link Collection} has a single type parameter. + * The list may be {@linkplain List#isEmpty() empty} either because the {@code clazz} is not generic, + * or because another {@link CodecProvider} did not propagate {@code clazz}'s type arguments to the {@code registry} when using it. + * @param registry the registry to use for resolving dependent Codec instances + * @return the Codec instance, which may be null, if this source is unable to provide one for the requested Class + * @param the type of the class for which a Codec is required + * @since 4.10 + */ + default Codec get(Class clazz, List typeArguments, CodecRegistry registry) { + return get(clazz, registry); + } } diff --git a/bson/src/main/org/bson/codecs/configuration/OverridableUuidRepresentationCodecProvider.java b/bson/src/main/org/bson/codecs/configuration/OverridableUuidRepresentationCodecProvider.java index edba21a9a07..e1c6223cfb3 100644 --- a/bson/src/main/org/bson/codecs/configuration/OverridableUuidRepresentationCodecProvider.java +++ b/bson/src/main/org/bson/codecs/configuration/OverridableUuidRepresentationCodecProvider.java @@ -20,7 +20,12 @@ import org.bson.codecs.Codec; import org.bson.codecs.OverridableUuidRepresentationCodec; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + import static org.bson.assertions.Assertions.notNull; +import static org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider; final class OverridableUuidRepresentationCodecProvider implements CodecProvider { @@ -33,11 +38,17 @@ final class OverridableUuidRepresentationCodecProvider implements CodecProvider } @Override - @SuppressWarnings({"unchecked"}) public Codec get(final Class clazz, final CodecRegistry registry) { - Codec codec = wrapped.get(clazz, registry); + return get(clazz, Collections.emptyList(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { + Codec codec = getFromCodecProvider(wrapped, clazz, typeArguments, registry); if (codec instanceof OverridableUuidRepresentationCodec) { - return ((OverridableUuidRepresentationCodec) codec).withUuidRepresentation(uuidRepresentation); + @SuppressWarnings("unchecked") + Codec codecWithUuidRepresentation = ((OverridableUuidRepresentationCodec) codec).withUuidRepresentation(uuidRepresentation); + codec = codecWithUuidRepresentation; } return codec; } diff --git a/bson/src/main/org/bson/internal/ChildCodecRegistry.java b/bson/src/main/org/bson/internal/ChildCodecRegistry.java index 21c3df25e2b..73bb46630de 100644 --- a/bson/src/main/org/bson/internal/ChildCodecRegistry.java +++ b/bson/src/main/org/bson/internal/ChildCodecRegistry.java @@ -21,6 +21,7 @@ import org.bson.codecs.configuration.CodecRegistry; import java.lang.reflect.Type; +import java.util.Collections; import java.util.List; import java.util.Objects; import java.util.Optional; @@ -71,7 +72,6 @@ public Codec get(final Class clazz) { @Override public Codec get(final Class clazz, final List typeArguments) { notNull("typeArguments", typeArguments); - isTrueArgument("typeArguments is not empty", !typeArguments.isEmpty()); isTrueArgument(format("typeArguments size should equal the number of type parameters in class %s, but is %d", clazz, typeArguments.size()), clazz.getTypeParameters().length == typeArguments.size()); @@ -84,7 +84,12 @@ public Codec get(final Class clazz, final List typeArguments) { @Override public Codec get(final Class clazz, final CodecRegistry registry) { - return this.registry.get(clazz, registry); + return get(clazz, Collections.emptyList(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { + return this.registry.get(clazz, typeArguments, registry); } private Boolean hasCycles(final Class theClass) { diff --git a/bson/src/main/org/bson/internal/ProvidersCodecRegistry.java b/bson/src/main/org/bson/internal/ProvidersCodecRegistry.java index 97c5d8eeed3..3decf4b1d1d 100644 --- a/bson/src/main/org/bson/internal/ProvidersCodecRegistry.java +++ b/bson/src/main/org/bson/internal/ProvidersCodecRegistry.java @@ -17,17 +17,19 @@ package org.bson.internal; import org.bson.codecs.Codec; -import org.bson.codecs.Parameterizable; import org.bson.codecs.configuration.CodecConfigurationException; import org.bson.codecs.configuration.CodecProvider; import org.bson.codecs.configuration.CodecRegistry; import org.bson.internal.CodecCache.CodecCacheKey; +import javax.annotation.Nullable; import java.lang.reflect.Type; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import static java.lang.String.format; +import static java.util.Collections.emptyList; import static org.bson.assertions.Assertions.isTrueArgument; import static org.bson.assertions.Assertions.notNull; @@ -51,16 +53,21 @@ public Codec get(final Class clazz) { @Override public Codec get(final Class clazz, final List typeArguments) { notNull("typeArguments", typeArguments); - isTrueArgument("typeArguments is not empty", !typeArguments.isEmpty()); isTrueArgument(format("typeArguments size should equal the number of type parameters in class %s, but is %d", clazz, typeArguments.size()), clazz.getTypeParameters().length == typeArguments.size()); return get(new ChildCodecRegistry<>(this, clazz, typeArguments)); } + @Override public Codec get(final Class clazz, final CodecRegistry registry) { + return get(clazz, Collections.emptyList(), registry); + } + + @Override + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { for (CodecProvider provider : codecProviders) { - Codec codec = provider.get(clazz, registry); + Codec codec = getFromCodecProvider(provider, clazz, typeArguments, registry); if (codec != null) { return codec; } @@ -68,16 +75,12 @@ public Codec get(final Class clazz, final CodecRegistry registry) { return null; } - @SuppressWarnings({"unchecked"}) public Codec get(final ChildCodecRegistry context) { CodecCacheKey codecCacheKey = new CodecCacheKey(context.getCodecClass(), context.getTypes().orElse(null)); return codecCache.get(codecCacheKey).orElseGet(() -> { for (CodecProvider provider : codecProviders) { - Codec codec = provider.get(context.getCodecClass(), context); + Codec codec = getFromCodecProvider(provider, context.getCodecClass(), context.getTypes().orElse(emptyList()), context); if (codec != null) { - if (codec instanceof Parameterizable && context.getTypes().isPresent()) { - codec = (Codec) ((Parameterizable) codec).parameterize(context, context.getTypes().get()); - } return codecCache.putIfAbsent(codecCacheKey, codec); } } @@ -85,6 +88,20 @@ public Codec get(final ChildCodecRegistry context) { }); } + @Nullable + @SuppressWarnings("deprecation") + public static Codec getFromCodecProvider(final CodecProvider provider, + final Class clazz, final List typeArguments, final CodecRegistry registry) { + Codec codec = provider.get(clazz, typeArguments, registry); + // `Parameterizable` is deprecated, but we still have to support it until it is removed + if (codec instanceof org.bson.codecs.Parameterizable && !typeArguments.isEmpty()) { + @SuppressWarnings("unchecked") + Codec parameterizedCodec = (Codec) ((org.bson.codecs.Parameterizable) codec).parameterize(registry, typeArguments); + codec = parameterizedCodec; + } + return codec; + } + @Override public boolean equals(final Object o) { if (this == o) { diff --git a/bson/src/test/unit/org/bson/codecs/CollectionCodecProviderTest.java b/bson/src/test/unit/org/bson/codecs/CollectionCodecProviderTest.java new file mode 100644 index 00000000000..d15a992f251 --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/CollectionCodecProviderTest.java @@ -0,0 +1,52 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.conversions.Bson; +import org.junit.jupiter.api.Test; + +import java.util.Set; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class CollectionCodecProviderTest { + @Test + void shouldReturnNullForNonCollection() { + CollectionCodecProvider provider = new CollectionCodecProvider(); + assertNull(provider.get(String.class, Bson.DEFAULT_CODEC_REGISTRY)); + } + + @Test + void shouldReturnCollectionCodecForCollection() { + CollectionCodecProvider provider = new CollectionCodecProvider(); + @SuppressWarnings({"rawtypes", "unchecked"}) + Codec> codec = (Codec>) (Codec) provider.get(Set.class, Bson.DEFAULT_CODEC_REGISTRY); + assertTrue(codec instanceof CollectionCodec); + CollectionCodec> recordCodec = (CollectionCodec>) codec; + assertEquals(Set.class, recordCodec.getEncoderClass()); + } + + @Test + public void shouldReturnCollectionCodecForCollectionUsingDefaultRegistry() { + @SuppressWarnings({"rawtypes", "unchecked"}) + Codec> codec = (Codec>) (Codec) Bson.DEFAULT_CODEC_REGISTRY.get(Set.class); + assertTrue(codec instanceof CollectionCodec); + CollectionCodec> recordCodec = (CollectionCodec>) codec; + assertEquals(Set.class, recordCodec.getEncoderClass()); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/CollectionCodecSpecification.groovy b/bson/src/test/unit/org/bson/codecs/CollectionCodecSpecification.groovy index d12c0db96bb..269032b8014 100644 --- a/bson/src/test/unit/org/bson/codecs/CollectionCodecSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/CollectionCodecSpecification.groovy @@ -30,6 +30,7 @@ import java.lang.reflect.ParameterizedType import java.time.Instant import java.util.concurrent.CopyOnWriteArrayList +import static java.util.Arrays.asList import static org.bson.BsonDocument.parse import static org.bson.UuidRepresentation.C_SHARP_LEGACY import static org.bson.UuidRepresentation.JAVA_LEGACY @@ -195,15 +196,15 @@ class CollectionCodecSpecification extends Specification { def 'should parameterize'() { given: - def codec = new CollectionCodec(REGISTRY, new BsonTypeClassMap(), null, Collection) + def codec = fromProviders(new Jsr310CodecProvider(), REGISTRY).get( + Collection, + asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) def writer = new BsonDocumentWriter(new BsonDocument()) def reader = new BsonDocumentReader(writer.getDocument()) def instants = [ ['firstMap': [Instant.ofEpochMilli(1), Instant.ofEpochMilli(2)]], ['secondMap': [Instant.ofEpochMilli(3), Instant.ofEpochMilli(4)]]] when: - codec = codec.parameterize(fromProviders(new Jsr310CodecProvider(), REGISTRY), - Arrays.asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) writer.writeStartDocument() writer.writeName('instants') codec.encode(writer, instants, EncoderContext.builder().build()) diff --git a/bson/src/test/unit/org/bson/codecs/MapCodecProviderTest.java b/bson/src/test/unit/org/bson/codecs/MapCodecProviderTest.java new file mode 100644 index 00000000000..5e850a8f64d --- /dev/null +++ b/bson/src/test/unit/org/bson/codecs/MapCodecProviderTest.java @@ -0,0 +1,53 @@ +/* + * Copyright 2008-present MongoDB, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.bson.codecs; + +import org.bson.conversions.Bson; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class MapCodecProviderTest { + @Test + void shouldReturnNullForNonMap() { + MapCodecProvider provider = new MapCodecProvider(); + assertNull(provider.get(String.class, Bson.DEFAULT_CODEC_REGISTRY)); + } + + @Test + void shouldReturnMapCodecForMap() { + MapCodecProvider provider = new MapCodecProvider(); + @SuppressWarnings({"rawtypes", "unchecked"}) + Codec> codec = (Codec>) (Codec) provider.get(Map.class, Bson.DEFAULT_CODEC_REGISTRY); + assertTrue(codec instanceof MapCodecV2); + MapCodecV2> recordCodec = (MapCodecV2>) codec; + assertEquals(Map.class, recordCodec.getEncoderClass()); + } + + @Test + public void shouldReturnMapCodecForMapUsingDefaultRegistry() { + @SuppressWarnings({"rawtypes", "unchecked"}) + Codec> codec = (Codec>) (Codec) Bson.DEFAULT_CODEC_REGISTRY.get(Map.class); + assertTrue(codec instanceof MapCodecV2); + MapCodecV2> recordCodec = (MapCodecV2>) codec; + assertEquals(Map.class, recordCodec.getEncoderClass()); + } +} diff --git a/bson/src/test/unit/org/bson/codecs/MapCodecV2Specification.groovy b/bson/src/test/unit/org/bson/codecs/MapCodecV2Specification.groovy index 20eff5d7ad3..911053a900a 100644 --- a/bson/src/test/unit/org/bson/codecs/MapCodecV2Specification.groovy +++ b/bson/src/test/unit/org/bson/codecs/MapCodecV2Specification.groovy @@ -254,15 +254,16 @@ class MapCodecV2Specification extends Specification { def 'should parameterize'() { given: - def codec = new MapCodecV2(REGISTRY, new BsonTypeClassMap(), null, Map) + def codec = fromProviders(new Jsr310CodecProvider(), REGISTRY).get( + Map, + asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) + def writer = new BsonDocumentWriter(new BsonDocument()) def reader = new BsonDocumentReader(writer.getDocument()) def instants = ['firstMap': [Instant.ofEpochMilli(1), Instant.ofEpochMilli(2)], 'secondMap': [Instant.ofEpochMilli(3), Instant.ofEpochMilli(4)]] when: - codec = codec.parameterize(fromProviders(new Jsr310CodecProvider(), REGISTRY), - asList(((ParameterizedType) Container.getMethod('getInstants').genericReturnType).actualTypeArguments)) writer.writeStartDocument() writer.writeName('instants') codec.encode(writer, instants, EncoderContext.builder().build()) diff --git a/bson/src/test/unit/org/bson/codecs/configuration/CodeRegistriesSpecification.groovy b/bson/src/test/unit/org/bson/codecs/configuration/CodeRegistriesSpecification.groovy index 7b4735250aa..9cae58f7468 100644 --- a/bson/src/test/unit/org/bson/codecs/configuration/CodeRegistriesSpecification.groovy +++ b/bson/src/test/unit/org/bson/codecs/configuration/CodeRegistriesSpecification.groovy @@ -16,19 +16,33 @@ package org.bson.codecs.configuration +import org.bson.BsonArray +import org.bson.BsonDateTime +import org.bson.BsonDocument +import org.bson.BsonDocumentReader +import org.bson.BsonDocumentWriter import org.bson.BsonInt32 import org.bson.codecs.BsonInt32Codec import org.bson.codecs.BsonValueCodecProvider +import org.bson.codecs.CollectionCodecProvider +import org.bson.codecs.DecoderContext +import org.bson.codecs.EncoderContext import org.bson.codecs.IntegerCodec import org.bson.codecs.LongCodec +import org.bson.codecs.MapCodecProvider import org.bson.codecs.UuidCodec import org.bson.codecs.ValueCodecProvider +import org.bson.codecs.jsr310.Jsr310CodecProvider import org.bson.internal.ProvidersCodecRegistry import spock.lang.Specification +import java.lang.reflect.ParameterizedType +import java.time.Instant + import static CodecRegistries.fromCodecs import static CodecRegistries.fromProviders import static CodecRegistries.fromRegistries +import static java.util.Arrays.asList import static org.bson.UuidRepresentation.STANDARD import static org.bson.UuidRepresentation.UNSPECIFIED import static org.bson.codecs.configuration.CodecRegistries.withUuidRepresentation @@ -91,4 +105,48 @@ class CodeRegistriesSpecification extends Specification { then: uuidCodec.getUuidRepresentation() == STANDARD } + + def 'withUuidRepresentation should not break parameterization'() { + given: + def registry = fromProviders( + new Jsr310CodecProvider(), + new ValueCodecProvider(), + withUuidRepresentation(fromProviders(new CollectionCodecProvider()), STANDARD), + withUuidRepresentation(fromProviders(new MapCodecProvider()), STANDARD) + ) + def codec = registry.get(Collection, asList( + ((ParameterizedType) CodeRegistriesSpecification.getMethod('parameterizedTypeProvider').genericReturnType) + .actualTypeArguments)) + def writer = new BsonDocumentWriter(new BsonDocument()) + def reader = new BsonDocumentReader(writer.getDocument()) + def value = [ + ['firstMap': [Instant.ofEpochMilli(1), Instant.ofEpochMilli(2)]], + ['secondMap': [Instant.ofEpochMilli(3), Instant.ofEpochMilli(4)]]] + when: + writer.writeStartDocument() + writer.writeName('value') + codec.encode(writer, value, EncoderContext.builder().build()) + writer.writeEndDocument() + + then: + writer.getDocument() == new BsonDocument() + .append('value', new BsonArray( + [ + new BsonDocument('firstMap', new BsonArray([new BsonDateTime(1), new BsonDateTime(2)])), + new BsonDocument('secondMap', new BsonArray([new BsonDateTime(3), new BsonDateTime(4)])) + ])) + + when: + reader.readStartDocument() + reader.readName('value') + def decodedValue = codec.decode(reader, DecoderContext.builder().build()) + + then: + decodedValue == value + } + + @SuppressWarnings('unused') + List>> parameterizedTypeProvider() { + [] + } } diff --git a/driver-core/src/main/com/mongodb/Jep395RecordCodecProvider.java b/driver-core/src/main/com/mongodb/Jep395RecordCodecProvider.java index 5de07e5f620..69d0ab12233 100644 --- a/driver-core/src/main/com/mongodb/Jep395RecordCodecProvider.java +++ b/driver-core/src/main/com/mongodb/Jep395RecordCodecProvider.java @@ -22,13 +22,20 @@ import org.bson.codecs.configuration.CodecRegistry; import org.bson.codecs.record.RecordCodecProvider; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + import static com.mongodb.internal.VisibleForTesting.AccessModifier.PRIVATE; +import static org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider; /** * A CodecProvider for Java Records. - * - *

Requires java.lang.Record support - eg Java 17 or greater.

+ * Delegates to {@code org.bson.codecs.record.RecordCodecProvider}. + * If neither the runtime supports {@code java.lang.Record}, which was introduced in Java SE 17, + * nor {@code org.bson.codecs.record.RecordCodecProvider} is available, + * {@linkplain CodecProvider#get(Class, CodecRegistry) provides} {@code null}. * * @since 4.6 */ @@ -53,10 +60,18 @@ public class Jep395RecordCodecProvider implements CodecProvider { @Override @Nullable public Codec get(final Class clazz, final CodecRegistry registry) { - return RECORD_CODEC_PROVIDER != null ? RECORD_CODEC_PROVIDER.get(clazz, registry) : null; + return get(clazz, Collections.emptyList(), registry); + } + + @Override + @Nullable + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { + return RECORD_CODEC_PROVIDER != null ? getFromCodecProvider(RECORD_CODEC_PROVIDER, clazz, typeArguments, registry) : null; } /** + * This method is not part of the public API and may be removed or changed at any time. + * * @return true if records are supported */ @VisibleForTesting(otherwise = PRIVATE) @@ -64,4 +79,3 @@ public boolean hasRecordSupport() { return RECORD_CODEC_PROVIDER != null; } } - diff --git a/driver-core/src/main/com/mongodb/KotlinCodecProvider.java b/driver-core/src/main/com/mongodb/KotlinCodecProvider.java index 045dd8c7e86..74f88ed0956 100644 --- a/driver-core/src/main/com/mongodb/KotlinCodecProvider.java +++ b/driver-core/src/main/com/mongodb/KotlinCodecProvider.java @@ -22,10 +22,18 @@ import org.bson.codecs.kotlin.DataClassCodecProvider; import org.bson.codecs.kotlinx.KotlinSerializerCodecProvider; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.List; + +import static org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider; + /** * A CodecProvider for Kotlin data classes. - * - *

Requires the bson-kotlin package and / or the bson-kotlinx package.

+ * Delegates to {@code org.bson.codecs.kotlinx.KotlinSerializerCodecProvider} + * and falls back to {@code org.bson.codecs.kotlin.DataClassCodecProvider}. + * If neither bson-kotlin package nor the bson-kotlinx package is available, + * {@linkplain CodecProvider#get(Class, CodecRegistry) provides} {@code null}. * * @since 4.10 */ @@ -60,16 +68,21 @@ public class KotlinCodecProvider implements CodecProvider { @Override @Nullable public Codec get(final Class clazz, final CodecRegistry registry) { + return get(clazz, Collections.emptyList(), registry); + } + + @Override + @Nullable + public Codec get(final Class clazz, final List typeArguments, final CodecRegistry registry) { Codec codec = null; if (KOTLIN_SERIALIZABLE_CODEC_PROVIDER != null) { - codec = KOTLIN_SERIALIZABLE_CODEC_PROVIDER.get(clazz, registry); + codec = getFromCodecProvider(KOTLIN_SERIALIZABLE_CODEC_PROVIDER, clazz, typeArguments, registry); } if (codec == null && DATA_CLASS_CODEC_PROVIDER != null) { - codec = DATA_CLASS_CODEC_PROVIDER.get(clazz, registry); + codec = getFromCodecProvider(DATA_CLASS_CODEC_PROVIDER, clazz, typeArguments, registry); } return codec; } } -