diff --git a/bson-kotlinx/build.gradle.kts b/bson-kotlinx/build.gradle.kts index 5f707239581..a278b4a3ab2 100644 --- a/bson-kotlinx/build.gradle.kts +++ b/bson-kotlinx/build.gradle.kts @@ -42,7 +42,10 @@ ext.set("kotlinxDatetimeVersion", "0.4.0") val kotlinxDatetimeVersion: String by ext -java { registerFeature("dateTimeSupport") { usingSourceSet(sourceSets["main"]) } } +java { + registerFeature("dateTimeSupport") { usingSourceSet(sourceSets["main"]) } + registerFeature("jsonSupport") { usingSourceSet(sourceSets["main"]) } +} dependencies { // Align versions of all Kotlin components @@ -52,6 +55,7 @@ dependencies { implementation(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.5.0")) implementation("org.jetbrains.kotlinx:kotlinx-serialization-core") "dateTimeSupportImplementation"("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") + "jsonSupportImplementation"("org.jetbrains.kotlinx:kotlinx-serialization-json") api(project(path = ":bson", configuration = "default")) implementation("org.jetbrains.kotlin:kotlin-reflect") @@ -59,6 +63,7 @@ dependencies { testImplementation("org.jetbrains.kotlin:kotlin-test-junit") testImplementation(project(path = ":driver-core", configuration = "default")) testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:$kotlinxDatetimeVersion") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") } kotlin { explicitApi() } diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt index 38d9c23309f..68ecbbabc13 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonDecoder.kt @@ -27,6 +27,7 @@ import kotlinx.serialization.encoding.AbstractDecoder import kotlinx.serialization.encoding.CompositeDecoder import kotlinx.serialization.encoding.CompositeDecoder.Companion.DECODE_DONE import kotlinx.serialization.encoding.CompositeDecoder.Companion.UNKNOWN_NAME +import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.modules.SerializersModule import org.bson.AbstractBsonReader import org.bson.BsonInvalidOperationException @@ -36,6 +37,10 @@ import org.bson.BsonType import org.bson.BsonValue import org.bson.codecs.BsonValueCodec import org.bson.codecs.DecoderContext +import org.bson.codecs.kotlinx.BsonDecoder.Companion.createBsonArrayDecoder +import org.bson.codecs.kotlinx.BsonDecoder.Companion.createBsonDocumentDecoder +import org.bson.codecs.kotlinx.BsonDecoder.Companion.createBsonMapDecoder +import org.bson.codecs.kotlinx.BsonDecoder.Companion.createBsonPolymorphicDecoder import org.bson.internal.NumberCodecHelper import org.bson.internal.StringCodecHelper import org.bson.types.ObjectId @@ -45,34 +50,93 @@ import org.bson.types.ObjectId * * For custom serialization handlers */ -public sealed interface BsonDecoder { +@ExperimentalSerializationApi +internal sealed interface BsonDecoder : Decoder, CompositeDecoder { + + /** Factory helper for creating concrete BsonDecoder implementations */ + companion object { + + @Suppress("SwallowedException") + private val hasJsonDecoder: Boolean by lazy { + try { + Class.forName("kotlinx.serialization.json.JsonDecoder") + true + } catch (e: ClassNotFoundException) { + false + } + } + + fun createBsonDecoder( + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonDecoder { + return if (hasJsonDecoder) JsonBsonDecoderImpl(reader, serializersModule, configuration) + else BsonDecoderImpl(reader, serializersModule, configuration) + } + + fun createBsonArrayDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonArrayDecoder { + return if (hasJsonDecoder) JsonBsonArrayDecoder(descriptor, reader, serializersModule, configuration) + else BsonArrayDecoder(descriptor, reader, serializersModule, configuration) + } + + fun createBsonDocumentDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonDocumentDecoder { + return if (hasJsonDecoder) JsonBsonDocumentDecoder(descriptor, reader, serializersModule, configuration) + else BsonDocumentDecoder(descriptor, reader, serializersModule, configuration) + } + + fun createBsonPolymorphicDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonPolymorphicDecoder { + return if (hasJsonDecoder) JsonBsonPolymorphicDecoder(descriptor, reader, serializersModule, configuration) + else BsonPolymorphicDecoder(descriptor, reader, serializersModule, configuration) + } + + fun createBsonMapDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonMapDecoder { + return if (hasJsonDecoder) JsonBsonMapDecoder(descriptor, reader, serializersModule, configuration) + else BsonMapDecoder(descriptor, reader, serializersModule, configuration) + } + } /** @return the decoded ObjectId */ - public fun decodeObjectId(): ObjectId + fun decodeObjectId(): ObjectId /** @return the decoded BsonValue */ - public fun decodeBsonValue(): BsonValue - - /** @return the BsonReader */ - public fun reader(): BsonReader + fun decodeBsonValue(): BsonValue } -@ExperimentalSerializationApi -internal open class DefaultBsonDecoder( - internal val reader: AbstractBsonReader, +@OptIn(ExperimentalSerializationApi::class) +internal sealed class AbstractBsonDecoder( + val reader: AbstractBsonReader, override val serializersModule: SerializersModule, - internal val configuration: BsonConfiguration + val configuration: BsonConfiguration ) : BsonDecoder, AbstractDecoder() { - private data class ElementMetadata(val name: String, val nullable: Boolean, var processed: Boolean = false) - private var elementsMetadata: Array? = null - private var currentIndex: Int = UNKNOWN_INDEX - companion object { - val validKeyKinds = setOf(PrimitiveKind.STRING, PrimitiveKind.CHAR, SerialKind.ENUM) + val bsonValueCodec = BsonValueCodec() const val UNKNOWN_INDEX = -10 + val validKeyKinds = setOf(PrimitiveKind.STRING, PrimitiveKind.CHAR, SerialKind.ENUM) + fun validateCurrentBsonType( - reader: AbstractBsonReader, + reader: BsonReader, expectedType: BsonType, descriptor: SerialDescriptor, actualType: (descriptor: SerialDescriptor) -> String = { it.kind.toString() } @@ -87,6 +151,10 @@ internal open class DefaultBsonDecoder( } } + private data class ElementMetadata(val name: String, val nullable: Boolean, var processed: Boolean = false) + private var elementsMetadata: Array? = null + private var currentIndex: Int = UNKNOWN_INDEX + private fun initElementMetadata(descriptor: SerialDescriptor) { if (this.elementsMetadata != null) return val elementsMetadata = @@ -134,14 +202,13 @@ internal open class DefaultBsonDecoder( ?: UNKNOWN_NAME } - @Suppress("ReturnCount") override fun beginStructure(descriptor: SerialDescriptor): CompositeDecoder { return when (descriptor.kind) { - is StructureKind.LIST -> BsonArrayDecoder(descriptor, reader, serializersModule, configuration) - is PolymorphicKind -> PolymorphicDecoder(descriptor, reader, serializersModule, configuration) + is PolymorphicKind -> createBsonPolymorphicDecoder(descriptor, reader, serializersModule, configuration) + is StructureKind.LIST -> createBsonArrayDecoder(descriptor, reader, serializersModule, configuration) is StructureKind.CLASS, - StructureKind.OBJECT -> BsonDocumentDecoder(descriptor, reader, serializersModule, configuration) - is StructureKind.MAP -> MapDecoder(descriptor, reader, serializersModule, configuration) + StructureKind.OBJECT -> createBsonDocumentDecoder(descriptor, reader, serializersModule, configuration) + is StructureKind.MAP -> createBsonMapDecoder(descriptor, reader, serializersModule, configuration) else -> throw SerializationException("Primitives are not supported at top-level") } } @@ -152,18 +219,15 @@ internal open class DefaultBsonDecoder( is StructureKind.MAP, StructureKind.CLASS, StructureKind.OBJECT -> reader.readEndDocument() - else -> super.endStructure(descriptor) + else -> {} } } override fun decodeByte(): Byte = NumberCodecHelper.decodeByte(reader) - override fun decodeChar(): Char = StringCodecHelper.decodeChar(reader) override fun decodeFloat(): Float = NumberCodecHelper.decodeFloat(reader) - override fun decodeShort(): Short = NumberCodecHelper.decodeShort(reader) override fun decodeBoolean(): Boolean = reader.readBoolean() - override fun decodeDouble(): Double = NumberCodecHelper.decodeDouble(reader) override fun decodeInt(): Int = NumberCodecHelper.decodeInt(reader) override fun decodeLong(): Long = NumberCodecHelper.decodeLong(reader) @@ -183,7 +247,6 @@ internal open class DefaultBsonDecoder( override fun decodeObjectId(): ObjectId = readOrThrow({ reader.readObjectId() }, BsonType.OBJECT_ID) override fun decodeBsonValue(): BsonValue = bsonValueCodec.decode(reader, DecoderContext.builder().build()) - override fun reader(): BsonReader = reader private inline fun readOrThrow(action: () -> T, bsonType: BsonType): T { return try { @@ -197,13 +260,20 @@ internal open class DefaultBsonDecoder( } } -@OptIn(ExperimentalSerializationApi::class) -private class BsonArrayDecoder( +/** The default Bson Decoder implementation */ +internal open class BsonDecoderImpl( + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : AbstractBsonDecoder(reader, serializersModule, configuration) + +/** The Bson array decoder */ +internal open class BsonArrayDecoder( descriptor: SerialDescriptor, reader: AbstractBsonReader, serializersModule: SerializersModule, configuration: BsonConfiguration -) : DefaultBsonDecoder(reader, serializersModule, configuration) { +) : AbstractBsonDecoder(reader, serializersModule, configuration) { init { validateCurrentBsonType(reader, BsonType.ARRAY, descriptor) @@ -218,13 +288,29 @@ private class BsonArrayDecoder( } } +/** The Bson document decoder */ @OptIn(ExperimentalSerializationApi::class) -private class PolymorphicDecoder( +internal open class BsonDocumentDecoder( descriptor: SerialDescriptor, reader: AbstractBsonReader, serializersModule: SerializersModule, configuration: BsonConfiguration -) : DefaultBsonDecoder(reader, serializersModule, configuration) { +) : AbstractBsonDecoder(reader, serializersModule, configuration) { + + init { + validateCurrentBsonType(reader, BsonType.DOCUMENT, descriptor) { it.serialName } + reader.readStartDocument() + } +} + +/** The Bson polymorphic class decoder */ +@OptIn(ExperimentalSerializationApi::class) +internal open class BsonPolymorphicDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : AbstractBsonDecoder(reader, serializersModule, configuration) { private var index = 0 private var mark: BsonReaderMark? @@ -239,7 +325,7 @@ private class PolymorphicDecoder( it.reset() mark = null } - return deserializer.deserialize(DefaultBsonDecoder(reader, serializersModule, configuration)) + return deserializer.deserialize(BsonDecoder.createBsonDecoder(reader, serializersModule, configuration)) } override fun decodeElementIndex(descriptor: SerialDescriptor): Int { @@ -266,27 +352,14 @@ private class PolymorphicDecoder( } } +/** The Bson map decoder */ @OptIn(ExperimentalSerializationApi::class) -private class BsonDocumentDecoder( - descriptor: SerialDescriptor, - reader: AbstractBsonReader, - serializersModule: SerializersModule, - configuration: BsonConfiguration -) : DefaultBsonDecoder(reader, serializersModule, configuration) { - init { - validateCurrentBsonType(reader, BsonType.DOCUMENT, descriptor) { it.serialName } - reader.readStartDocument() - } -} - -@OptIn(ExperimentalSerializationApi::class) -private class MapDecoder( +internal open class BsonMapDecoder( descriptor: SerialDescriptor, reader: AbstractBsonReader, serializersModule: SerializersModule, configuration: BsonConfiguration -) : DefaultBsonDecoder(reader, serializersModule, configuration) { - +) : AbstractBsonDecoder(reader, serializersModule, configuration) { private var index = 0 private var isKey = false diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt index 75080254cdb..899b1b7a981 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/BsonEncoder.kt @@ -25,6 +25,7 @@ import kotlinx.serialization.descriptors.SerialKind import kotlinx.serialization.descriptors.StructureKind import kotlinx.serialization.encoding.AbstractEncoder import kotlinx.serialization.encoding.CompositeEncoder +import kotlinx.serialization.encoding.Encoder import kotlinx.serialization.modules.SerializersModule import org.bson.BsonValue import org.bson.BsonWriter @@ -37,31 +38,57 @@ import org.bson.types.ObjectId * * For custom serialization handlers */ -public sealed interface BsonEncoder { +@ExperimentalSerializationApi +internal sealed interface BsonEncoder : Encoder, CompositeEncoder { + + /** Factory helper for creating concrete BsonEncoder implementations */ + companion object { + @Suppress("SwallowedException") + private val hasJsonEncoder: Boolean by lazy { + try { + Class.forName("kotlinx.serialization.json.JsonEncoder") + true + } catch (e: ClassNotFoundException) { + false + } + } + + fun createBsonEncoder( + writer: BsonWriter, + serializersModule: SerializersModule, + configuration: BsonConfiguration + ): BsonEncoder { + return if (hasJsonEncoder) JsonBsonEncoder(writer, serializersModule, configuration) + else BsonEncoderImpl(writer, serializersModule, configuration) + } + } /** * Encodes an ObjectId * * @param value the ObjectId */ - public fun encodeObjectId(value: ObjectId) + fun encodeObjectId(value: ObjectId) /** * Encodes a BsonValue * * @param value the BsonValue */ - public fun encodeBsonValue(value: BsonValue) - - /** @return the BsonWriter */ - public fun writer(): BsonWriter + fun encodeBsonValue(value: BsonValue) } -@ExperimentalSerializationApi -internal class DefaultBsonEncoder( - private val writer: BsonWriter, +/** + * The default BsonEncoder implementation + * + * Unlike BsonDecoder implementations, state is shared when encoding, so a single class is used to encode Bson Arrays, + * Documents, Polymorphic types and Maps. + */ +@OptIn(ExperimentalSerializationApi::class) +internal open class BsonEncoderImpl( + val writer: BsonWriter, override val serializersModule: SerializersModule, - private val configuration: BsonConfiguration + val configuration: BsonConfiguration ) : BsonEncoder, AbstractEncoder() { companion object { @@ -72,19 +99,19 @@ internal class DefaultBsonEncoder( private var isPolymorphic = false private var state = STATE.VALUE private var mapState = MapState() - private val deferredElementHandler: DeferredElementHandler = DeferredElementHandler() + internal val deferredElementHandler: DeferredElementHandler = DeferredElementHandler() override fun shouldEncodeElementDefault(descriptor: SerialDescriptor, index: Int): Boolean = configuration.encodeDefaults override fun beginStructure(descriptor: SerialDescriptor): CompositeEncoder { when (descriptor.kind) { - is StructureKind.LIST -> writer.writeStartArray() is PolymorphicKind -> { writer.writeStartDocument() writer.writeName(configuration.classDiscriminator) isPolymorphic = true } + is StructureKind.LIST -> writer.writeStartArray() is StructureKind.CLASS, StructureKind.OBJECT -> { if (isPolymorphic) { @@ -99,7 +126,7 @@ internal class DefaultBsonEncoder( } else -> throw SerializationException("Primitives are not supported at top-level") } - return super.beginStructure(descriptor) + return this } override fun endStructure(descriptor: SerialDescriptor) { @@ -108,7 +135,7 @@ internal class DefaultBsonEncoder( StructureKind.MAP, StructureKind.CLASS, StructureKind.OBJECT -> writer.writeEndDocument() - else -> super.endStructure(descriptor) + else -> {} } } @@ -146,10 +173,10 @@ internal class DefaultBsonEncoder( // See: https://youtrack.jetbrains.com/issue/KT-66206 if (value != null || configuration.explicitNulls) { encodeName(it) - super.encodeSerializableValue(serializer, value) + super.encodeSerializableValue(serializer, value) } }, - { super.encodeSerializableValue(serializer, value) }) + { super.encodeSerializableValue(serializer, value) }) } override fun encodeNullableSerializableValue(serializer: SerializationStrategy, value: T?) { @@ -157,10 +184,10 @@ internal class DefaultBsonEncoder( { if (value != null || configuration.explicitNulls) { encodeName(it) - super.encodeNullableSerializableValue(serializer, value) + super.encodeNullableSerializableValue(serializer, value) } }, - { super.encodeNullableSerializableValue(serializer, value) }) + { super.encodeNullableSerializableValue(serializer, value) }) } override fun encodeByte(value: Byte) = encodeInt(value.toInt()) @@ -176,7 +203,7 @@ internal class DefaultBsonEncoder( override fun encodeString(value: String) { when (state) { - STATE.NAME -> encodeName(value) + STATE.NAME -> deferredElementHandler.set(value) STATE.VALUE -> writer.writeString(value) } } @@ -197,9 +224,7 @@ internal class DefaultBsonEncoder( bsonValueCodec.encode(writer, value, EncoderContext.builder().build()) } - override fun writer(): BsonWriter = writer - - private fun encodeName(value: Any) { + internal fun encodeName(value: Any) { writer.writeName(value.toString()) state = STATE.VALUE } @@ -211,7 +236,6 @@ internal class DefaultBsonEncoder( private class MapState { var currentState: STATE = STATE.VALUE - fun getState(): STATE = currentState fun nextState(): STATE { @@ -224,15 +248,15 @@ internal class DefaultBsonEncoder( } } - private class DeferredElementHandler { + internal class DeferredElementHandler { private var deferredElementName: String? = null fun set(name: String) { - assert(deferredElementName == null) { -> "Overwriting an existing deferred name" } + assert(deferredElementName == null) { "Overwriting an existing deferred name" } deferredElementName = name } - fun with(actionWithDeferredElement: (String) -> Unit, actionWithoutDeferredElement: () -> Unit): Unit { + fun with(actionWithDeferredElement: (String) -> Unit, actionWithoutDeferredElement: () -> Unit) { deferredElementName?.let { reset() actionWithDeferredElement(it) diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt new file mode 100644 index 00000000000..4b0eee8213a --- /dev/null +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonDecoder.kt @@ -0,0 +1,152 @@ +/* + * 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.kotlinx + +import java.util.Base64 +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.modules.SerializersModule +import org.bson.AbstractBsonReader +import org.bson.BsonBinarySubType +import org.bson.BsonType +import org.bson.UuidRepresentation +import org.bson.internal.UuidHelper + +@OptIn(ExperimentalSerializationApi::class) +internal interface JsonBsonDecoder : BsonDecoder, JsonDecoder { + val reader: AbstractBsonReader + val configuration: BsonConfiguration + + fun json(): Json = Json { + explicitNulls = configuration.explicitNulls + encodeDefaults = configuration.encodeDefaults + classDiscriminator = configuration.classDiscriminator + serializersModule = this@JsonBsonDecoder.serializersModule + } + + @Suppress("ComplexMethod") + override fun decodeJsonElement(): JsonElement = + reader.run { + when (currentBsonType) { + BsonType.DOCUMENT -> readJsonObject() + BsonType.ARRAY -> readJsonArray() + BsonType.NULL -> JsonPrimitive(decodeNull()) + BsonType.STRING -> JsonPrimitive(decodeString()) + BsonType.BOOLEAN -> JsonPrimitive(decodeBoolean()) + BsonType.INT32 -> JsonPrimitive(decodeInt()) + BsonType.INT64 -> JsonPrimitive(decodeLong()) + BsonType.DOUBLE -> JsonPrimitive(decodeDouble()) + BsonType.DECIMAL128 -> JsonPrimitive(reader.readDecimal128()) + BsonType.OBJECT_ID -> JsonPrimitive(decodeObjectId().toHexString()) + BsonType.DATE_TIME -> JsonPrimitive(reader.readDateTime()) + BsonType.TIMESTAMP -> JsonPrimitive(reader.readTimestamp().value) + BsonType.BINARY -> { + val subtype = reader.peekBinarySubType() + val data = reader.readBinaryData().data + when (subtype) { + BsonBinarySubType.UUID_LEGACY.value -> + JsonPrimitive( + UuidHelper.decodeBinaryToUuid(data, subtype, UuidRepresentation.JAVA_LEGACY).toString()) + BsonBinarySubType.UUID_STANDARD.value -> + JsonPrimitive( + UuidHelper.decodeBinaryToUuid(data, subtype, UuidRepresentation.STANDARD).toString()) + else -> JsonPrimitive(Base64.getEncoder().encodeToString(data)) + } + } + else -> error("Unsupported json type: $currentBsonType") + } + } + + private fun readJsonObject(): JsonObject { + reader.readStartDocument() + val obj = buildJsonObject { + var type = reader.readBsonType() + while (type != BsonType.END_OF_DOCUMENT) { + put(reader.readName(), decodeJsonElement()) + type = reader.readBsonType() + } + } + + reader.readEndDocument() + return obj + } + + private fun readJsonArray(): JsonArray { + reader.readStartArray() + val array = buildJsonArray { + var type = reader.readBsonType() + while (type != BsonType.END_OF_DOCUMENT) { + add(decodeJsonElement()) + type = reader.readBsonType() + } + } + + reader.readEndArray() + return array + } +} + +internal class JsonBsonDecoderImpl( + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : BsonDecoderImpl(reader, serializersModule, configuration), JsonBsonDecoder { + override val json = json() +} + +internal class JsonBsonArrayDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : BsonArrayDecoder(descriptor, reader, serializersModule, configuration), JsonBsonDecoder { + override val json = json() +} + +internal class JsonBsonDocumentDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : BsonDocumentDecoder(descriptor, reader, serializersModule, configuration), JsonBsonDecoder { + override val json = json() +} + +internal class JsonBsonPolymorphicDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : BsonPolymorphicDecoder(descriptor, reader, serializersModule, configuration), JsonBsonDecoder { + override val json = json() +} + +internal class JsonBsonMapDecoder( + descriptor: SerialDescriptor, + reader: AbstractBsonReader, + serializersModule: SerializersModule, + configuration: BsonConfiguration +) : BsonMapDecoder(descriptor, reader, serializersModule, configuration), JsonBsonDecoder { + override val json = json() +} diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt new file mode 100644 index 00000000000..6cff36a0909 --- /dev/null +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/JsonBsonEncoder.kt @@ -0,0 +1,132 @@ +/* + * 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.kotlinx + +import java.math.BigDecimal +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.json.JsonNull +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.double +import kotlinx.serialization.json.int +import kotlinx.serialization.json.long +import kotlinx.serialization.modules.SerializersModule +import org.bson.BsonWriter +import org.bson.types.Decimal128 + +@OptIn(ExperimentalSerializationApi::class) +internal class JsonBsonEncoder( + writer: BsonWriter, + override val serializersModule: SerializersModule, + configuration: BsonConfiguration, +) : BsonEncoderImpl(writer, serializersModule, configuration), JsonEncoder { + + companion object { + private val DOUBLE_MIN_VALUE = BigDecimal.valueOf(Double.MIN_VALUE) + private val DOUBLE_MAX_VALUE = BigDecimal.valueOf(Double.MAX_VALUE) + private val INT_MIN_VALUE = BigDecimal.valueOf(Int.MIN_VALUE.toLong()) + private val INT_MAX_VALUE = BigDecimal.valueOf(Int.MAX_VALUE.toLong()) + private val LONG_MIN_VALUE = BigDecimal.valueOf(Long.MIN_VALUE) + private val LONG_MAX_VALUE = BigDecimal.valueOf(Long.MAX_VALUE) + } + + override val json = Json { + explicitNulls = configuration.explicitNulls + encodeDefaults = configuration.encodeDefaults + classDiscriminator = configuration.classDiscriminator + serializersModule = this@JsonBsonEncoder.serializersModule + } + + override fun encodeSerializableValue(serializer: SerializationStrategy, value: T) { + if (value is JsonElement) encodeJsonElement(value) + else super.encodeSerializableValue(serializer, value) + } + + override fun encodeJsonElement(element: JsonElement) { + deferredElementHandler.with( + { + when (element) { + is JsonNull -> + if (configuration.explicitNulls) { + encodeName(it) + encodeNull() + } + is JsonPrimitive -> { + encodeName(it) + encodeJsonPrimitive(element) + } + is JsonObject -> { + encodeName(it) + encodeJsonObject(element) + } + is JsonArray -> { + encodeName(it) + encodeJsonArray(element) + } + } + }, + { + when (element) { + is JsonNull -> if (configuration.explicitNulls) encodeNull() + is JsonPrimitive -> encodeJsonPrimitive(element) + is JsonObject -> encodeJsonObject(element) + is JsonArray -> encodeJsonArray(element) + } + }) + } + + private fun encodeJsonPrimitive(primitive: JsonPrimitive) { + val content = primitive.content + when { + primitive.isString -> encodeString(content) + content == "true" || content == "false" -> encodeBoolean(content.toBooleanStrict()) + else -> { + val decimal = BigDecimal(content) + when { + decimal.scale() != 0 -> + if (DOUBLE_MIN_VALUE <= decimal && decimal <= DOUBLE_MAX_VALUE) { + encodeDouble(primitive.double) + } else { + writer.writeDecimal128(Decimal128(decimal)) + } + INT_MIN_VALUE <= decimal && decimal <= INT_MAX_VALUE -> encodeInt(primitive.int) + LONG_MIN_VALUE <= decimal && decimal <= LONG_MAX_VALUE -> encodeLong(primitive.long) + else -> writer.writeDecimal128(Decimal128(decimal)) + } + } + } + } + + private fun encodeJsonObject(obj: JsonObject) { + writer.writeStartDocument() + obj.forEach { k, v -> + deferredElementHandler.set(k) + encodeJsonElement(v) + } + writer.writeEndDocument() + } + + private fun encodeJsonArray(array: JsonArray) { + writer.writeStartArray() + array.forEach(::encodeJsonElement) + writer.writeEndArray() + } +} diff --git a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodec.kt b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodec.kt index 30d40fe6f31..41e674568a5 100644 --- a/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodec.kt +++ b/bson-kotlinx/src/main/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodec.kt @@ -172,13 +172,13 @@ private constructor( } override fun encode(writer: BsonWriter, value: T, encoderContext: EncoderContext) { - serializer.serialize(DefaultBsonEncoder(writer, serializersModule, bsonConfiguration), value) + serializer.serialize(BsonEncoder.createBsonEncoder(writer, serializersModule, bsonConfiguration), value) } override fun getEncoderClass(): Class = kClass.java override fun decode(reader: BsonReader, decoderContext: DecoderContext): T { require(reader is AbstractBsonReader) - return serializer.deserialize(DefaultBsonDecoder(reader, serializersModule, bsonConfiguration)) + return serializer.deserialize(BsonDecoder.createBsonDecoder(reader, serializersModule, bsonConfiguration)) } } diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt index e9d3742db10..aa749368e04 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/KotlinSerializerCodecTest.kt @@ -15,6 +15,8 @@ */ package org.bson.codecs.kotlinx +import java.math.BigDecimal +import java.util.Base64 import java.util.stream.Stream import kotlin.test.assertEquals import kotlinx.datetime.Instant @@ -24,6 +26,10 @@ import kotlinx.datetime.LocalTime import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.JsonPrimitive +import kotlinx.serialization.json.buildJsonArray +import kotlinx.serialization.json.buildJsonObject +import kotlinx.serialization.json.put import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.plus import kotlinx.serialization.modules.polymorphic @@ -85,6 +91,9 @@ import org.bson.codecs.kotlinx.samples.DataClassWithEncodeDefault import org.bson.codecs.kotlinx.samples.DataClassWithEnum import org.bson.codecs.kotlinx.samples.DataClassWithEnumMapKey import org.bson.codecs.kotlinx.samples.DataClassWithFailingInit +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElement +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElements +import org.bson.codecs.kotlinx.samples.DataClassWithJsonElementsNullable import org.bson.codecs.kotlinx.samples.DataClassWithListThatLastItemDefaultsToNull import org.bson.codecs.kotlinx.samples.DataClassWithMutableList import org.bson.codecs.kotlinx.samples.DataClassWithMutableMap @@ -102,6 +111,8 @@ import org.bson.codecs.kotlinx.samples.DataClassWithTriple import org.bson.codecs.kotlinx.samples.Key import org.bson.codecs.kotlinx.samples.SealedInterface import org.bson.codecs.kotlinx.samples.ValueClass +import org.bson.json.JsonMode +import org.bson.json.JsonWriterSettings import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import org.junit.jupiter.params.ParameterizedTest @@ -111,6 +122,8 @@ import org.junit.jupiter.params.provider.MethodSource @Suppress("LargeClass") class KotlinSerializerCodecTest { private val oid = "\$oid" + private val numberLong = "\$numberLong" + private val numberDecimal = "\$numberDecimal" private val emptyDocument = "{}" private val altConfiguration = BsonConfiguration(encodeDefaults = false, classDiscriminator = "_t", explicitNulls = true) @@ -128,7 +141,7 @@ class KotlinSerializerCodecTest { | "binary": {"${'$'}binary": {"base64": "S2Fma2Egcm9ja3Mh", "subType": "00"}}, | "boolean": true, | "code": {"${'$'}code": "int i = 0;"}, - | "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": {"${'$'}numberInt": "1"}}}, + | "codeWithScope": {"${'$'}code": "int x = y", "${'$'}scope": {"y": 1}}, | "dateTime": {"${'$'}date": {"${'$'}numberLong": "1577836801000"}}, | "decimal128": {"${'$'}numberDecimal": "1.0"}, | "documentEmpty": {}, @@ -148,6 +161,14 @@ class KotlinSerializerCodecTest { .trimMargin() private val allBsonTypesDocument = BsonDocument.parse(allBsonTypesJson) + private val jsonAllSupportedTypesDocument: BsonDocument by + lazy { + val doc = BsonDocument.parse(allBsonTypesJson) + listOf("minKey", "maxKey", "code", "codeWithScope", "regex", "symbol", "undefined").forEach { + doc.remove(it) + } + doc + } companion object { @JvmStatic @@ -799,6 +820,233 @@ class KotlinSerializerCodecTest { assertRoundTrips(expected, dataClass) } + @Test + fun testDataClassWithJsonElement() { + val expected = + """{"value": { + |"char": "c", + |"byte": 0, + |"short": 1, + |"int": 22, + |"long": {"$numberLong": "3000000000"}, + |"decimal": {"$numberDecimal": "10000000000000000000"} + |"decimal2": {"$numberDecimal": "3.1230E+700"} + |"float": 4.0, + |"double": 4.2, + |"boolean": true, + |"string": "String" + |}}""" + .trimMargin() + + val dataClass = + DataClassWithJsonElement( + buildJsonObject { + put("char", "c") + put("byte", 0) + put("short", 1) + put("int", 22) + put("long", 3_000_000_000) + put("decimal", BigDecimal("10000000000000000000")) + put("decimal2", BigDecimal("3.1230E+700")) + put("float", 4.0) + put("double", 4.2) + put("boolean", true) + put("string", "String") + }) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithJsonElements() { + val expected = + """{ + | "jsonElement": {"string": "String"}, + | "jsonArray": [1, 2], + | "jsonElements": [{"string": "String"}, {"int": 42}], + | "jsonNestedMap": {"nestedString": {"string": "String"}, + | "nestedLong": {"long": {"$numberLong": "3000000000"}}} + |}""" + .trimMargin() + + val dataClass = + DataClassWithJsonElements( + buildJsonObject { put("string", "String") }, + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + }, + listOf(buildJsonObject { put("string", "String") }, buildJsonObject { put("int", 42) }), + mapOf( + Pair("nestedString", buildJsonObject { put("string", "String") }), + Pair("nestedLong", buildJsonObject { put("long", 3000000000L) }))) + + assertRoundTrips(expected, dataClass) + } + + @Test + fun testDataClassWithJsonElementsNullable() { + val expected = + """{ + | "jsonElement": {"null": null}, + | "jsonArray": [1, 2, null], + | "jsonElements": [{"null": null}], + | "jsonNestedMap": {"nestedNull": null} + |}""" + .trimMargin() + + val dataClass = + DataClassWithJsonElementsNullable( + buildJsonObject { put("null", null) }, + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(null)) + }, + listOf(buildJsonObject { put("null", null) }), + mapOf(Pair("nestedNull", null))) + + assertRoundTrips(expected, dataClass, altConfiguration) + + val expectedNoNulls = + """{ + | "jsonElement": {}, + | "jsonArray": [1, 2], + | "jsonElements": [{}], + | "jsonNestedMap": {} + |}""" + .trimMargin() + + val dataClassNoNulls = + DataClassWithJsonElementsNullable( + buildJsonObject {}, + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + }, + listOf(buildJsonObject {}), + mapOf()) + assertEncodesTo(expectedNoNulls, dataClass) + assertDecodesTo(expectedNoNulls, dataClassNoNulls) + } + + @Test + fun testDataClassWithJsonElementNullSupport() { + val expected = + """{"jsonElement": {"null": null}, + | "jsonArray": [1, 2, null], + | "jsonElements": [{"null": null}], + | "jsonNestedMap": {"nestedNull": null} + | } + | """ + .trimMargin() + + val dataClass = + DataClassWithJsonElements( + buildJsonObject { put("null", null) }, + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(null)) + }, + listOf(buildJsonObject { put("null", null) }), + mapOf(Pair("nestedNull", JsonPrimitive(null)))) + + assertRoundTrips(expected, dataClass, altConfiguration) + + val expectedNoNulls = + """{"jsonElement": {}, + | "jsonArray": [1, 2], + | "jsonElements": [{}], + | "jsonNestedMap": {} + | } + | """ + .trimMargin() + + val dataClassNoNulls = + DataClassWithJsonElements( + buildJsonObject {}, + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + }, + listOf(buildJsonObject {}), + mapOf()) + assertEncodesTo(expectedNoNulls, dataClass) + assertDecodesTo(expectedNoNulls, dataClassNoNulls) + } + + @Test + @Suppress("LongMethod") + fun testDataClassWithJsonElementBsonSupport() { + val dataClassWithAllSupportedJsonTypes = + DataClassWithJsonElement( + buildJsonObject { + put("id", "111111111111111111111111") + put("arrayEmpty", buildJsonArray {}) + put( + "arraySimple", + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(3)) + }) + put( + "arrayComplex", + buildJsonArray { + add(buildJsonObject { put("a", JsonPrimitive(1)) }) + add(buildJsonObject { put("a", JsonPrimitive(2)) }) + }) + put( + "arrayMixedTypes", + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(true)) + add( + buildJsonArray { + add(JsonPrimitive(1)) + add(JsonPrimitive(2)) + add(JsonPrimitive(3)) + }) + add(buildJsonObject { put("a", JsonPrimitive(2)) }) + }) + put( + "arrayComplexMixedTypes", + buildJsonArray { + add(buildJsonObject { put("a", JsonPrimitive(1)) }) + add(buildJsonObject { put("a", JsonPrimitive("a")) }) + }) + put("binary", JsonPrimitive("S2Fma2Egcm9ja3Mh")) + put("boolean", JsonPrimitive(true)) + put("dateTime", JsonPrimitive(1577836801000)) + put("decimal128", JsonPrimitive(1.0)) + put("documentEmpty", buildJsonObject {}) + put("document", buildJsonObject { put("a", JsonPrimitive(1)) }) + put("double", JsonPrimitive(62.0)) + put("int32", JsonPrimitive(42)) + put("int64", JsonPrimitive(52)) + put("objectId", JsonPrimitive("211111111111111111111112")) + put("string", JsonPrimitive("the fox ...")) + put("timestamp", JsonPrimitive(1311768464867721221)) + }) + + val jsonWriterSettings = + JsonWriterSettings.builder() + .outputMode(JsonMode.RELAXED) + .objectIdConverter { oid, writer -> writer.writeString(oid.toHexString()) } + .dateTimeConverter { d, writer -> writer.writeNumber(d.toString()) } + .timestampConverter { ts, writer -> writer.writeNumber(ts.value.toString()) } + .binaryConverter { b, writer -> writer.writeString(Base64.getEncoder().encodeToString(b.data)) } + .decimal128Converter { d, writer -> writer.writeNumber(d.toDouble().toString()) } + .build() + val dataClassWithAllSupportedJsonTypesSimpleJson = jsonAllSupportedTypesDocument.toJson(jsonWriterSettings) + + assertEncodesTo( + """{"value": $dataClassWithAllSupportedJsonTypesSimpleJson }""", dataClassWithAllSupportedJsonTypes) + assertDecodesTo("""{"value": $jsonAllSupportedTypesDocument}""", dataClassWithAllSupportedJsonTypes) + } + @Test fun testDataFailures() { assertThrows("Missing data") { @@ -896,6 +1144,7 @@ class KotlinSerializerCodecTest { ): BsonDocument { val expected = BsonDocument.parse(json) val actual = serialize(value, serializersModule, configuration) + println(actual.toJson()) assertEquals(expected, actual) return actual } @@ -913,6 +1162,15 @@ class KotlinSerializerCodecTest { return document } + private inline fun assertDecodesTo( + value: String, + expected: T, + serializersModule: SerializersModule = defaultSerializersModule, + configuration: BsonConfiguration = BsonConfiguration() + ) { + assertDecodesTo(BsonDocument.parse(value), expected, serializersModule, configuration) + } + private inline fun assertDecodesTo( value: BsonDocument, expected: T, diff --git a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt index cbdf41ab2f3..e7a06600d20 100644 --- a/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt +++ b/bson-kotlinx/src/test/kotlin/org/bson/codecs/kotlinx/samples/DataClasses.kt @@ -25,6 +25,8 @@ import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Required import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonArray +import kotlinx.serialization.json.JsonElement import org.bson.BsonArray import org.bson.BsonBinary import org.bson.BsonBoolean @@ -324,3 +326,21 @@ data class DataClassWithFailingInit(val id: String) { @Serializable data class Box(val boxed: T) @Serializable data class DataClassWithNullableGeneric(val box: Box) + +@Serializable data class DataClassWithJsonElement(val value: JsonElement) + +@Serializable +data class DataClassWithJsonElements( + val jsonElement: JsonElement, + val jsonArray: JsonArray, + val jsonElements: List, + val jsonNestedMap: Map +) + +@Serializable +data class DataClassWithJsonElementsNullable( + val jsonElement: JsonElement?, + val jsonArray: JsonArray?, + val jsonElements: List?, + val jsonNestedMap: Map? +) diff --git a/gradle/publish.gradle b/gradle/publish.gradle index 25edda53f49..07f43f762bd 100644 --- a/gradle/publish.gradle +++ b/gradle/publish.gradle @@ -102,6 +102,9 @@ configure(javaProjects) { project -> suppressPomMetadataWarningsFor("dateTimeSupportApiElements") suppressPomMetadataWarningsFor("dateTimeRuntimeElements") + + suppressPomMetadataWarningsFor("jsonSupportApiElements") + suppressPomMetadataWarningsFor("jsonSupportRuntimeElements") } }