Skip to content

Commit

Permalink
Merge remote-tracking branch 'FasterXML/2.16'
Browse files Browse the repository at this point in the history
  • Loading branch information
k163377 committed Aug 6, 2023
2 parents aacb2b8 + 9730a1b commit c58cc3e
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 29 deletions.
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@
<version>${version.kotlin}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${version.kotlin}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
Expand All @@ -116,6 +122,12 @@
<artifactId>jackson-dataformat-xml</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<!-- needed for kotlin.time.Duration converter test -->
<groupId>tools.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
3 changes: 3 additions & 0 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ Contributors:

# 2.16.0 (not yet released)

kkurczewski
* #689: Add KotlinDuration support

WrongWrong (@k163377)
* #687: Optimize and Refactor KotlinValueInstantiator.createFromObjectWith
* #686: Add KotlinPropertyNameAsImplicitName option
Expand Down
2 changes: 2 additions & 0 deletions release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ Co-maintainers:

2.16.0 (not yet released)

#689: Added UseJavaDurationConversion feature.
By enabling this feature and adding the Java Time module, Kotlin Duration can be handled in the same way as Java Duration.
#687: Optimize and Refactor KotlinValueInstantiator.createFromObjectWith.
This improves deserialization throughput about 1.3 ~ 1.5 times faster.
https://github.com/FasterXML/jackson-module-kotlin/pull/687#issuecomment-1637365799
Expand Down
28 changes: 28 additions & 0 deletions src/main/kotlin/tools/jackson/module/kotlin/Converters.kt
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
package tools.jackson.module.kotlin

import tools.jackson.databind.JavaType
import tools.jackson.databind.deser.std.StdConvertingDeserializer
import tools.jackson.databind.ser.std.StdDelegatingSerializer
import tools.jackson.databind.type.TypeFactory
import tools.jackson.databind.util.StdConverter
import kotlin.reflect.KClass
import kotlin.time.toJavaDuration
import kotlin.time.toKotlinDuration
import java.time.Duration as JavaDuration
import kotlin.time.Duration as KotlinDuration

internal class SequenceToIteratorConverter(private val input: JavaType) : StdConverter<Sequence<*>, Iterator<*>>() {
override fun convert(value: Sequence<*>): Iterator<*> = value.iterator()
Expand All @@ -16,6 +21,29 @@ internal class SequenceToIteratorConverter(private val input: JavaType) : StdCon
?: typeFactory.constructType(Iterator::class.java)
}

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

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

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

