Skip to content

Commit

Permalink
Merge pull request #131 from ProjectMapK/port-689
Browse files Browse the repository at this point in the history
Porting Kotlin Duration Support
  • Loading branch information
k163377 authored Aug 26, 2023
2 parents 7d9dbcd + 58a1276 commit deb88d1
Show file tree
Hide file tree
Showing 9 changed files with 296 additions and 12 deletions.
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
testImplementation("io.mockk:mockk:1.13.3")

testImplementation("com.fasterxml.jackson.dataformat:jackson-dataformat-xml")
testImplementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
}

kotlin {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ import java.lang.reflect.Constructor
import java.lang.reflect.Field
import java.lang.reflect.Method

internal typealias JavaDuration = java.time.Duration
internal typealias KotlinDuration = kotlin.time.Duration

internal fun Class<*>.isUnboxableValueClass() = this.getAnnotation(JvmInline::class.java) != null

internal fun Class<*>.toKmClass(): KmClass? = this.getAnnotation(Metadata::class.java)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,16 @@ public enum class KotlinFeature(internal val enabledByDefault: Boolean) {
*
* @see KotlinClassIntrospector
*/
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false);
CopySyntheticConstructorParameterAnnotations(enabledByDefault = false),

/**
* This feature represents whether to handle [kotlin.time.Duration] using [java.time.Duration] as conversion bridge.
*
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
* `@JsonFormat` annotations need to be declared either on getter using `@get:JsonFormat` or field using `@field:JsonFormat`.
* See [jackson-module-kotlin#651] for details.
*/
UseJavaDurationConversion(enabledByDefault = false);

internal val bitSet: BitSet = (1 shl ordinal).toBitSet()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyColl
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.NullToEmptyMap
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.SingletonSupport
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.StrictNullChecks
import io.github.projectmapk.jackson.module.kogera.KotlinFeature.UseJavaDurationConversion
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinFallbackAnnotationIntrospector
import io.github.projectmapk.jackson.module.kogera.annotation_introspector.KotlinPrimaryAnnotationIntrospector
import io.github.projectmapk.jackson.module.kogera.deser.deserializers.KotlinDeserializers
Expand All @@ -32,6 +33,8 @@ import java.util.*
* the default, collections which are typed to disallow null members
* (e.g. List<String>) may contain null values after deserialization. Enabling it
* protects against this but has significant performance impact.
* @param useJavaDurationConversion Default: false. Whether to use [java.time.Duration] as a bridge for [kotlin.time.Duration].
* This allows use Kotlin Duration type with [com.fasterxml.jackson.datatype.jsr310.JavaTimeModule].
*/
// Do not delete default arguments,
// as this will cause an error during initialization by Spring's Jackson2ObjectMapperBuilder.
Expand All @@ -43,7 +46,8 @@ public class KotlinModule private constructor(
public val singletonSupport: Boolean = SingletonSupport.enabledByDefault,
public val strictNullChecks: Boolean = StrictNullChecks.enabledByDefault,
public val copySyntheticConstructorParameterAnnotations: Boolean =
CopySyntheticConstructorParameterAnnotations.enabledByDefault
CopySyntheticConstructorParameterAnnotations.enabledByDefault,
public val useJavaDurationConversion: Boolean = UseJavaDurationConversion.enabledByDefault
) : SimpleModule(KotlinModule::class.java.name, kogeraVersion) { // kogeraVersion is generated by building.
private constructor(builder: Builder) : this(
builder.reflectionCacheSize,
Expand All @@ -52,7 +56,8 @@ public class KotlinModule private constructor(
builder.isEnabled(NullIsSameAsDefault),
builder.isEnabled(SingletonSupport),
builder.isEnabled(StrictNullChecks),
builder.isEnabled(CopySyntheticConstructorParameterAnnotations)
builder.isEnabled(CopySyntheticConstructorParameterAnnotations),
builder.isEnabled(UseJavaDurationConversion)
)

@Deprecated(
Expand Down Expand Up @@ -87,13 +92,15 @@ public class KotlinModule private constructor(
context.insertAnnotationIntrospector(
KotlinPrimaryAnnotationIntrospector(nullToEmptyCollection, nullToEmptyMap, cache)
)
context.appendAnnotationIntrospector(KotlinFallbackAnnotationIntrospector(strictNullChecks, cache))
context.appendAnnotationIntrospector(
KotlinFallbackAnnotationIntrospector(strictNullChecks, useJavaDurationConversion, cache)
)

if (copySyntheticConstructorParameterAnnotations) {
context.setClassIntrospector(KotlinClassIntrospector)
}

context.addDeserializers(KotlinDeserializers(cache))
context.addDeserializers(KotlinDeserializers(cache, useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,16 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.CollectionValueStrictNullChecksConverter
import io.github.projectmapk.jackson.module.kogera.deser.MapValueStrictNullChecksConverter
import io.github.projectmapk.jackson.module.kogera.deser.ValueClassUnboxConverter
import io.github.projectmapk.jackson.module.kogera.isNullable
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull
import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter
import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter
import io.github.projectmapk.jackson.module.kogera.ser.SequenceToIteratorConverter
import kotlinx.metadata.KmTypeProjection
import kotlinx.metadata.KmValueParameter
Expand All @@ -31,6 +34,7 @@ import java.lang.reflect.Modifier
// Original name: KotlinNamesAnnotationIntrospector
internal class KotlinFallbackAnnotationIntrospector(
private val strictNullChecks: Boolean,
private val useJavaDurationConversion: Boolean,
private val cache: ReflectionCache
) : NopAnnotationIntrospector() {
private fun findKotlinParameter(param: AnnotatedParameter): KmValueParameter? =
Expand Down Expand Up @@ -73,12 +77,24 @@ internal class KotlinFallbackAnnotationIntrospector(

override fun findSerializationConverter(a: Annotated): Converter<*, *>? = when (a) {
// Find a converter to handle the case where the getter returns an unboxed value from the value class.
is AnnotatedMethod -> cache.findValueClassReturnType(a)
?.let { cache.getValueClassBoxConverter(a.rawReturnType, it) }
is AnnotatedClass ->
a
.takeIf { Sequence::class.java.isAssignableFrom(it.rawType) }
?.let { SequenceToIteratorConverter(it.type) }
is AnnotatedMethod -> cache.findValueClassReturnType(a)?.let {
if (useJavaDurationConversion && it == KotlinDuration::class.java) {
if (a.rawReturnType == KotlinDuration::class.java) {
KotlinToJavaDurationConverter
} else {
KotlinDurationValueToJavaDurationConverter
}
} else {
cache.getValueClassBoxConverter(a.rawReturnType, it)
}
}
is AnnotatedClass -> lookupKotlinTypeConverter(a)
else -> null
}

private fun lookupKotlinTypeConverter(a: AnnotatedClass) = when {
Sequence::class.java.isAssignableFrom(a.rawType) -> SequenceToIteratorConverter(a.type)
KotlinDuration::class.java == a.rawType -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
else -> null
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
package io.github.projectmapk.jackson.module.kogera.deser

import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.deser.std.StdDelegatingDeserializer
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.Converter
import com.fasterxml.jackson.databind.util.StdConverter
import io.github.projectmapk.jackson.module.kogera.JavaDuration
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import kotlin.time.toKotlinDuration

internal class ValueClassUnboxConverter<T : Any>(private val valueClass: Class<T>) : StdConverter<T, Any?>() {
private val unboxMethod = valueClass.getDeclaredMethod("unbox-impl").apply {
Expand Down Expand Up @@ -73,3 +77,16 @@ internal class MapValueStrictNullChecksConverter(
override fun getInputType(typeFactory: TypeFactory): JavaType = type
override fun getOutputType(typeFactory: TypeFactory): JavaType = type
}

/**
* Currently it is not possible to deduce type of [kotlin.time.Duration] fields therefore explicit annotation is needed on fields in order to properly deserialize POJO.
*
* @see [com.fasterxml.jackson.module.kotlin.test.DurationTests]
*/
internal object JavaToKotlinDurationConverter : StdConverter<JavaDuration, KotlinDuration>() {
override fun convert(value: JavaDuration) = value.toKotlinDuration()

val delegatingDeserializer: StdDelegatingDeserializer<KotlinDuration> by lazy {
StdDelegatingDeserializer(this)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import com.fasterxml.jackson.databind.deser.Deserializers
import com.fasterxml.jackson.databind.deser.std.StdDeserializer
import com.fasterxml.jackson.databind.exc.InvalidDefinitionException
import io.github.projectmapk.jackson.module.kogera.JmClass
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.deser.JavaToKotlinDurationConverter
import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
import io.github.projectmapk.jackson.module.kogera.toSignature
Expand Down Expand Up @@ -127,7 +129,10 @@ private fun findValueCreator(type: JavaType, clazz: Class<*>, jmClass: JmClass):
return primaryConstructor
}

internal class KotlinDeserializers(private val cache: ReflectionCache) : Deserializers.Base() {
internal class KotlinDeserializers(
private val cache: ReflectionCache,
private val useJavaDurationConversion: Boolean
) : Deserializers.Base() {
override fun findBeanDeserializer(
type: JavaType,
config: DeserializationConfig?,
Expand All @@ -142,6 +147,8 @@ internal class KotlinDeserializers(private val cache: ReflectionCache) : Deseria
rawClass == UShort::class.java -> UShortDeserializer
rawClass == UInt::class.java -> UIntDeserializer
rawClass == ULong::class.java -> ULongDeserializer
rawClass == KotlinDuration::class.java ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
rawClass.isUnboxableValueClass() -> findValueCreator(type, rawClass, cache.getJmClass(rawClass)!!)
?.let { ValueClassBoxDeserializer(it, rawClass) }
else -> null
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.ser.std.StdDelegatingSerializer
import com.fasterxml.jackson.databind.type.TypeFactory
import com.fasterxml.jackson.databind.util.StdConverter
import io.github.projectmapk.jackson.module.kogera.JavaDuration
import io.github.projectmapk.jackson.module.kogera.KotlinDuration
import kotlin.time.toJavaDuration

internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter<Sequence<*>, Iterator<*>>() {
override fun convert(value: Sequence<*>): Iterator<*> = value.iterator()
Expand Down Expand Up @@ -31,3 +34,13 @@ internal class ValueClassBoxConverter<S : Any?, D : Any>(

val delegatingSerializer: StdDelegatingSerializer by lazy { StdDelegatingSerializer(this) }
}

internal object KotlinDurationValueToJavaDurationConverter : StdConverter<Long, JavaDuration>() {
private val boxConverter by lazy { ValueClassBoxConverter(Long::class.java, KotlinDuration::class.java) }

override fun convert(value: Long): JavaDuration = KotlinToJavaDurationConverter.convert(boxConverter.convert(value))
}

internal object KotlinToJavaDurationConverter : StdConverter<KotlinDuration, JavaDuration>() {
override fun convert(value: KotlinDuration) = value.toJavaDuration()
}
Loading

0 comments on commit deb88d1

Please sign in to comment.