From e4b2b6f316af2234f83103f5e924d8296ab41596 Mon Sep 17 00:00:00 2001 From: Viacheslav Babanin Date: Thu, 8 Aug 2024 03:08:07 -0700 Subject: [PATCH] Enhance KotlinSerializer with value codecs for widening primitive conversion. (#1301) (#1471) JAVA-5303 --- .../org/bson/codecs/kotlinx/BsonDecoder.kt | 21 +++--- .../kotlinx/KotlinSerializerCodecTest.kt | 69 ++++++++++++++++--- .../org/bson/codecs/AtomicIntegerCodec.java | 2 +- .../main/org/bson/codecs/AtomicLongCodec.java | 2 +- bson/src/main/org/bson/codecs/ByteCodec.java | 10 +-- .../main/org/bson/codecs/CharacterCodec.java | 11 +-- .../src/main/org/bson/codecs/DoubleCodec.java | 2 +- bson/src/main/org/bson/codecs/FloatCodec.java | 10 +-- .../main/org/bson/codecs/IntegerCodec.java | 2 +- bson/src/main/org/bson/codecs/LongCodec.java | 2 +- bson/src/main/org/bson/codecs/ShortCodec.java | 10 +-- .../NumberCodecHelper.java | 37 ++++++++-- .../org/bson/internal/StringCodecHelper.java | 46 +++++++++++++ config/spotbugs/exclude.xml | 6 ++ 14 files changed, 170 insertions(+), 60 deletions(-) rename bson/src/main/org/bson/{codecs => internal}/NumberCodecHelper.java (78%) create mode 100644 bson/src/main/org/bson/internal/StringCodecHelper.java 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 435964d4ac0..38d9c23309f 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 @@ -36,6 +36,8 @@ import org.bson.BsonType import org.bson.BsonValue import org.bson.codecs.BsonValueCodec import org.bson.codecs.DecoderContext +import org.bson.internal.NumberCodecHelper +import org.bson.internal.StringCodecHelper import org.bson.types.ObjectId /** @@ -154,14 +156,17 @@ internal open class DefaultBsonDecoder( } } - override fun decodeByte(): Byte = decodeInt().toByte() - override fun decodeChar(): Char = decodeString().single() - override fun decodeFloat(): Float = decodeDouble().toFloat() - override fun decodeShort(): Short = decodeInt().toShort() - override fun decodeBoolean(): Boolean = readOrThrow({ reader.readBoolean() }, BsonType.BOOLEAN) - override fun decodeDouble(): Double = readOrThrow({ reader.readDouble() }, BsonType.DOUBLE) - override fun decodeInt(): Int = readOrThrow({ reader.readInt32() }, BsonType.INT32) - override fun decodeLong(): Long = readOrThrow({ reader.readInt64() }, BsonType.INT64) + 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) override fun decodeString(): String = readOrThrow({ reader.readString() }, BsonType.STRING) override fun decodeNull(): Nothing? { 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 0aed60b27ba..30fc6f7fbb4 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,7 @@ */ package org.bson.codecs.kotlinx +import java.util.stream.Stream import kotlin.test.assertEquals import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.MissingFieldException @@ -23,12 +24,17 @@ import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.plus import kotlinx.serialization.modules.polymorphic import kotlinx.serialization.modules.subclass +import org.bson.BsonBoolean import org.bson.BsonDocument import org.bson.BsonDocumentReader import org.bson.BsonDocumentWriter +import org.bson.BsonDouble +import org.bson.BsonInt32 +import org.bson.BsonInt64 import org.bson.BsonInvalidOperationException import org.bson.BsonMaxKey import org.bson.BsonMinKey +import org.bson.BsonString import org.bson.BsonUndefined import org.bson.codecs.DecoderContext import org.bson.codecs.EncoderContext @@ -90,11 +96,12 @@ import org.bson.codecs.kotlinx.samples.SealedInterface import org.bson.codecs.kotlinx.samples.ValueClass import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.MethodSource @OptIn(ExperimentalSerializationApi::class) @Suppress("LargeClass") class KotlinSerializerCodecTest { - private val numberLong = "\$numberLong" private val oid = "\$oid" private val emptyDocument = "{}" private val altConfiguration = @@ -134,15 +141,59 @@ class KotlinSerializerCodecTest { private val allBsonTypesDocument = BsonDocument.parse(allBsonTypesJson) - @Test - fun testDataClassWithSimpleValues() { - val expected = - """{"char": "c", "byte": 0, "short": 1, "int": 22, "long": {"$numberLong": "42"}, "float": 4.0, - | "double": 4.2, "boolean": true, "string": "String"}""" - .trimMargin() - val dataClass = DataClassWithSimpleValues('c', 0, 1, 22, 42L, 4.0f, 4.2, true, "String") + companion object { + @JvmStatic + fun testTypesCastingDataClassWithSimpleValues(): Stream { + return Stream.of( + BsonDocument() + .append("char", BsonString("c")) + .append("byte", BsonInt32(1)) + .append("short", BsonInt32(2)) + .append("int", BsonInt32(10)) + .append("long", BsonInt32(10)) + .append("float", BsonInt32(2)) + .append("double", BsonInt32(3)) + .append("boolean", BsonBoolean.TRUE) + .append("string", BsonString("String")), + BsonDocument() + .append("char", BsonString("c")) + .append("byte", BsonDouble(1.0)) + .append("short", BsonDouble(2.0)) + .append("int", BsonDouble(9.9999999999999992)) + .append("long", BsonDouble(9.9999999999999992)) + .append("float", BsonDouble(2.0)) + .append("double", BsonDouble(3.0)) + .append("boolean", BsonBoolean.TRUE) + .append("string", BsonString("String")), + BsonDocument() + .append("char", BsonString("c")) + .append("byte", BsonDouble(1.0)) + .append("short", BsonDouble(2.0)) + .append("int", BsonDouble(10.0)) + .append("long", BsonDouble(10.0)) + .append("float", BsonDouble(2.0)) + .append("double", BsonDouble(3.0)) + .append("boolean", BsonBoolean.TRUE) + .append("string", BsonString("String")), + BsonDocument() + .append("char", BsonString("c")) + .append("byte", BsonInt64(1)) + .append("short", BsonInt64(2)) + .append("int", BsonInt64(10)) + .append("long", BsonInt64(10)) + .append("float", BsonInt64(2)) + .append("double", BsonInt64(3)) + .append("boolean", BsonBoolean.TRUE) + .append("string", BsonString("String"))) + } + } - assertRoundTrips(expected, dataClass) + @ParameterizedTest + @MethodSource("testTypesCastingDataClassWithSimpleValues") + fun testTypesCastingDataClassWithSimpleValues(data: BsonDocument) { + val expectedDataClass = DataClassWithSimpleValues('c', 1, 2, 10, 10L, 2.0f, 3.0, true, "String") + + assertDecodesTo(data, expectedDataClass) } @Test diff --git a/bson/src/main/org/bson/codecs/AtomicIntegerCodec.java b/bson/src/main/org/bson/codecs/AtomicIntegerCodec.java index 8fd3e55876b..d8963ed40d7 100644 --- a/bson/src/main/org/bson/codecs/AtomicIntegerCodec.java +++ b/bson/src/main/org/bson/codecs/AtomicIntegerCodec.java @@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicInteger; -import static org.bson.codecs.NumberCodecHelper.decodeInt; +import static org.bson.internal.NumberCodecHelper.decodeInt; /** * Encodes and decodes {@code AtomicInteger} objects. diff --git a/bson/src/main/org/bson/codecs/AtomicLongCodec.java b/bson/src/main/org/bson/codecs/AtomicLongCodec.java index c6e053c6d9f..7f08af77961 100644 --- a/bson/src/main/org/bson/codecs/AtomicLongCodec.java +++ b/bson/src/main/org/bson/codecs/AtomicLongCodec.java @@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicLong; -import static org.bson.codecs.NumberCodecHelper.decodeLong; +import static org.bson.internal.NumberCodecHelper.decodeLong; /** * Encodes and decodes {@code AtomicLong} objects. diff --git a/bson/src/main/org/bson/codecs/ByteCodec.java b/bson/src/main/org/bson/codecs/ByteCodec.java index 26b5005ea66..e7011f8b58d 100644 --- a/bson/src/main/org/bson/codecs/ByteCodec.java +++ b/bson/src/main/org/bson/codecs/ByteCodec.java @@ -16,12 +16,10 @@ package org.bson.codecs; -import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonWriter; -import static java.lang.String.format; -import static org.bson.codecs.NumberCodecHelper.decodeInt; +import static org.bson.internal.NumberCodecHelper.decodeByte; /** * Encodes and decodes {@code Byte} objects. @@ -37,11 +35,7 @@ public void encode(final BsonWriter writer, final Byte value, final EncoderConte @Override public Byte decode(final BsonReader reader, final DecoderContext decoderContext) { - int value = decodeInt(reader); - if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { - throw new BsonInvalidOperationException(format("%s can not be converted into a Byte.", value)); - } - return (byte) value; + return decodeByte(reader); } @Override diff --git a/bson/src/main/org/bson/codecs/CharacterCodec.java b/bson/src/main/org/bson/codecs/CharacterCodec.java index 0a9e6252056..4ad6efa2663 100644 --- a/bson/src/main/org/bson/codecs/CharacterCodec.java +++ b/bson/src/main/org/bson/codecs/CharacterCodec.java @@ -16,11 +16,10 @@ package org.bson.codecs; -import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonWriter; +import org.bson.internal.StringCodecHelper; -import static java.lang.String.format; import static org.bson.assertions.Assertions.notNull; /** @@ -38,13 +37,7 @@ public void encode(final BsonWriter writer, final Character value, final Encoder @Override public Character decode(final BsonReader reader, final DecoderContext decoderContext) { - String string = reader.readString(); - if (string.length() != 1) { - throw new BsonInvalidOperationException(format("Attempting to decode the string '%s' to a character, but its length is not " - + "equal to one", string)); - } - - return string.charAt(0); + return StringCodecHelper.decodeChar(reader); } @Override diff --git a/bson/src/main/org/bson/codecs/DoubleCodec.java b/bson/src/main/org/bson/codecs/DoubleCodec.java index 523042bb163..33e3f6782bd 100644 --- a/bson/src/main/org/bson/codecs/DoubleCodec.java +++ b/bson/src/main/org/bson/codecs/DoubleCodec.java @@ -19,7 +19,7 @@ import org.bson.BsonReader; import org.bson.BsonWriter; -import static org.bson.codecs.NumberCodecHelper.decodeDouble; +import static org.bson.internal.NumberCodecHelper.decodeDouble; /** * Encodes and decodes {@code Double} objects. diff --git a/bson/src/main/org/bson/codecs/FloatCodec.java b/bson/src/main/org/bson/codecs/FloatCodec.java index 84b85c5aa1b..49dc7e22aff 100644 --- a/bson/src/main/org/bson/codecs/FloatCodec.java +++ b/bson/src/main/org/bson/codecs/FloatCodec.java @@ -16,12 +16,10 @@ package org.bson.codecs; -import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonWriter; -import static java.lang.String.format; -import static org.bson.codecs.NumberCodecHelper.decodeDouble; +import static org.bson.internal.NumberCodecHelper.decodeFloat; /** * Encodes and decodes {@code Float} objects. @@ -37,11 +35,7 @@ public void encode(final BsonWriter writer, final Float value, final EncoderCont @Override public Float decode(final BsonReader reader, final DecoderContext decoderContext) { - double value = decodeDouble(reader); - if (value < -Float.MAX_VALUE || value > Float.MAX_VALUE) { - throw new BsonInvalidOperationException(format("%s can not be converted into a Float.", value)); - } - return (float) value; + return decodeFloat(reader); } @Override diff --git a/bson/src/main/org/bson/codecs/IntegerCodec.java b/bson/src/main/org/bson/codecs/IntegerCodec.java index dee6e2512fb..bb0c5c082d5 100644 --- a/bson/src/main/org/bson/codecs/IntegerCodec.java +++ b/bson/src/main/org/bson/codecs/IntegerCodec.java @@ -19,7 +19,7 @@ import org.bson.BsonReader; import org.bson.BsonWriter; -import static org.bson.codecs.NumberCodecHelper.decodeInt; +import static org.bson.internal.NumberCodecHelper.decodeInt; /** * Encodes and decodes {@code Integer} objects. diff --git a/bson/src/main/org/bson/codecs/LongCodec.java b/bson/src/main/org/bson/codecs/LongCodec.java index 29adc373488..0e16e4430bc 100644 --- a/bson/src/main/org/bson/codecs/LongCodec.java +++ b/bson/src/main/org/bson/codecs/LongCodec.java @@ -19,7 +19,7 @@ import org.bson.BsonReader; import org.bson.BsonWriter; -import static org.bson.codecs.NumberCodecHelper.decodeLong; +import static org.bson.internal.NumberCodecHelper.decodeLong; /** * Encodes and decodes {@code Long} objects. diff --git a/bson/src/main/org/bson/codecs/ShortCodec.java b/bson/src/main/org/bson/codecs/ShortCodec.java index e5aaf8f9acb..8c439e36b8d 100644 --- a/bson/src/main/org/bson/codecs/ShortCodec.java +++ b/bson/src/main/org/bson/codecs/ShortCodec.java @@ -16,12 +16,10 @@ package org.bson.codecs; -import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; import org.bson.BsonWriter; -import static java.lang.String.format; -import static org.bson.codecs.NumberCodecHelper.decodeInt; +import static org.bson.internal.NumberCodecHelper.decodeShort; /** * Encodes and decodes {@code Short} objects. @@ -37,11 +35,7 @@ public void encode(final BsonWriter writer, final Short value, final EncoderCont @Override public Short decode(final BsonReader reader, final DecoderContext decoderContext) { - int value = decodeInt(reader); - if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { - throw new BsonInvalidOperationException(format("%s can not be converted into a Short.", value)); - } - return (short) value; + return decodeShort(reader); } @Override diff --git a/bson/src/main/org/bson/codecs/NumberCodecHelper.java b/bson/src/main/org/bson/internal/NumberCodecHelper.java similarity index 78% rename from bson/src/main/org/bson/codecs/NumberCodecHelper.java rename to bson/src/main/org/bson/internal/NumberCodecHelper.java index 69dfe29ac7e..faf63e56eb5 100644 --- a/bson/src/main/org/bson/codecs/NumberCodecHelper.java +++ b/bson/src/main/org/bson/internal/NumberCodecHelper.java @@ -14,7 +14,7 @@ * limitations under the License. */ -package org.bson.codecs; +package org.bson.internal; import org.bson.BsonInvalidOperationException; import org.bson.BsonReader; @@ -25,9 +25,28 @@ import static java.lang.String.format; -final class NumberCodecHelper { +/** + * This class is not part of the public API. It may be removed or changed at any time. + */ +public final class NumberCodecHelper { + + public static byte decodeByte(final BsonReader reader) { + int value = decodeInt(reader); + if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) { + throw new BsonInvalidOperationException(format("%s can not be converted into a Byte.", value)); + } + return (byte) value; + } + + public static short decodeShort(final BsonReader reader) { + int value = decodeInt(reader); + if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { + throw new BsonInvalidOperationException(format("%s can not be converted into a Short.", value)); + } + return (short) value; + } - static int decodeInt(final BsonReader reader) { + public static int decodeInt(final BsonReader reader) { int intValue; BsonType bsonType = reader.getCurrentBsonType(); switch (bsonType) { @@ -61,7 +80,7 @@ static int decodeInt(final BsonReader reader) { return intValue; } - static long decodeLong(final BsonReader reader) { + public static long decodeLong(final BsonReader reader) { long longValue; BsonType bsonType = reader.getCurrentBsonType(); switch (bsonType) { @@ -91,7 +110,15 @@ static long decodeLong(final BsonReader reader) { return longValue; } - static double decodeDouble(final BsonReader reader) { + public static float decodeFloat(final BsonReader reader) { + double value = decodeDouble(reader); + if (value < -Float.MAX_VALUE || value > Float.MAX_VALUE) { + throw new BsonInvalidOperationException(format("%s can not be converted into a Float.", value)); + } + return (float) value; + } + + public static double decodeDouble(final BsonReader reader) { double doubleValue; BsonType bsonType = reader.getCurrentBsonType(); switch (bsonType) { diff --git a/bson/src/main/org/bson/internal/StringCodecHelper.java b/bson/src/main/org/bson/internal/StringCodecHelper.java new file mode 100644 index 00000000000..04225aad939 --- /dev/null +++ b/bson/src/main/org/bson/internal/StringCodecHelper.java @@ -0,0 +1,46 @@ +/* + * 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.internal; + +import org.bson.BsonInvalidOperationException; +import org.bson.BsonReader; +import org.bson.BsonType; + +import static java.lang.String.format; + +/** + * This class is not part of the public API. It may be removed or changed at any time. + */ +public final class StringCodecHelper { + + private StringCodecHelper(){ + //NOP + } + + public static char decodeChar(final BsonReader reader) { + BsonType currentBsonType = reader.getCurrentBsonType(); + if (currentBsonType != BsonType.STRING) { + throw new BsonInvalidOperationException(format("Invalid string type, found: %s", currentBsonType)); + } + String string = reader.readString(); + if (string.length() != 1) { + throw new BsonInvalidOperationException(format("Attempting to decode the string '%s' to a character, but its length is not " + + "equal to one", string)); + } + return string.charAt(0); + } +} diff --git a/config/spotbugs/exclude.xml b/config/spotbugs/exclude.xml index fb0e4e9ec0e..cfb26fc8296 100644 --- a/config/spotbugs/exclude.xml +++ b/config/spotbugs/exclude.xml @@ -198,6 +198,12 @@ + + + + + +