/**
* 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: StdConvertingDeserializer<KotlinDuration> by lazy {
StdConvertingDeserializer(this)
}
}

// S is nullable because value corresponds to a nullable value class
// @see KotlinNamesAnnotationIntrospector.findNullSerializer
internal class ValueClassBoxConverter<S : Any?, D : Any>(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,23 @@ import kotlin.reflect.KType
import kotlin.reflect.full.createType
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.memberProperties
import kotlin.reflect.full.valueParameters
import kotlin.reflect.jvm.javaField
import kotlin.reflect.jvm.javaGetter
import kotlin.reflect.jvm.javaSetter
import kotlin.reflect.jvm.javaType
import kotlin.reflect.jvm.kotlinFunction
import kotlin.reflect.jvm.kotlinProperty
import kotlin.time.Duration

internal class KotlinAnnotationIntrospector(private val context: JacksonModule.SetupContext,
private val cache: ReflectionCache,
private val nullToEmptyCollection: Boolean,
private val nullToEmptyMap: Boolean,
private val nullIsSameAsDefault: Boolean) : NopAnnotationIntrospector() {
internal class KotlinAnnotationIntrospector(
private val context: JacksonModule.SetupContext,
private val cache: ReflectionCache,
private val nullToEmptyCollection: Boolean,
private val nullToEmptyMap: Boolean,
private val nullIsSameAsDefault: Boolean,
private val useJavaDurationConversion: Boolean,
) : NopAnnotationIntrospector() {

// TODO: implement nullIsSameAsDefault flag, which represents when TRUE that if something has a default value, it can be passed a null to default it
// this likely impacts this class to be accurate about what COULD be considered required
Expand Down Expand Up @@ -73,11 +78,23 @@ internal class KotlinAnnotationIntrospector(private val context: JacksonModule.S

override fun findSerializationConverter(config: MapperConfig<*>?, 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 -> a.findValueClassReturnType()?.let {
if (useJavaDurationConversion && it == Duration::class) {
if (a.rawReturnType == Duration::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)
Duration::class.java == a.rawType -> KotlinToJavaDurationConverter.takeIf { useJavaDurationConversion }
else -> null
}

Expand All @@ -88,10 +105,29 @@ internal class KotlinAnnotationIntrospector(private val context: JacksonModule.S

// Perform proper serialization even if the value wrapped by the value class is null.
// If value is a non-null object type, it must not be reboxing.
override fun findNullSerializer(config: MapperConfig<*>?, am: Annotated) = (am as? AnnotatedMethod)?.let { _ ->
cache.findValueClassReturnType(am)
?.takeIf { it.requireRebox() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }
override fun findNullSerializer(config: MapperConfig<*>?, am: Annotated) = (am as? AnnotatedMethod)
?.findValueClassReturnType()
?.takeIf { it.requireRebox() }
?.let { cache.getValueClassBoxConverter(am.rawReturnType, it).delegatingSerializer }

override fun findDeserializationConverter(config: MapperConfig<*>, a: Annotated): Any? {
if (!useJavaDurationConversion) return null

return (a as? AnnotatedParameter)?.let { param ->
@Suppress("UNCHECKED_CAST")
val function: KFunction<*> = when (val owner = param.owner.member) {
is Constructor<*> -> cache.kotlinFromJava(owner as Constructor<Any>)
is Method -> cache.kotlinFromJava(owner)
else -> null
} ?: return@let null
val valueParameter = function.valueParameters[a.index]

if (valueParameter.type.classifier == Duration::class) {
JavaToKotlinDurationConverter
} else {
null
}
}
}

/**
Expand All @@ -111,7 +147,7 @@ internal class KotlinAnnotationIntrospector(private val context: JacksonModule.S

private fun AnnotatedField.hasRequiredMarker(): Boolean? {
val byAnnotation = (member as Field).isRequiredByAnnotation()
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()
val byNullability = (member as Field).kotlinProperty?.returnType?.isRequired()

return requiredAnnotationOrNullability(byAnnotation, byNullability)
}
Expand All @@ -131,7 +167,7 @@ internal class KotlinAnnotationIntrospector(private val context: JacksonModule.S
}

private fun Method.isRequiredByAnnotation(): Boolean? {
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
return (this.annotations.firstOrNull { it.annotationClass.java == JsonProperty::class.java } as? JsonProperty)?.required
}

// Since Kotlin's property has the same Type for each field, getter, and setter,
Expand Down Expand Up @@ -180,6 +216,8 @@ internal class KotlinAnnotationIntrospector(private val context: JacksonModule.S
return requiredAnnotationOrNullability(byAnnotation, byNullability)
}

private fun AnnotatedMethod.findValueClassReturnType() = cache.findValueClassReturnType(this)

private fun KFunction<*>.isConstructorParameterRequired(index: Int): Boolean {
return isParameterRequired(index)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import tools.jackson.databind.JavaType
import tools.jackson.databind.ValueDeserializer
import tools.jackson.databind.deser.Deserializers
import tools.jackson.databind.deser.std.StdDeserializer

import kotlin.time.Duration as KotlinDuration

object SequenceDeserializer : StdDeserializer<Sequence<*>>(Sequence::class.java) {
override fun deserialize(p: JsonParser, ctxt: DeserializationContext): Sequence<*> {
Expand Down Expand Up @@ -92,7 +92,9 @@ object ULongDeserializer : StdDeserializer<ULong>(ULong::class.java) {
)
}

internal class KotlinDeserializers : Deserializers.Base() {
internal class KotlinDeserializers(
private val useJavaDurationConversion: Boolean,
) : Deserializers.Base() {
override fun findBeanDeserializer(
type: JavaType,
config: DeserializationConfig?,
Expand All @@ -105,17 +107,19 @@ internal class KotlinDeserializers : Deserializers.Base() {
type.rawClass == UShort::class.java -> UShortDeserializer
type.rawClass == UInt::class.java -> UIntDeserializer
type.rawClass == ULong::class.java -> ULongDeserializer
type.rawClass == KotlinDuration::class.java ->
JavaToKotlinDurationConverter.takeIf { useJavaDurationConversion }?.delegatingDeserializer
else -> null
}
}

override fun hasDeserializerFor(config: DeserializationConfig,
valueType: Class<*>): Boolean {
override fun hasDeserializerFor(config: DeserializationConfig, valueType: Class<*>): Boolean {
return valueType == Sequence::class.java
|| valueType == Regex::class.java
|| valueType == UByte::class.java
|| valueType == UShort::class.java
|| valueType == UInt::class.java
|| valueType == ULong::class.java
|| valueType == KotlinDuration::class.java
}
}
11 changes: 10 additions & 1 deletion src/main/kotlin/tools/jackson/module/kotlin/KotlinFeature.kt
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,16 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) {
* In addition, the adjustment of behavior using get:JvmName is disabled.
* Note also that this feature does not apply to setters.
*/
KotlinPropertyNameAsImplicitName(enabledByDefault = false);
KotlinPropertyNameAsImplicitName(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
24 changes: 16 additions & 8 deletions src/main/kotlin/tools/jackson/module/kotlin/KotlinModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ package tools.jackson.module.kotlin
import kotlin.reflect.KClass
import tools.jackson.databind.MapperFeature
import tools.jackson.databind.module.SimpleModule
import tools.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault
import tools.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection
import tools.jackson.module.kotlin.KotlinFeature.NullToEmptyMap
import tools.jackson.module.kotlin.KotlinFeature.StrictNullChecks
import tools.jackson.module.kotlin.KotlinFeature.*
import tools.jackson.module.kotlin.SingletonSupport.CANONICALIZE
import tools.jackson.module.kotlin.SingletonSupport.DISABLED
import java.util.*
Expand All @@ -31,6 +28,8 @@ fun Class<*>.isKotlinClass(): Boolean {
* 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].
*/
class KotlinModule @Deprecated(
level = DeprecationLevel.WARNING,
Expand All @@ -53,7 +52,8 @@ class KotlinModule @Deprecated(
val nullIsSameAsDefault: Boolean = false,
val singletonSupport: SingletonSupport = DISABLED,
val strictNullChecks: Boolean = false,
val useKotlinPropertyNameForGetter: Boolean = false
val useKotlinPropertyNameForGetter: Boolean = false,
val useJavaDurationConversion: Boolean = false,
) : SimpleModule(KotlinModule::class.java.name, PackageVersion.VERSION) {
init {
if (!KotlinVersion.CURRENT.isAtLeast(1, 5)) {
Expand Down Expand Up @@ -102,7 +102,8 @@ class KotlinModule @Deprecated(
else -> DISABLED
},
builder.isEnabled(StrictNullChecks),
builder.isEnabled(KotlinFeature.KotlinPropertyNameAsImplicitName)
builder.isEnabled(KotlinPropertyNameAsImplicitName),
builder.isEnabled(UseJavaDurationConversion),
)

companion object {
Expand Down Expand Up @@ -130,7 +131,14 @@ class KotlinModule @Deprecated(
}
}

context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(context, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault))
context.insertAnnotationIntrospector(KotlinAnnotationIntrospector(
context,
cache,
nullToEmptyCollection,
nullToEmptyMap,
nullIsSameAsDefault,
useJavaDurationConversion
))
context.appendAnnotationIntrospector(
KotlinNamesAnnotationIntrospector(
this,
Expand All @@ -139,7 +147,7 @@ class KotlinModule @Deprecated(
useKotlinPropertyNameForGetter)
)

context.addDeserializers(KotlinDeserializers())
context.addDeserializers(KotlinDeserializers(useJavaDurationConversion))
context.addKeyDeserializers(KotlinKeyDeserializers)
context.addSerializers(KotlinSerializers())
context.addKeySerializers(KotlinKeySerializers())
Expand Down
Loading

0 comments on commit c58cc3e

Please sign in to comment.