diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/QueryGatewayExtensions.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/QueryGatewayExtensions.kt index fdfa12d3..ec417e53 100644 --- a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/QueryGatewayExtensions.kt +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/QueryGatewayExtensions.kt @@ -41,7 +41,7 @@ import java.util.stream.Stream * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.queryMany(query: Q): CompletableFuture> { +inline fun QueryGateway.queryMany(query: Q): CompletableFuture> { return this.query(query, ResponseTypes.multipleInstancesOf(R::class.java)) } @@ -57,7 +57,7 @@ inline fun QueryGateway.queryMany(query: Q): CompletableF * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.queryMany(queryName: String, query: Q): CompletableFuture> { +inline fun QueryGateway.queryMany(queryName: String, query: Q): CompletableFuture> { return this.query(queryName, query, ResponseTypes.multipleInstancesOf(R::class.java)) } @@ -72,7 +72,7 @@ inline fun QueryGateway.queryMany(queryName: String, quer * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.query(query: Q): CompletableFuture { +inline fun QueryGateway.query(query: Q): CompletableFuture { return this.query(query, ResponseTypes.instanceOf(R::class.java)) } @@ -88,7 +88,7 @@ inline fun QueryGateway.query(query: Q): CompletableFutur * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.query(queryName: String, query: Q): CompletableFuture { +inline fun QueryGateway.query(queryName: String, query: Q): CompletableFuture { return this.query(queryName, query, ResponseTypes.instanceOf(R::class.java)) } @@ -103,7 +103,7 @@ inline fun QueryGateway.query(queryName: String, query: Q * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.queryOptional(query: Q): CompletableFuture> { +inline fun QueryGateway.queryOptional(query: Q): CompletableFuture> { return this.query(query, ResponseTypes.optionalInstanceOf(R::class.java)) } @@ -119,7 +119,7 @@ inline fun QueryGateway.queryOptional(query: Q): Completa * @see ResponseTypes * @since 0.1.0 */ -inline fun QueryGateway.queryOptional(queryName: String, query: Q): CompletableFuture> { +inline fun QueryGateway.queryOptional(queryName: String, query: Q): CompletableFuture> { return this.query(queryName, query, ResponseTypes.optionalInstanceOf(R::class.java)) } @@ -136,7 +136,7 @@ inline fun QueryGateway.queryOptional(queryName: String, * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGather(query: Q, timeout: Long, +inline fun QueryGateway.scatterGather(query: Q, timeout: Long, timeUnit: TimeUnit): Stream { return this.scatterGather(query, ResponseTypes.instanceOf(R::class.java), timeout, timeUnit) } @@ -155,7 +155,7 @@ inline fun QueryGateway.scatterGather(query: Q, timeout: * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGather(queryName: String, query: Q, timeout: Long, +inline fun QueryGateway.scatterGather(queryName: String, query: Q, timeout: Long, timeUnit: TimeUnit): Stream { return this.scatterGather(queryName, query, ResponseTypes.instanceOf(R::class.java), timeout, timeUnit) } @@ -173,7 +173,7 @@ inline fun QueryGateway.scatterGather(queryName: String, * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGatherMany(query: Q, timeout: Long, +inline fun QueryGateway.scatterGatherMany(query: Q, timeout: Long, timeUnit: TimeUnit): Stream> { return this.scatterGather(query, ResponseTypes.multipleInstancesOf(R::class.java), timeout, timeUnit) } @@ -192,7 +192,7 @@ inline fun QueryGateway.scatterGatherMany(query: Q, timeo * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGatherMany(queryName: String, query: Q, timeout: Long, +inline fun QueryGateway.scatterGatherMany(queryName: String, query: Q, timeout: Long, timeUnit: TimeUnit): Stream> { return this.scatterGather(queryName, query, ResponseTypes.multipleInstancesOf(R::class.java), timeout, timeUnit) } @@ -210,7 +210,7 @@ inline fun QueryGateway.scatterGatherMany(queryName: Stri * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGatherOptional(query: Q, timeout: Long, +inline fun QueryGateway.scatterGatherOptional(query: Q, timeout: Long, timeUnit: TimeUnit): Stream> { return this.scatterGather(query, ResponseTypes.optionalInstanceOf(R::class.java), timeout, timeUnit) } @@ -229,7 +229,7 @@ inline fun QueryGateway.scatterGatherOptional(query: Q, t * @see ResponseTypes * @since 0.2.0 */ -inline fun QueryGateway.scatterGatherOptional(queryName: String, query: Q, timeout: Long, +inline fun QueryGateway.scatterGatherOptional(queryName: String, query: Q, timeout: Long, timeUnit: TimeUnit): Stream> { return this.scatterGather(queryName, query, ResponseTypes.optionalInstanceOf(R::class.java), timeout, timeUnit) } @@ -246,7 +246,7 @@ inline fun QueryGateway.scatterGatherOptional(queryName: * @see ResponseTypes * @since 0.3.0 */ -inline fun QueryGateway.subscriptionQuery(query: Q): SubscriptionQueryResult = +inline fun QueryGateway.subscriptionQuery(query: Q): SubscriptionQueryResult = this.subscriptionQuery(query, ResponseTypes.instanceOf(I::class.java), ResponseTypes.instanceOf(U::class.java)) /** @@ -262,5 +262,5 @@ inline fun QueryGateway.subscriptionQuery(quer * @see ResponseTypes * @since 0.3.0 */ -inline fun QueryGateway.subscriptionQuery(queryName: String, query: Q): SubscriptionQueryResult = +inline fun QueryGateway.subscriptionQuery(queryName: String, query: Q): SubscriptionQueryResult = this.subscriptionQuery(queryName, query, ResponseTypes.instanceOf(I::class.java), ResponseTypes.instanceOf(U::class.java)) \ No newline at end of file diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/ArrayResponseType.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/ArrayResponseType.kt new file mode 100644 index 00000000..60e127c9 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/ArrayResponseType.kt @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2010-2024. 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 org.axonframework.common.ReflectionUtils +import org.axonframework.common.TypeReflectionUtils +import org.axonframework.messaging.responsetypes.AbstractResponseType +import org.axonframework.messaging.responsetypes.InstanceResponseType +import org.axonframework.messaging.responsetypes.ResponseType +import java.lang.reflect.Type +import java.util.concurrent.Future + +/** + * A [ResponseType] implementation that will match with query handlers which return a multiple instances of the + * expected response type. If matching succeeds, the [ResponseType.convert] function will be called, which + * will cast the query handler it's response to an [Array] with element type [E]. + * + * @param E The element type which will be matched against and converted to + * @author Gerard de Leeuw + * @see org.axonframework.messaging.responsetypes.MultipleInstancesResponseType + */ +class ArrayResponseType(elementType: Class) : AbstractResponseType>(elementType) { + + companion object { + /** + * Indicates that the response matches with the [Type] while returning an iterable result. + * + * @see ResponseType.MATCH + * + * @see ResponseType.NO_MATCH + */ + const val ITERABLE_MATCH = 1024 + } + + private val instanceResponseType: InstanceResponseType = InstanceResponseType(elementType) + + /** + * Match the query handler's response [Type] with this implementation's [E]. + * Will return true in the following scenarios: + * + * * If the response type is an [Array] + * * If the response type is a [E] + * + * If there is no match at all, it will return false to indicate a non-match. + * + * @param responseType the response [Type] of the query handler which is matched against + * @return true for [Array] or [E] and [ResponseType.NO_MATCH] for non-matches + */ + override fun matches(responseType: Type): Boolean = + matchRank(responseType) > NO_MATCH + + /** + * Match the query handler's response [Type] with this implementation's [E]. + * Will return a value greater than 0 in the following scenarios: + * + * * [ITERABLE_MATCH]: If the response type is an [Array] + * * [ResponseType.MATCH]: If the response type is a [E] + * + * If there is no match at all, it will return [ResponseType.NO_MATCH] to indicate a non-match. + * + * @param responseType the response [Type] of the query handler which is matched against + * @return [ITERABLE_MATCH] for [Array], [ResponseType.MATCH] for [E] and [ResponseType.NO_MATCH] for non-matches + */ + override fun matchRank(responseType: Type): Int = when { + isMatchingArray(responseType) -> ITERABLE_MATCH + else -> instanceResponseType.matchRank(responseType) + } + + /** + * Converts the given [response] of type [Object] into an [Array] with element type [E] from + * this [ResponseType] instance. Should only be called if [ResponseType.matches] returns true. + * Will throw an [IllegalArgumentException] if the given response + * is not convertible to an [Array] of the expected response type. + * + * @param response the [Object] to convert into an [Array] with element type [E] + * @return an [Array] with element type [E], based on the given [response] + */ + override fun convert(response: Any): Array { + val responseType: Class<*> = response.javaClass + if (responseType.isArray) { + @Suppress("UNCHECKED_CAST") + return response as Array + } + throw IllegalArgumentException( + "Retrieved response [$responseType] is not convertible to an array with the expected element type [$expectedResponseType]" + ) + } + + @Suppress("UNCHECKED_CAST") + override fun responseMessagePayloadType(): Class> = + Array::class.java as Class> + + override fun toString(): String = "ArrayResponseType[$expectedResponseType]" + + private fun isMatchingArray(responseType: Type): Boolean { + val unwrapped = ReflectionUtils.unwrapIfType(responseType, Future::class.java) + val iterableType = TypeReflectionUtils.getExactSuperType(unwrapped, Array::class.java) + return iterableType != null && isParameterizedTypeOfExpectedType(iterableType) + } +} diff --git a/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/AxonSerializers.kt b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/AxonSerializers.kt new file mode 100644 index 00000000..564d1ac2 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/AxonSerializers.kt @@ -0,0 +1,346 @@ +/* + * Copyright (c) 2010-2024. 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.PolymorphicSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.builtins.nullable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.polymorphic +import kotlinx.serialization.modules.subclass +import org.axonframework.eventhandling.GapAwareTrackingToken +import org.axonframework.eventhandling.GlobalSequenceTrackingToken +import org.axonframework.eventhandling.MergedTrackingToken +import org.axonframework.eventhandling.MultiSourceTrackingToken +import org.axonframework.eventhandling.ReplayToken +import org.axonframework.eventhandling.TrackingToken +import org.axonframework.eventhandling.scheduling.ScheduleToken +import org.axonframework.eventhandling.scheduling.java.SimpleScheduleToken +import org.axonframework.eventhandling.scheduling.quartz.QuartzScheduleToken +import org.axonframework.eventhandling.tokenstore.ConfigToken +import org.axonframework.messaging.responsetypes.InstanceResponseType +import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType +import org.axonframework.messaging.responsetypes.OptionalResponseType +import org.axonframework.messaging.responsetypes.ResponseType +import kotlin.reflect.KClass + +private val trackingTokenSerializer = PolymorphicSerializer(TrackingToken::class).nullable + +val AxonSerializersModule = SerializersModule { + contextual(ConfigToken::class) { ConfigTokenSerializer } + contextual(GapAwareTrackingToken::class) { GapAwareTrackingTokenSerializer } + contextual(MultiSourceTrackingToken::class) { MultiSourceTrackingTokenSerializer } + contextual(MergedTrackingToken::class) { MergedTrackingTokenSerializer } + contextual(ReplayToken::class) { ReplayTokenSerializer } + contextual(GlobalSequenceTrackingToken::class) { GlobalSequenceTrackingTokenSerializer } + polymorphic(TrackingToken::class) { + subclass(ConfigTokenSerializer) + subclass(GapAwareTrackingTokenSerializer) + subclass(MultiSourceTrackingTokenSerializer) + subclass(MergedTrackingTokenSerializer) + subclass(ReplayTokenSerializer) + subclass(GlobalSequenceTrackingTokenSerializer) + } + + contextual(SimpleScheduleToken::class) { SimpleScheduleTokenSerializer } + contextual(QuartzScheduleToken::class) { QuartzScheduleTokenSerializer } + polymorphic(ScheduleToken::class) { + subclass(SimpleScheduleTokenSerializer) + subclass(QuartzScheduleTokenSerializer) + } + + contextual(InstanceResponseType::class) { InstanceResponseTypeSerializer } + contextual(OptionalResponseType::class) { OptionalResponseTypeSerializer } + contextual(MultipleInstancesResponseType::class) { MultipleInstancesResponseTypeSerializer } + contextual(ArrayResponseType::class) { ArrayResponseTypeSerializer } + polymorphic(ResponseType::class) { + subclass(InstanceResponseTypeSerializer) + subclass(OptionalResponseTypeSerializer) + subclass(MultipleInstancesResponseTypeSerializer) + subclass(ArrayResponseTypeSerializer) + } +} + +object ConfigTokenSerializer : KSerializer { + + private val mapSerializer = MapSerializer(String.serializer(), String.serializer()) + override val descriptor = buildClassSerialDescriptor(ConfigToken::class.java.name) { + element>("config") + } + + override fun deserialize(decoder: Decoder): ConfigToken = decoder.decodeStructure(descriptor) { + var config: Map? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> config = decodeSerializableElement(descriptor, index, mapSerializer) + } + } + ConfigToken( + config ?: throw SerializationException("Element 'config' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: ConfigToken) = encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, mapSerializer, value.config) + } +} + +object GapAwareTrackingTokenSerializer : KSerializer { + + private val setSerializer = SetSerializer(Long.serializer()) + override val descriptor = buildClassSerialDescriptor(GapAwareTrackingToken::class.java.name) { + element("index") + element("gaps", setSerializer.descriptor) + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var gapIndex: Long? = null + var gaps: Set? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> gapIndex = decodeLongElement(descriptor, index) + 1 -> gaps = decodeSerializableElement(descriptor, index, setSerializer) + } + } + GapAwareTrackingToken( + gapIndex ?: throw SerializationException("Element 'gapIndex' is missing"), + gaps ?: throw SerializationException("Element 'gaps' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: GapAwareTrackingToken) = encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.index) + encodeSerializableElement(descriptor, 1, setSerializer, value.gaps) + } +} + +object MultiSourceTrackingTokenSerializer : KSerializer { + + private val mapSerializer = MapSerializer(String.serializer(), trackingTokenSerializer) + override val descriptor = buildClassSerialDescriptor(MultiSourceTrackingToken::class.java.name) { + element>("trackingTokens") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var trackingTokens: Map? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> trackingTokens = decodeSerializableElement(descriptor, index, mapSerializer) + } + } + MultiSourceTrackingToken( + trackingTokens ?: throw SerializationException("Element 'trackingTokens' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: MultiSourceTrackingToken) = encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, mapSerializer, value.trackingTokens) + } +} + +object MergedTrackingTokenSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor(MergedTrackingToken::class.java.name) { + element("lowerSegmentToken") + element("upperSegmentToken") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var lowerSegmentToken: TrackingToken? = null + var upperSegmentToken: TrackingToken? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> lowerSegmentToken = decodeSerializableElement(descriptor, index, trackingTokenSerializer) + 1 -> upperSegmentToken = decodeSerializableElement(descriptor, index, trackingTokenSerializer) + } + } + MergedTrackingToken( + lowerSegmentToken ?: throw SerializationException("Element 'lowerSegmentToken' is missing"), + upperSegmentToken ?: throw SerializationException("Element 'upperSegmentToken' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: MergedTrackingToken) = encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, trackingTokenSerializer, value.lowerSegmentToken()) + encodeSerializableElement(descriptor, 1, trackingTokenSerializer, value.upperSegmentToken()) + } +} + +object ReplayTokenSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor(ReplayToken::class.java.name) { + element("tokenAtReset") + element("currentToken") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var tokenAtReset: TrackingToken? = null + var currentToken: TrackingToken? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> tokenAtReset = decodeSerializableElement(descriptor, index, trackingTokenSerializer) + 1 -> currentToken = decodeSerializableElement(descriptor, index, trackingTokenSerializer) + } + } + ReplayToken( + tokenAtReset ?: throw SerializationException("Element 'tokenAtReset' is missing"), + currentToken, + ) + } + + override fun serialize(encoder: Encoder, value: ReplayToken) = encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, trackingTokenSerializer, value.tokenAtReset) + encodeSerializableElement(descriptor, 1, trackingTokenSerializer, value.currentToken) + } +} + +object GlobalSequenceTrackingTokenSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor(GlobalSequenceTrackingToken::class.java.name) { + element("globalIndex") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var globalIndex: Long? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> globalIndex = decodeLongElement(descriptor, index) + } + } + GlobalSequenceTrackingToken( + globalIndex ?: throw SerializationException("Element 'globalIndex' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: GlobalSequenceTrackingToken) = encoder.encodeStructure(descriptor) { + encodeLongElement(descriptor, 0, value.globalIndex) + } +} + +object SimpleScheduleTokenSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor(SimpleScheduleToken::class.java.name) { + element("tokenId") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var tokenId: String? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> tokenId = decodeStringElement(descriptor, index) + } + } + SimpleScheduleToken( + tokenId ?: throw SerializationException("Element 'tokenId' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: SimpleScheduleToken) = encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.tokenId) + } +} + +object QuartzScheduleTokenSerializer : KSerializer { + + override val descriptor = buildClassSerialDescriptor(QuartzScheduleToken::class.java.name) { + element("jobIdentifier") + element("groupIdentifier") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var jobIdentifier: String? = null + var groupIdentifier: String? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> jobIdentifier = decodeStringElement(descriptor, index) + 1 -> groupIdentifier = decodeStringElement(descriptor, index) + } + } + QuartzScheduleToken( + jobIdentifier ?: throw SerializationException("Element 'jobIdentifier' is missing"), + groupIdentifier ?: throw SerializationException("Element 'groupIdentifier' is missing"), + ) + } + + override fun serialize(encoder: Encoder, value: QuartzScheduleToken) = encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.jobIdentifier) + encodeStringElement(descriptor, 1, value.groupIdentifier) + } +} + +abstract class ResponseTypeSerializer>(kClass: KClass, private val factory: (Class<*>) -> R) : KSerializer { + + override val descriptor = buildClassSerialDescriptor(kClass.java.name) { + element("expectedResponseType") + } + + override fun deserialize(decoder: Decoder) = decoder.decodeStructure(descriptor) { + var expectedResponseType: Class<*>? = null + while (true) { + val index = decodeElementIndex(descriptor) + if (index == CompositeDecoder.DECODE_DONE) break + when (index) { + 0 -> expectedResponseType = Class.forName(decodeStringElement(descriptor, index)) + } + } + factory( + expectedResponseType ?: throw SerializationException("Element 'expectedResponseType' is missing") + ) + } + + override fun serialize(encoder: Encoder, value: R) = encoder.encodeStructure(descriptor) { + encodeStringElement(descriptor, 0, value.expectedResponseType.name) + } +} + +object InstanceResponseTypeSerializer : KSerializer>, + ResponseTypeSerializer>(InstanceResponseType::class, { InstanceResponseType(it) }) + +object OptionalResponseTypeSerializer : KSerializer>, + ResponseTypeSerializer>(OptionalResponseType::class, { OptionalResponseType(it) }) + +object MultipleInstancesResponseTypeSerializer : KSerializer>, + ResponseTypeSerializer>(MultipleInstancesResponseType::class, { MultipleInstancesResponseType(it) }) + +object ArrayResponseTypeSerializer : KSerializer>, + ResponseTypeSerializer>(ArrayResponseType::class, { ArrayResponseType(it) }) 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..68727fb7 --- /dev/null +++ b/kotlin/src/main/kotlin/org/axonframework/extensions/kotlin/serialization/KotlinSerializer.kt @@ -0,0 +1,197 @@ +/* + * Copyright (c) 2010-2024. 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.BinaryFormat +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerialFormat +import kotlinx.serialization.SerializationException +import kotlinx.serialization.StringFormat +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.serializer +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 java.util.concurrent.ConcurrentHashMap +import org.axonframework.serialization.SerializationException as AxonSerializationException + +/** + * Implementation of Axon Serializer that uses a kotlinx.serialization implementation. + * + * @see kotlinx.serialization.Serializer + * @see org.axonframework.serialization.Serializer + * + * @since 4.9.1 + * @author Gerard de Leeuw + */ +class KotlinSerializer( + private val serialFormat: SerialFormat, + private val revisionResolver: RevisionResolver = AnnotationRevisionResolver(), + private val converter: Converter = ChainingConverter(), +) : Serializer { + + private val serializerCache: MutableMap, KSerializer<*>> = ConcurrentHashMap() + private val unknownSerializedType = UnknownSerializedType::class.java + + override fun getConverter(): Converter = converter + + override fun canSerializeTo(expectedRepresentation: Class): Boolean = when (serialFormat) { + is StringFormat -> converter.canConvert(String::class.java, expectedRepresentation) + is BinaryFormat -> converter.canConvert(ByteArray::class.java, expectedRepresentation) + else -> false + } + + override fun serialize(value: Any?, expectedRepresentation: Class): SerializedObject { + try { + val serializedType = typeForValue(value) + val classForType = classForType(serializedType) + + val serializer = serializerFor(classForType, serializedType) + val serialized: SerializedObject<*> = when (serialFormat) { + is StringFormat -> SimpleSerializedObject( + serialFormat.encodeToString(serializer, value), + String::class.java, + serializedType + ) + + is BinaryFormat -> SimpleSerializedObject( + serialFormat.encodeToByteArray(serializer, value), + ByteArray::class.java, + serializedType + ) + + else -> throw SerializationException("Unsupported serial format: $serialFormat") + } + + return converter.convert(serialized, expectedRepresentation) + } catch (ex: SerializationException) { + throw AxonSerializationException("Cannot serialize type ${value?.javaClass?.name} to representation $expectedRepresentation.", ex) + } + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(serializedObject: SerializedObject): T? { + try { + if (serializedObject.type == SerializedType.emptyType()) { + return null + } + + val classForType = classForType(serializedObject.type) + if (unknownSerializedType.isAssignableFrom(classForType)) { + return UnknownSerializedType(this, serializedObject) as T + } + + val serializer = serializerFor(classForType, serializedObject.type) as KSerializer + return when (serialFormat) { + is StringFormat -> { + val json = converter.convert(serializedObject, String::class.java).data + when { + json.isEmpty() -> null + else -> serialFormat.decodeFromString(serializer, json) + } + } + + is BinaryFormat -> { + val bytes = converter.convert(serializedObject, ByteArray::class.java).data + when { + bytes.isEmpty() -> null + else -> serialFormat.decodeFromByteArray(serializer, bytes) + } + } + + else -> throw SerializationException("Unsupported serial format: $serialFormat") + } + } catch (ex: SerializationException) { + throw AxonSerializationException("Could not deserialize from content type ${serializedObject.contentType} to type ${serializedObject.type}", ex) + } + } + + override fun classForType(type: SerializedType): Class<*> = when (val unwrapped = type.unwrap()) { + SerializedType.emptyType() -> Void.TYPE + else -> try { + Class.forName(unwrapped.name) + } catch (ex: ClassNotFoundException) { + unknownSerializedType + } + } + + override fun typeForClass(type: Class<*>?): SerializedType = + if (type == null || Void.TYPE == type || Void::class.java.isAssignableFrom(type)) { + SimpleSerializedType.emptyType() + } else { + SimpleSerializedType(type.name, revisionResolver.revisionOf(type)) + } + + private fun typeForValue(value: Any?): SerializedType = when (value) { + null -> SimpleSerializedType.emptyType() + Unit -> SimpleSerializedType.emptyType() + is Class<*> -> typeForClass(value) + is Array<*> -> typeForClass(value.javaClass.componentType).wrap("Array") + is List<*> -> typeForClass(value.findCommonInterfaceOfEntries()).wrap("List") + is Set<*> -> typeForClass(value.findCommonInterfaceOfEntries()).wrap("Set") + else -> typeForClass(value.javaClass) + } + + private fun SerializedType.wrap(wrapper: String) = SimpleSerializedType("$wrapper:$name", revision) + private fun SerializedType.unwrap() = SimpleSerializedType(name.substringAfter(':'), revision) + + @Suppress("UNCHECKED_CAST", "OPT_IN_USAGE") + private fun serializerFor(type: Class<*>, serializedType: SerializedType): KSerializer { + val serializer = when { + Void.TYPE == type || Void::class.java.isAssignableFrom(type) -> Unit.serializer() + unknownSerializedType.isAssignableFrom(type) -> throw ClassNotFoundException("Can't load class for type $this") + else -> serializerCache.computeIfAbsent(type, serialFormat.serializersModule::serializer) + } as KSerializer + + return when (serializedType.name.substringBefore(':')) { + "Array" -> ArraySerializer(serializer) + "List" -> ListSerializer(serializer) + "Set" -> SetSerializer(serializer) + else -> serializer + } as KSerializer + } +} + +private fun Iterable<*>.findCommonInterfaceOfEntries(): Class<*>? { + val firstElement = firstOrNull() ?: return null + + val commonTypeInIterable = fold(firstElement.javaClass as Class<*>) { resultingClass, element -> + if (element == null) { + // Element is null, so we just assume the type of the previous elements + resultingClass + } else if (resultingClass.isAssignableFrom(element.javaClass)) { + // Element matches the elements we've seen so far, so we can keep the type + resultingClass + } else { + // Element does not match the elements we've seen so far, so we look for a common + // interface and continue with that + element.javaClass.interfaces.firstOrNull { it.isAssignableFrom(resultingClass) } + ?: throw SerializationException("Cannot find interface for serialization") + } + } + + return commonTypeInIterable +} diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/AxonSerializersTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/AxonSerializersTest.kt new file mode 100644 index 00000000..3ca88c5b --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/AxonSerializersTest.kt @@ -0,0 +1,165 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +import kotlinx.serialization.json.Json +import org.axonframework.eventhandling.GapAwareTrackingToken +import org.axonframework.eventhandling.GlobalSequenceTrackingToken +import org.axonframework.eventhandling.MergedTrackingToken +import org.axonframework.eventhandling.MultiSourceTrackingToken +import org.axonframework.eventhandling.ReplayToken +import org.axonframework.eventhandling.TrackingToken +import org.axonframework.eventhandling.scheduling.ScheduleToken +import org.axonframework.eventhandling.scheduling.java.SimpleScheduleToken +import org.axonframework.eventhandling.scheduling.quartz.QuartzScheduleToken +import org.axonframework.eventhandling.tokenstore.ConfigToken +import org.axonframework.extensions.kotlin.serialization.ArrayResponseType +import org.axonframework.extensions.kotlin.serialization.AxonSerializersModule +import org.axonframework.extensions.kotlin.serialization.KotlinSerializer +import org.axonframework.messaging.responsetypes.InstanceResponseType +import org.axonframework.messaging.responsetypes.MultipleInstancesResponseType +import org.axonframework.messaging.responsetypes.OptionalResponseType +import org.axonframework.messaging.responsetypes.ResponseType +import org.axonframework.serialization.Serializer +import org.axonframework.serialization.SimpleSerializedObject +import org.axonframework.serialization.SimpleSerializedType +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +internal class AxonSerializersTest { + + private val serializer = KotlinSerializer(Json { serializersModule = AxonSerializersModule }) + + @Test + fun configToken() { + val token = ConfigToken(mapOf("important-property" to "important-value", "other-property" to "other-value")) + val json = """{"config":{"important-property":"important-value","other-property":"other-value"}}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun gapAwareTrackingToken() { + val token = GapAwareTrackingToken(10, setOf(7, 8, 9)) + val json = """{"index":10,"gaps":[7,8,9]}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun multiSourceTrackingToken() { + val token = MultiSourceTrackingToken(mapOf("primary" to GlobalSequenceTrackingToken(5), "secondary" to GlobalSequenceTrackingToken(10))) + val json = """{"trackingTokens":{"primary":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"secondary":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10}}}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun mergedTrackingToken() { + val token = MergedTrackingToken(GlobalSequenceTrackingToken(5), GlobalSequenceTrackingToken(10)) + val json = """{"lowerSegmentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"upperSegmentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10}}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun replayToken() { + val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(15), GlobalSequenceTrackingToken(10)) + val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":15},"currentToken":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":10}}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun `replay token with currentToken with null value`() { + val token = ReplayToken.createReplayToken(GlobalSequenceTrackingToken(5), null) + val json = """{"tokenAtReset":{"type":"org.axonframework.eventhandling.GlobalSequenceTrackingToken","globalIndex":5},"currentToken":null}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun globalSequenceTrackingToken() { + val token = GlobalSequenceTrackingToken(5) + val json = """{"globalIndex":5}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeTrackingToken(token.javaClass.name, json)) + } + + @Test + fun simpleScheduleToken() { + val token = SimpleScheduleToken("my-token-id") + val json = """{"tokenId":"my-token-id"}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeScheduleToken(token.javaClass.name, json)) + } + + @Test + fun quartzScheduleToken() { + val token = QuartzScheduleToken("my-job-id", "my-group-id") + val json = """{"jobIdentifier":"my-job-id","groupIdentifier":"my-group-id"}""" + assertEquals(json, serializer.serialize(token, String::class.java).data) + assertEquals(token, serializer.deserializeScheduleToken(token.javaClass.name, json)) + } + + @Test + fun instanceResponseType() { + val responseType = InstanceResponseType(String::class.java) + val json = """{"expectedResponseType":"java.lang.String"}""" + assertEquals(json, serializer.serialize(responseType, String::class.java).data) + assertEquals(responseType, serializer.deserializeResponseType(responseType.javaClass.name, json)) + } + + @Test + fun optionalResponseType() { + val responseType = OptionalResponseType(String::class.java) + val json = """{"expectedResponseType":"java.lang.String"}""" + assertEquals(json, serializer.serialize(responseType, String::class.java).data) + assertEquals(responseType, serializer.deserializeResponseType(responseType.javaClass.name, json)) + } + + @Test + fun multipleInstancesResponseType() { + val responseType = MultipleInstancesResponseType(String::class.java) + val json = """{"expectedResponseType":"java.lang.String"}""" + assertEquals(json, serializer.serialize(responseType, String::class.java).data) + assertEquals(responseType, serializer.deserializeResponseType(responseType.javaClass.name, json)) + } + + @Test + fun arrayResponseType() { + val responseType = ArrayResponseType(String::class.java) + val json = """{"expectedResponseType":"java.lang.String"}""" + assertEquals(json, serializer.serialize(responseType, String::class.java).data) + assertEquals(responseType, serializer.deserializeResponseType(responseType.javaClass.name, json)) + } + + private fun Serializer.deserializeTrackingToken(tokenType: String, json: String): TrackingToken = + deserializeJson(tokenType, json) + + private fun Serializer.deserializeScheduleToken(tokenType: String, json: String): ScheduleToken = + deserializeJson(tokenType, json) + + private fun Serializer.deserializeResponseType(responseType: String, json: String): ResponseType<*> = + deserializeJson(responseType, json) + + private fun Serializer.deserializeJson(type: String, json: String): T { + val serializedType = SimpleSerializedType(type, null) + val serializedToken = SimpleSerializedObject(json, String::class.java, serializedType) + return deserialize(serializedToken) + } + +} diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerCborTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerCborTest.kt new file mode 100644 index 00000000..ca4fef3f --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerCborTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.cbor.Cbor +import org.axonframework.extensions.kotlin.serialization.KotlinSerializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KotlinSerializerCborTest { + @OptIn(ExperimentalSerializationApi::class) + private val sut = KotlinSerializer(Cbor) + + @Test + fun testStandardList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + ) + val actual = sut.serialize(items, ByteArray::class.java) + val expected = "9fbf646e616d65616163666f6f01ffbf646e616d65616263666f6f02ffff" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.TypeOne", actual.type.name) + assertEquals(expected, actual.data.toHex()) + } + + @Test + fun testPolymorphicList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + TypeTwo("c", listOf(3, 4)), + TypeOne("d", 5), + ) + val actual = sut.serialize(items, ByteArray::class.java) + val expected = "9f9f636f6e65bf646e616d65616163666f6f01ffff9f636f6e65bf646e616d65616263666f6f02ffff9f6374776fbf646e616d656163636261729f0304ffffff9f636f6e65bf646e616d65616463666f6f05ffffff" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.SuperType", actual.type.name) + assertEquals(expected, actual.data.toHex()) + } +} diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerJsonTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerJsonTest.kt new file mode 100644 index 00000000..7defa704 --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerJsonTest.kt @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +import kotlinx.serialization.json.Json +import org.axonframework.extensions.kotlin.serialization.KotlinSerializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KotlinSerializerJsonTest { + private val sut = KotlinSerializer(Json) + + @Test + fun testStandardList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + ) + val actual = sut.serialize(items, String::class.java) + val expected = """[{"name":"a","foo":1},{"name":"b","foo":2}]""" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.TypeOne", actual.type.name) + assertEquals(expected, actual.data) + } + + @Test + fun testPolymorphicList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + TypeTwo("c", listOf(3, 4)), + TypeOne("d", 5), + ) + val actual = sut.serialize(items, String::class.java) + val expected = """[{"type":"one","name":"a","foo":1},{"type":"one","name":"b","foo":2},{"type":"two","name":"c","bar":[3,4]},{"type":"one","name":"d","foo":5}]""" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.SuperType", actual.type.name) + assertEquals(expected, actual.data) + } +} diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerProtobufTest.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerProtobufTest.kt new file mode 100644 index 00000000..46fd1825 --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/KotlinSerializerProtobufTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.protobuf.ProtoBuf +import org.axonframework.extensions.kotlin.serialization.KotlinSerializer +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class KotlinSerializerProtobufTest { + @OptIn(ExperimentalSerializationApi::class) + private val sut = KotlinSerializer(ProtoBuf) + + @Test + fun testStandardList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + ) + val actual = sut.serialize(items, ByteArray::class.java) + val expected = "02050a01611001050a01621002" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.TypeOne", actual.type.name) + assertEquals(expected, actual.data.toHex()) + } + + @Test + fun testPolymorphicList() { + val items = listOf( + TypeOne("a", 1), + TypeOne("b", 2), + TypeTwo("c", listOf(3, 4)), + TypeOne("d", 5), + ) + val actual = sut.serialize(items, ByteArray::class.java) + val expected = "040c0a036f6e6512050a016110010c0a036f6e6512050a016210020e0a0374776f12070a0163100310040c0a036f6e6512050a01641005" + assertEquals("List:org.axonframework.extensions.kotlin.serializer.SuperType", actual.type.name) + assertEquals(expected, actual.data.toHex()) + } +} diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/hexExtensions.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/hexExtensions.kt new file mode 100644 index 00000000..fa91835e --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/hexExtensions.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +// copied from https://www.baeldung.com/kotlin/byte-arrays-to-hex-strings#1-formatter +fun ByteArray.toHex(): String = + joinToString("") { eachByte -> "%02x".format(eachByte) } \ No newline at end of file diff --git a/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/testTypes.kt b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/testTypes.kt new file mode 100644 index 00000000..fa909dca --- /dev/null +++ b/kotlin/src/test/kotlin/org/axonframework/extensions/kotlin/serializer/testTypes.kt @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2024. 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.serializer + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +sealed interface SuperType { + val name: String +} + +@Serializable +@SerialName("one") +data class TypeOne( + override val name: String, + val foo: Int, +) : SuperType + +@Serializable +@SerialName("two") +data class TypeTwo( + override val name: String, + val bar: List, +) : SuperType diff --git a/pom.xml b/pom.xml index 08468e60..ea999e14 100644 --- a/pom.xml +++ b/pom.xml @@ -55,18 +55,18 @@ 4.9.0 - 1.7.21 + 1.9.24 - 2.16.0 + 2.17.1 + 1.6.3 - 3.0.5 2.0.13 2.13.3 5.10.1 1.12.4 - 1.7.20 + 1.9.20 0.8.11 3.7.1 3.3.2 @@ -99,11 +99,6 @@ pom import - - io.github.microutils - kotlin-logging-jvm - ${kotlin-logging.version} - org.axonframework @@ -121,6 +116,11 @@ jackson-module-kotlin ${jackson-kotlin.version} + + org.jetbrains.kotlinx + kotlinx-serialization-core + ${kotlin-serialization.version} + org.junit.jupiter @@ -145,6 +145,24 @@ ${slf4j.version} test + + org.jetbrains.kotlinx + kotlinx-serialization-json + ${kotlin-serialization.version} + test + + + org.jetbrains.kotlinx + kotlinx-serialization-cbor + ${kotlin-serialization.version} + test + + + org.jetbrains.kotlinx + kotlinx-serialization-protobuf + ${kotlin-serialization.version} + test + @@ -163,6 +181,10 @@ com.fasterxml.jackson.module jackson-module-kotlin + + org.jetbrains.kotlinx + kotlinx-serialization-core + org.junit.jupiter @@ -184,6 +206,21 @@ slf4j-simple test + + org.jetbrains.kotlinx + kotlinx-serialization-json + test + + + org.jetbrains.kotlinx + kotlinx-serialization-cbor + test + + + org.jetbrains.kotlinx + kotlinx-serialization-protobuf + test + javax.xml.bind @@ -238,6 +275,7 @@ no-arg all-open + kotlinx-serialization @@ -281,6 +319,11 @@ kotlin-maven-noarg ${kotlin.version} + + org.jetbrains.kotlin + kotlin-maven-serialization + ${kotlin.version} +