From 87ee5fff02e1d21507618a9eda2a13b239deab70 Mon Sep 17 00:00:00 2001 From: Hidde Wieringa Date: Sun, 23 May 2021 18:59:35 +0200 Subject: [PATCH] Add Kotlin serializer and basic test --- .../kotlin/serialization/KotlinSerializer.kt | 177 ++++++++++++++++++ .../serialization/KotlinSerializerTest.kt | 91 +++++++++ pom.xml | 16 +- 3 files changed, 283 insertions(+), 1 deletion(-) create mode 100644 kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt create mode 100644 kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializerTest.kt diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt new file mode 100644 index 00000000..35094d2f --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt @@ -0,0 +1,177 @@ +/* + * Copyright (c) 2010-2020. Axon Framework + * + * 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.axonframework.extensions.kotlin.serialization + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import org.axonframework.common.ObjectUtils +import org.axonframework.serialization.AnnotationRevisionResolver +import org.axonframework.serialization.ChainingConverter +import org.axonframework.serialization.Converter +import org.axonframework.serialization.RevisionResolver +import org.axonframework.serialization.SerializedObject +import org.axonframework.serialization.SerializedType +import org.axonframework.serialization.Serializer +import org.axonframework.serialization.SimpleSerializedObject +import org.axonframework.serialization.SimpleSerializedType +import org.axonframework.serialization.UnknownSerializedType +import kotlin.reflect.KClass +import kotlin.reflect.full.companionObject +import kotlin.reflect.full.companionObjectInstance + +/** + * Implementation of Axon Serializer that uses a kotlinx.serialization implementation. + * The serialized format is JSON. + * + * @see kotlinx.serialization.Serializer + * @see org.axonframework.serialization.Serializer + */ +class KotlinSerializer( + private val revisionResolver: RevisionResolver, + private val converter: Converter, + private val json: Json +) : Serializer { + + private val serializerCache: MutableMap, KSerializer<*>> = mutableMapOf() + + override fun serialize(`object`: Any?, expectedRepresentation: Class): SerializedObject { + try { + val type = ObjectUtils.nullSafeTypeOf(`object`) + val serializer: KSerializer = type.serializer() + return when { + expectedRepresentation.isAssignableFrom(String::class.java) -> + SimpleSerializedObject( + json.encodeToString(serializer, `object`) as T, + expectedRepresentation, + typeForClass(type) + ) + + expectedRepresentation.isAssignableFrom(JsonElement::class.java) -> + SimpleSerializedObject( + json.encodeToJsonElement(serializer, `object`) as T, + expectedRepresentation, + typeForClass(type) + ) + + else -> + throw org.axonframework.serialization.SerializationException("Cannot serialize type $type to representation $expectedRepresentation. String and JsonElement are supported.") + } + } catch (ex: SerializationException) { + throw org.axonframework.serialization.SerializationException("Cannot serialize type ${`object`?.javaClass?.name} to representation $expectedRepresentation.", ex) + } + } + + override fun canSerializeTo(expectedRepresentation: Class): Boolean = + expectedRepresentation == String::class.java || + expectedRepresentation == JsonElement::class.java + + override fun deserialize(serializedObject: SerializedObject?): T? { + try { + if (serializedObject == null) { + return null + } + + if (serializedObject.type == SerializedType.emptyType()) { + return null + } + + val serializer: KSerializer = serializedObject.serializer() + return when { + serializedObject.contentType.isAssignableFrom(String::class.java) -> + json.decodeFromString(serializer, serializedObject.data as String) + + serializedObject.contentType.isAssignableFrom(JsonElement::class.java) -> + json.decodeFromJsonElement(serializer, serializedObject.data as JsonElement) + + else -> + throw org.axonframework.serialization.SerializationException("Cannot deserialize from content type ${serializedObject.contentType} to type ${serializedObject.type}. String and JsonElement are supported.") + } + } catch (ex: SerializationException) { + throw org.axonframework.serialization.SerializationException( + "Could not deserialize from content type ${serializedObject?.contentType} to type ${serializedObject?.type}", + ex + ) + } + } + + private fun SerializedObject.serializer(): KSerializer = + classForType(type).serializer() as KSerializer + + /** + * When a type is compiled by the Kotlin compiler extension, a companion object + * is created which contains a method `serializer()`. This method should be called + * to get the serializer of the class. + * + * In a 'normal' serialization environment, you would call the MyClass.serializer() + * method directly. Here we are in a generic setting, and need reflection to call + * the method. + * + * This method caches the reflection mapping from class to serializer for efficiency. + */ + private fun Class.serializer(): KSerializer = + serializerCache.computeIfAbsent(this) { + // Class: T must be non-null + val kClass = (this as Class).kotlin + + val companion = kClass.companionObject + ?: throw SerializationException("Class $this has no companion object. Did you mark it as @Serializable?") + + val serializerMethod = companion.java.getMethod("serializer") + ?: throw SerializationException("Class $this has no serializer() method. Did you mark it as @Serializable?") + + serializerMethod.invoke(kClass.companionObjectInstance) as KSerializer<*> + } as KSerializer + + override fun classForType(type: SerializedType): Class<*> = + if (SerializedType.emptyType() == type) { + Void.TYPE + } else { + try { + Class.forName(type.name) + } catch (e: ClassNotFoundException) { + UnknownSerializedType::class.java + } + } + + override fun typeForClass(type: Class<*>): SerializedType = + SimpleSerializedType(type.name, revisionOf(type)) + + private fun revisionOf(type: Class<*>): String? = + revisionResolver.revisionOf(type) + + override fun getConverter(): Converter = + converter + +} + +class KotlinSerializerConfiguration { + var revisionResolver: RevisionResolver = AnnotationRevisionResolver() + var converter: Converter = ChainingConverter() + var json: Json = Json +} + +fun kotlinSerializer(init: KotlinSerializerConfiguration.() -> Unit = {}): KotlinSerializer { + val configuration = KotlinSerializerConfiguration() + configuration.init() + return KotlinSerializer( + configuration.revisionResolver, + configuration.converter, + configuration.json, + ) +} \ No newline at end of file diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializerTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializerTest.kt new file mode 100644 index 00000000..7337f132 --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializerTest.kt @@ -0,0 +1,91 @@ +package org.axonframework.extensions.kotlin.serialization + +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonElement +import org.axonframework.serialization.AnnotationRevisionResolver +import org.axonframework.serialization.ChainingConverter +import org.axonframework.serialization.SerializedType +import org.axonframework.serialization.SimpleSerializedObject +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +class KotlinSerializerTest { + + /** + * This class will automatically become serializable through the Kotlin serialization compiler plugin. + */ + @Serializable + data class TestData( + val name: String, + val value: Float? + ) + + @Test + fun canSerializeTo() { + val serializer = kotlinSerializer() + + assertTrue(serializer.canSerializeTo(String::class.java)) + assertTrue(serializer.canSerializeTo(JsonElement::class.java)) + } + + @Test + fun `configuration options`() { + val serializer = kotlinSerializer { + json = Json + converter = ChainingConverter() + revisionResolver = AnnotationRevisionResolver() + } + assertNotNull(serializer) + } + + @Test + fun serialize() { + val serializer = kotlinSerializer() + + val emptySerialized = serializer.serialize(TestData("", null), String::class.java) + assertEquals("SimpleSerializedType[org.axonframework.extensions.kotlin.serialization.KotlinSerializerTest\$TestData] (revision null)", emptySerialized.type.toString()) + assertEquals("""{"name":"","value":null}""", emptySerialized.data) + assertEquals(String::class.java, emptySerialized.contentType) + + val filledSerialized = serializer.serialize(TestData("name", 1.23f), String::class.java) + assertEquals("SimpleSerializedType[org.axonframework.extensions.kotlin.serialization.KotlinSerializerTest\$TestData] (revision null)", filledSerialized.type.toString()) + assertEquals("""{"name":"name","value":1.23}""", filledSerialized.data) + assertEquals(String::class.java, filledSerialized.contentType) + } + + @Test + fun deserialize() { + val serializer = kotlinSerializer() + + val nullDeserialized: Any? = serializer.deserialize(SimpleSerializedObject( + "", + String::class.java, + SerializedType.emptyType() + )) + assertNull(nullDeserialized) + + val emptyDeserialized: Any? = serializer.deserialize(SimpleSerializedObject( + """{"name":"","value":null}""", + String::class.java, + TestData::class.java.name, + null + )) + assertNotNull(emptyDeserialized as TestData) + assertEquals(emptyDeserialized.name, "") + assertEquals(emptyDeserialized.value, null) + + val filledDeserialized: Any? = serializer.deserialize(SimpleSerializedObject( + """{"name":"name","value":1.23}""", + String::class.java, + TestData::class.java.name, + null + )) + assertNotNull(filledDeserialized as TestData) + assertEquals(filledDeserialized.name, "name") + assertEquals(filledDeserialized.value, 1.23f) + } +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 319c361e..c8438252 100644 --- a/pom.xml +++ b/pom.xml @@ -34,8 +34,9 @@ 4.5 UTF-8 - 1.4.32 + 1.5.0 2.0.6 + 1.2.1 2.12.3 1.11.0 5.7.2 @@ -130,6 +131,13 @@ test + + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${kotlin-serialization.version} + + org.junit.jupiter @@ -434,6 +442,7 @@ no-arg all-open + kotlinx-serialization @@ -477,6 +486,11 @@ kotlin-maven-noarg ${kotlin.version} + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} +