Skip to content

Commit a0add9c

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 3fc18bf commit a0add9c

File tree

6 files changed

+308
-4
lines changed

6 files changed

+308
-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
@@ -210,7 +210,7 @@ internal data class DataClassCodec<T : Any>(
210210
is KTypeParameter -> {
211211
when (val pType = typeMap[kParameter.type.classifier] ?: kParameter.type.javaType) {
212212
is Class<*> ->
213-
codecRegistry.getCodec(kParameter, (pType as Class<Any>).kotlin.javaObjectType, emptyList())
213+
codecRegistry.getCodec(kParameter, (pType as Class<Any>).kotlin.java, emptyList())
214214
is ParameterizedType ->
215215
codecRegistry.getCodec(
216216
kParameter,
@@ -235,11 +235,14 @@ internal data class DataClassCodec<T : Any>(
235235
@Suppress("UNCHECKED_CAST")
236236
private fun CodecRegistry.getCodec(kParameter: KParameter, clazz: Class<Any>, types: List<Type>): Codec<Any> {
237237
val codec =
238-
if (types.isEmpty()) {
238+
if (clazz.isArray) {
239+
ArrayCodec.create(clazz.kotlin, types, this)
240+
} else if (types.isEmpty()) {
239241
this.get(clazz)
240242
} else {
241243
this.get(clazz, types)
242244
}
245+
243246
return kParameter.findAnnotation<BsonRepresentation>()?.let {
244247
if (codec !is RepresentationConfigurable<*>) {
245248
throw CodecConfigurationException(

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

+56-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import org.bson.codecs.kotlin.samples.DataClassSealedA
3737
import org.bson.codecs.kotlin.samples.DataClassSealedB
3838
import org.bson.codecs.kotlin.samples.DataClassSealedC
3939
import org.bson.codecs.kotlin.samples.DataClassSelfReferential
40+
import org.bson.codecs.kotlin.samples.DataClassWithArrays
4041
import org.bson.codecs.kotlin.samples.DataClassWithBooleanMapKey
4142
import org.bson.codecs.kotlin.samples.DataClassWithBsonConstructor
4243
import org.bson.codecs.kotlin.samples.DataClassWithBsonDiscriminator
@@ -56,6 +57,7 @@ import org.bson.codecs.kotlin.samples.DataClassWithListThatLastItemDefaultsToNul
5657
import org.bson.codecs.kotlin.samples.DataClassWithMutableList
5758
import org.bson.codecs.kotlin.samples.DataClassWithMutableMap
5859
import org.bson.codecs.kotlin.samples.DataClassWithMutableSet
60+
import org.bson.codecs.kotlin.samples.DataClassWithNativeArrays
5961
import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterized
6062
import org.bson.codecs.kotlin.samples.DataClassWithNestedParameterizedDataClass
6163
import org.bson.codecs.kotlin.samples.DataClassWithNullableGeneric
@@ -112,6 +114,59 @@ class DataClassCodecTest {
112114
assertRoundTrips(expected, dataClass)
113115
}
114116

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

537-
private fun registry() = fromProviders(DataClassCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)
592+
private fun registry() = fromProviders(ArrayCodecProvider(), DataClassCodecProvider(), Bson.DEFAULT_CODEC_REGISTRY)
538593
}

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

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
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

@@ -27,6 +28,7 @@
2728
import java.util.List;
2829

2930
import static org.bson.internal.ProvidersCodecRegistry.getFromCodecProvider;
31+
import static org.bson.codecs.configuration.CodecRegistries.fromProviders;
3032

3133
/**
3234
* A CodecProvider for Kotlin data classes.
@@ -58,7 +60,7 @@ public class KotlinCodecProvider implements CodecProvider {
5860
possibleCodecProvider = null;
5961
try {
6062
Class.forName("org.bson.codecs.kotlin.DataClassCodecProvider"); // Kotlin bson canary test
61-
possibleCodecProvider = new DataClassCodecProvider();
63+
possibleCodecProvider = fromProviders(new ArrayCodecProvider(), new DataClassCodecProvider());
6264
} catch (ClassNotFoundException e) {
6365
// No kotlin data class support
6466
}

0 commit comments

Comments
 (0)