Skip to content

Commit 570c164

Browse files
rozzavbabanin
andcommitted
Added Bson-Kotlin Array Codec (mongodb#1457)
Adds Kotlin array support to the bson-kotlin library JAVA-5122 Co-authored-by: Viacheslav Babanin <[email protected]>
1 parent c17bb47 commit 570c164

File tree

6 files changed

+310
-4
lines changed

6 files changed

+310
-4
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.bson.codecs.kotlin
17+
18+
import java.lang.reflect.ParameterizedType
19+
import java.lang.reflect.Type
20+
import kotlin.reflect.KClass
21+
import org.bson.BsonReader
22+
import org.bson.BsonType
23+
import org.bson.BsonWriter
24+
import org.bson.codecs.Codec
25+
import org.bson.codecs.DecoderContext
26+
import org.bson.codecs.EncoderContext
27+
import org.bson.codecs.configuration.CodecRegistry
28+
29+
@Suppress("UNCHECKED_CAST")
30+
internal data class ArrayCodec<R : Any, V>(private val kClass: KClass<R>, private val codec: Codec<V?>) : Codec<R> {
31+
32+
companion object {
33+
internal fun <R : Any> create(
34+
kClass: KClass<R>,
35+
typeArguments: List<Type>,
36+
codecRegistry: CodecRegistry
37+
): Codec<R> {
38+
assert(kClass.javaObjectType.isArray) { "$kClass must be an array type" }
39+
val (valueClass, nestedTypes) =
40+
if (typeArguments.isEmpty()) {
41+
Pair(kClass.java.componentType.kotlin.javaObjectType as Class<Any>, emptyList())
42+
} else {
43+
// Unroll the actual class and any type arguments
44+
when (val pType = typeArguments[0]) {
45+
is Class<*> -> Pair(pType as Class<Any>, emptyList())
46+
is ParameterizedType -> Pair(pType.rawType as Class<Any>, pType.actualTypeArguments.toList())
47+
else -> Pair(Object::class.java as Class<Any>, emptyList())
48+
}
49+
}
50+
val codec =
51+
if (nestedTypes.isEmpty()) codecRegistry.get(valueClass) else codecRegistry.get(valueClass, nestedTypes)
52+
return ArrayCodec(kClass, codec)
53+
}
54+
}
55+
56+
private val isPrimitiveArray = kClass.java.componentType != kClass.java.componentType.kotlin.javaObjectType
57+
58+
override fun encode(writer: BsonWriter, arrayValue: R, encoderContext: EncoderContext) {
59+
writer.writeStartArray()
60+
61+
boxed(arrayValue).forEach {
62+
if (it == null) writer.writeNull() else encoderContext.encodeWithChildContext(codec, writer, it)
63+
}
64+
65+
writer.writeEndArray()
66+
}
67+
68+
override fun getEncoderClass(): Class<R> = kClass.java
69+
70+
override fun decode(reader: BsonReader, decoderContext: DecoderContext): R {
71+
reader.readStartArray()
72+
val data = ArrayList<V?>()
73+
while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
74+
if (reader.currentBsonType == BsonType.NULL) {
75+
reader.readNull()
76+
data.add(null)
77+
} else {
78+
data.add(decoderContext.decodeWithChildContext(codec, reader))
79+
}
80+
}
81+
reader.readEndArray()
82+
return unboxed(data)
83+
}
84+
85+
fun boxed(arrayValue: R): Iterable<V?> {
86+
val boxedValue =
87+
if (!isPrimitiveArray) {
88+
(arrayValue as Array<V?>).asIterable()
89+
} else if (arrayValue is BooleanArray) {
90+
arrayValue.asIterable()
91+
} else if (arrayValue is ByteArray) {
92+
arrayValue.asIterable()
93+
} else if (arrayValue is CharArray) {
94+
arrayValue.asIterable()
95+
} else if (arrayValue is DoubleArray) {
96+
arrayValue.asIterable()
97+
} else if (arrayValue is FloatArray) {
98+
arrayValue.asIterable()
99+
} else if (arrayValue is IntArray) {
100+
arrayValue.asIterable()
101+
} else if (arrayValue is LongArray) {
102+
arrayValue.asIterable()
103+
} else if (arrayValue is ShortArray) {
104+
arrayValue.asIterable()
105+
} else {
106+
throw IllegalArgumentException("Unsupported array type ${arrayValue.javaClass}")
107+
}
108+
return boxedValue as Iterable<V?>
109+
}
110+
111+
private fun unboxed(data: ArrayList<V?>): R {
112+
return when (kClass) {
113+
BooleanArray::class -> (data as ArrayList<Boolean>).toBooleanArray() as R
114+
ByteArray::class -> (data as ArrayList<Byte>).toByteArray() as R
115+
CharArray::class -> (data as ArrayList<Char>).toCharArray() as R
116+
DoubleArray::class -> (data as ArrayList<Double>).toDoubleArray() as R
117+
FloatArray::class -> (data as ArrayList<Float>).toFloatArray() as R
118+
IntArray::class -> (data as ArrayList<Int>).toIntArray() as R
119+
LongArray::class -> (data as ArrayList<Long>).toLongArray() as R
120+
ShortArray::class -> (data as ArrayList<Short>).toShortArray() as R
121+
else -> data.toArray(arrayOfNulls(data.size)) as R
122+
}
123+
}
124+
125+
private fun arrayOfNulls(size: Int): Array<V?> {
126+
return java.lang.reflect.Array.newInstance(codec.encoderClass, size) as Array<V?>
127+
}
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/*
2+
* Copyright 2008-present MongoDB, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.bson.codecs.kotlin
17+
18+
import java.lang.reflect.Type
19+
import org.bson.codecs.Codec
20+
import org.bson.codecs.configuration.CodecProvider
21+
import org.bson.codecs.configuration.CodecRegistry
22+
23+
/** A Kotlin reflection based Codec Provider for data classes */
24+
public class ArrayCodecProvider : CodecProvider {
25+
override fun <T : Any> get(clazz: Class<T>, registry: CodecRegistry): Codec<T>? = get(clazz, emptyList(), registry)
26+
27+
override fun <T : Any> get(clazz: Class<T>, typeArguments: List<Type>, registry: CodecRegistry): Codec<T>? =
28+
if (clazz.isArray) {
29+
ArrayCodec.create(clazz.kotlin, typeArguments, registry)
30+
} else null
31+
}

bson-kotlin/src/main/kotlin/org/bson/codecs/kotlin/DataClassCodec.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ internal data class DataClassCodec<T : Any>(
206206
is KTypeParameter -> {
207207
when (val pType = typeMap[kParameter.type] ?: kParameter.type.javaType) {
208208
is Class<*> ->
209-
codecRegistry.getCodec(kParameter, (pType as Class<Any>).kotlin.javaObjectType, emptyList())
209+
codecRegistry.getCodec(kParameter, (pType as Class<Any>).kotlin.java, emptyList())
210210
is ParameterizedType ->
211211
codecRegistry.getCodec(
212212
kParameter,
@@ -231,11 +231,14 @@ internal data class DataClassCodec<T : Any>(
231231
@Suppress("UNCHECKED_CAST")
232232
private fun CodecRegistry.getCodec(kParameter: KParameter, clazz: Class<Any>, types: List<Type>): Codec<Any> {
233233
val codec =
234-
if (types.isEmpty()) {
234+
if (clazz.isArray) {
235+
ArrayCodec.create(clazz.kotlin, types, this)
236+
} else if (types.isEmpty()) {
235237
this.get(clazz)
236238
} else {
237239
this.get(clazz, types)
238240
}
241+
239242
return kParameter.findAnnotation<BsonRepresentation>()?.let {
240243
if (codec !is RepresentationConfigurable<*>) {
241244
throw CodecConfigurationException(

bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/DataClassCodecTest.kt

+56-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import org.bson.codecs.kotlin.samples.DataClassSealedA
3636
import org.bson.codecs.kotlin.samples.DataClassSealedB
3737
import org.bson.codecs.kotlin.samples.DataClassSealedC
3838
import org.bson.codecs.kotlin.samples.DataClassSelfReferential
39+
import org.bson.codecs.kotlin.samples.DataClassWithArrays
3940
import org.bson.codecs.kotlin.samples.DataClassWithBooleanMapKey
4041
import org.bson.codecs.kotlin.samples.DataClassWithBsonConstructor
4142
import org.bson.codecs.kotlin.samples.DataClassWithBsonDiscriminator
@@ -54,6 +55,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithInvalidBsonRepresentation
5455
import org.bson.codecs.kotlin.samples.DataClassWithMutableList
5556
import org.bson.codecs.kotlin.samples.DataClassWithMutableMap
5657
import org.bson.codecs.kotlin.samples.DataClassWithMutableSet
58+
import org.bson.codecs.kotlin.samples.DataClassWithNativeArrays
5759
import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterized
5860
import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterizedDataClass
5961
import org.bson.codecs.kotlin.samples.DataClassWithNullableGeneric
@@ -110,6 +112,59 @@ class DataClassCodecTest {
110112
assertRoundTrips(expected, dataClass)
111113
}
112114

115+
@Test
116+
fun testDataClassWithArrays() {
117+
val expected =
118+
"""{
119+
| "arraySimple": ["a", "b", "c", "d"],
120+
| "nestedArrays": [["e", "f"], [], ["g", "h"]],
121+
| "arrayOfMaps": [{"A": ["aa"], "B": ["bb"]}, {}, {"C": ["cc", "ccc"]}],
122+
|}"""
123+
.trimMargin()
124+
125+
val dataClass =
126+
DataClassWithArrays(
127+
arrayOf("a", "b", "c", "d"),
128+
arrayOf(arrayOf("e", "f"), emptyArray(), arrayOf("g", "h")),
129+
arrayOf(
130+
mapOf("A" to arrayOf("aa"), "B" to arrayOf("bb")), emptyMap(), mapOf("C" to arrayOf("cc", "ccc"))))
131+
132+
assertRoundTrips(expected, dataClass)
133+
}
134+
135+
@Test
136+
fun testDataClassWithNativeArrays() {
137+
val expected =
138+
"""{
139+
| "booleanArray": [true, false],
140+
| "byteArray": [1, 2],
141+
| "charArray": ["a", "b"],
142+
| "doubleArray": [ 1.1, 2.2, 3.3],
143+
| "floatArray": [1.0, 2.0, 3.0],
144+
| "intArray": [10, 20, 30, 40],
145+
| "longArray": [{ "$numberLong": "111" }, { "$numberLong": "222" }, { "$numberLong": "333" }],
146+
| "shortArray": [1, 2, 3],
147+
| "listOfArrays": [[true, false], [false, true]],
148+
| "mapOfArrays": {"A": [1, 2], "B":[], "C": [3, 4]}
149+
|}"""
150+
.trimMargin()
151+
152+
val dataClass =
153+
DataClassWithNativeArrays(
154+
booleanArrayOf(true, false),
155+
byteArrayOf(1, 2),
156+
charArrayOf('a', 'b'),
157+
doubleArrayOf(1.1, 2.2, 3.3),
158+
floatArrayOf(1.0f, 2.0f, 3.0f),
159+
intArrayOf(10, 20, 30, 40),
160+
longArrayOf(111, 222, 333),
161+
shortArrayOf(1, 2, 3),
162+
listOf(booleanArrayOf(true, false), booleanArrayOf(false, true)),
163+
mapOf(Pair("A", intArrayOf(1, 2)), Pair("B", intArrayOf()), Pair("C", intArrayOf(3, 4))))
164+
165+
assertRoundTrips(expected, dataClass)
166+
}
167+
113168
@Test
114169
fun testDataClassWithDefaults() {
115170
val expectedDefault =
@@ -516,5 +571,5 @@ class DataClassCodecTest {
516571
assertEquals(expected, decoded)
517572
}
518573

519-
private fun registry() = fromProviders(DataClassCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)
574+
private fun registry() = fromProviders(ArrayCodecProvider(), DataClassCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)
520575
}

bson-kotlin/src/test/kotlin/org/bson/codecs/kotlin/samples/DataClasses.kt

+85
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,91 @@ data class DataClassWithCollections(
4949
val mapMap: Map<String, Map<String, Int>>
5050
)
5151

52+
data class DataClassWithArrays(
53+
val arraySimple: Array<String>,
54+
val nestedArrays: Array<Array<String>>,
55+
val arrayOfMaps: Array<Map<String, Array<String>>>
56+
) {
57+
override fun equals(other: Any?): Boolean {
58+
if (this === other) return true
59+
if (javaClass != other?.javaClass) return false
60+
61+
other as DataClassWithArrays
62+
63+
if (!arraySimple.contentEquals(other.arraySimple)) return false
64+
if (!nestedArrays.contentDeepEquals(other.nestedArrays)) return false
65+
66+
if (arrayOfMaps.size != other.arrayOfMaps.size) return false
67+
arrayOfMaps.forEachIndexed { i, map ->
68+
val otherMap = other.arrayOfMaps[i]
69+
if (map.keys != otherMap.keys) return false
70+
map.keys.forEach { key -> if (!map[key].contentEquals(otherMap[key])) return false }
71+
}
72+
73+
return true
74+
}
75+
76+
override fun hashCode(): Int {
77+
var result = arraySimple.contentHashCode()
78+
result = 31 * result + nestedArrays.contentDeepHashCode()
79+
result = 31 * result + arrayOfMaps.contentHashCode()
80+
return result
81+
}
82+
}
83+
84+
data class DataClassWithNativeArrays(
85+
val booleanArray: BooleanArray,
86+
val byteArray: ByteArray,
87+
val charArray: CharArray,
88+
val doubleArray: DoubleArray,
89+
val floatArray: FloatArray,
90+
val intArray: IntArray,
91+
val longArray: LongArray,
92+
val shortArray: ShortArray,
93+
val listOfArrays: List<BooleanArray>,
94+
val mapOfArrays: Map<String, IntArray>
95+
) {
96+
97+
@SuppressWarnings("ComplexMethod")
98+
override fun equals(other: Any?): Boolean {
99+
if (this === other) return true
100+
if (javaClass != other?.javaClass) return false
101+
102+
other as DataClassWithNativeArrays
103+
104+
if (!booleanArray.contentEquals(other.booleanArray)) return false
105+
if (!byteArray.contentEquals(other.byteArray)) return false
106+
if (!charArray.contentEquals(other.charArray)) return false
107+
if (!doubleArray.contentEquals(other.doubleArray)) return false
108+
if (!floatArray.contentEquals(other.floatArray)) return false
109+
if (!intArray.contentEquals(other.intArray)) return false
110+
if (!longArray.contentEquals(other.longArray)) return false
111+
if (!shortArray.contentEquals(other.shortArray)) return false
112+
113+
if (listOfArrays.size != other.listOfArrays.size) return false
114+
listOfArrays.forEachIndexed { i, value -> if (!value.contentEquals(other.listOfArrays[i])) return false }
115+
116+
if (mapOfArrays.keys != other.mapOfArrays.keys) return false
117+
mapOfArrays.keys.forEach { key -> if (!mapOfArrays[key].contentEquals(other.mapOfArrays[key])) return false }
118+
119+
return true
120+
}
121+
122+
override fun hashCode(): Int {
123+
var result = booleanArray.contentHashCode()
124+
result = 31 * result + byteArray.contentHashCode()
125+
result = 31 * result + charArray.contentHashCode()
126+
result = 31 * result + doubleArray.contentHashCode()
127+
result = 31 * result + floatArray.contentHashCode()
128+
result = 31 * result + intArray.contentHashCode()
129+
result = 31 * result + longArray.contentHashCode()
130+
result = 31 * result + shortArray.contentHashCode()
131+
result = 31 * result + listOfArrays.hashCode()
132+
result = 31 * result + mapOfArrays.hashCode()
133+
return result
134+
}
135+
}
136+
52137
data class DataClassWithDefaults(
53138
val boolean: Boolean = false,
54139
val string: String = "String",

driver-core/src/main/com/mongodb/KotlinCodecProvider.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,17 @@
1919
import org.bson.codecs.Codec;
2020
import org.bson.codecs.configuration.CodecProvider;
2121
import org.bson.codecs.configuration.CodecRegistry;
22+
import org.bson.codecs.kotlin.ArrayCodecProvider;
2223
import org.bson.codecs.kotlin.DataClassCodecProvider;
2324
import org.bson.codecs.kotlinx.KotlinSerializerCodecProvider;
2425

2526
import java.lang.reflect.Type;
2627
import java.util.Collections;
2728
import java.util.List;
2829

30+
31+
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
32+
2933
/**
3034
* A CodecProvider for Kotlin data classes.
3135
* Delegates to {@code org.bson.codecs.kotlinx.KotlinSerializerCodecProvider}
@@ -56,7 +60,7 @@ public class KotlinCodecProvider implements CodecProvider {
5660
possibleCodecProvider = null;
5761
try {
5862
Class.forName("org.bson.codecs.kotlin.DataClassCodecProvider"); // Kotlin bson canary test
59-
possibleCodecProvider = new DataClassCodecProvider();
63+
possibleCodecProvider = fromProviders(new ArrayCodecProvider(), new DataClassCodecProvider());
6064
} catch (ClassNotFoundException e) {
6165
// No kotlin data class support
6266
}

0 commit comments

Comments
 (0)