From 1b07300354bb259a9bae5d388f5ea9c8193c4351 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:49:19 +0900 Subject: [PATCH 01/13] Add configurations to use Java --- pom.xml | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 5a163502..c7b6ddf1 100644 --- a/pom.xml +++ b/pom.xml @@ -114,7 +114,6 @@ - ${project.basedir}/src/main/kotlin ${project.basedir}/src/test/kotlin @@ -129,6 +128,13 @@ compile + + + ${project.basedir}/target/generated-sources + ${project.basedir}/src/main/java + ${project.basedir}/src/main/kotlin + + @@ -145,10 +151,12 @@ + org.apache.maven.plugins maven-surefire-plugin + com.google.code.maven-replacer-plugin @@ -160,6 +168,7 @@ + org.apache.maven.plugins maven-compiler-plugin @@ -168,6 +177,32 @@ true true + + + + default-compile + none + + + + default-testCompile + none + + + java-compile + compile + + compile + + + + java-test-compile + test-compile + + testCompile + + + From 1ab295404f7cfb86ef31dd787cbee16c4fffa9a6 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:52:01 +0900 Subject: [PATCH 02/13] add SpreadWrapper --- .../jackson/module/kotlin/SpreadWrapper.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/com/fasterxml/jackson/module/kotlin/SpreadWrapper.java diff --git a/src/main/java/com/fasterxml/jackson/module/kotlin/SpreadWrapper.java b/src/main/java/com/fasterxml/jackson/module/kotlin/SpreadWrapper.java new file mode 100644 index 00000000..5acea0fa --- /dev/null +++ b/src/main/java/com/fasterxml/jackson/module/kotlin/SpreadWrapper.java @@ -0,0 +1,47 @@ +package com.fasterxml.jackson.module.kotlin; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +/** + * Wrapper to avoid costly calls using spread operator. + * @since 2.13 + */ +class SpreadWrapper { + public static Constructor getConstructor( + @NotNull Class clazz, + @NotNull Class[] parameterTypes + ) throws NoSuchMethodException { + return clazz.getConstructor(parameterTypes); + } + + public static T newInstance( + @NotNull Constructor constructor, + @NotNull Object[] initargs + ) throws InvocationTargetException, InstantiationException, IllegalAccessException { + return constructor.newInstance(initargs); + } + + public static Method getDeclaredMethod( + @NotNull Class clazz, + @NotNull String name, + @NotNull Class[] parameterTypes + ) throws NoSuchMethodException { + return clazz.getDeclaredMethod(name, parameterTypes); + } + + /** + * Instance is null on static method + */ + public static Object invoke( + @NotNull Method method, + @Nullable Object instance, + @NotNull Object[] args + ) throws InvocationTargetException, IllegalAccessException { + return method.invoke(instance, args); + } +} From 325eaa42e1e0e96899751ad2da341607d72ec1b6 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:54:17 +0900 Subject: [PATCH 03/13] add ArgumentBucket and Generator --- .../jackson/module/kotlin/ArgumentBucket.kt | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt new file mode 100644 index 00000000..75614447 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt @@ -0,0 +1,86 @@ +package com.fasterxml.jackson.module.kotlin + +import kotlin.reflect.KParameter + +internal class BucketGenerator(parameters: List) { + private val paramSize: Int = parameters.size + val maskSize = (paramSize / Int.SIZE_BITS) + 1 + // For Optional and Primitive types, set the initial value because the function cannot be called if the argument is null. + private val originalValues: Array = Array(paramSize) { + val param = parameters[it] + + if (param.isOptional) { + ABSENT_VALUE[param.type.erasedType()] + } else { + null + } + } + private val originalMasks: IntArray = IntArray(maskSize) { FILLED_MASK } + + fun generate() = ArgumentBucket(paramSize, originalValues.clone(), originalMasks.clone()) + + companion object { + private const val FILLED_MASK = -1 + + private val ABSENT_VALUE: Map, Any> = mapOf( + Boolean::class.javaPrimitiveType!! to false, + Char::class.javaPrimitiveType!! to Char.MIN_VALUE, + Byte::class.javaPrimitiveType!! to Byte.MIN_VALUE, + Short::class.javaPrimitiveType!! to Short.MIN_VALUE, + Int::class.javaPrimitiveType!! to Int.MIN_VALUE, + Long::class.javaPrimitiveType!! to Long.MIN_VALUE, + Float::class.javaPrimitiveType!! to Float.MIN_VALUE, + Double::class.javaPrimitiveType!! to Double.MIN_VALUE + ) + } +} + +/** + * Class for managing arguments and their initialization state. + * [masks] is used to manage the initialization state of arguments, and is also a mask to indicate whether to use default arguments in Kotlin. + * For the [masks] bit, 0 means initialized and 1 means uninitialized. + * + * @property values Arguments arranged in order in the manner of a bucket sort. + */ +internal class ArgumentBucket( + private val paramSize: Int, + val values: Array, + private val masks: IntArray +) { + private var initializedCount: Int = 0 + + private fun getMaskAddress(index: Int): Pair = (index / Int.SIZE_BITS) to (index % Int.SIZE_BITS) + + /** + * Set the argument. The second and subsequent inputs for the same `index` will be ignored. + */ + operator fun set(index: Int, value: Any?) { + val maskAddress = getMaskAddress(index) + + val updatedMask = masks[maskAddress.first] and BIT_FLAGS[maskAddress.second] + + if (updatedMask != masks[maskAddress.first]) { + values[index] = value + masks[maskAddress.first] = updatedMask + initializedCount++ + } + } + + fun isFullInitialized(): Boolean = initializedCount == paramSize + + /** + * An array of values to be used when making calls with default arguments. + * The null at the end is a marker for synthetic method. + * @return arrayOf(*values, *masks, null) + */ + fun getValuesOnDefault(): Array = values.copyOf(values.size + masks.size + 1).apply { + masks.forEachIndexed { i, mask -> + this[values.size + i] = mask + } + } + + companion object { + // List of Int with only 1 bit enabled. + private val BIT_FLAGS: List = IntArray(Int.SIZE_BITS) { (1 shl it).inv() }.asList() + } +} From 98785c234df0c6bd4fed963e513d70823f8d91e7 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:56:02 +0900 Subject: [PATCH 04/13] add Instantiator interface --- .../jackson/module/kotlin/Instantiator.kt | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/kotlin/com/fasterxml/jackson/module/kotlin/Instantiator.kt diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Instantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Instantiator.kt new file mode 100644 index 00000000..9636f887 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/Instantiator.kt @@ -0,0 +1,34 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.databind.DeserializationContext +import kotlin.reflect.KParameter + +internal interface Instantiator { + val hasInstanceParameter: Boolean + + /** + * ValueParameters of the KFunction to be called. + */ + val valueParameters: List + + /** + * Checking process to see if access from context is possible. + * @throws IllegalAccessException + */ + fun checkAccessibility(ctxt: DeserializationContext) + + /** + * The process of getting the target bucket to set the value. + */ + fun generateBucket(): ArgumentBucket + + /** + * Function call from bucket. + * If there are uninitialized arguments, the call is made using the default function. + */ + fun callBy(bucket: ArgumentBucket): T + + companion object { + val INT_PRIMITIVE_CLASS: Class = Int::class.javaPrimitiveType!! + } +} From 41e7397cd2961d50802b6d959497dd86549fd4f2 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:57:02 +0900 Subject: [PATCH 05/13] add ConstructorInstantiator --- .../module/kotlin/ConstructorInstantiator.kt | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt new file mode 100644 index 00000000..5963e016 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt @@ -0,0 +1,61 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.module.kotlin.Instantiator.Companion.INT_PRIMITIVE_CLASS +import java.lang.reflect.Constructor +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter + +// This class does not support inner constructor. +internal class ConstructorInstantiator( + kConstructor: KFunction, private val constructor: Constructor +) : Instantiator { + // Top level constructor does not require any instance parameters. + override val hasInstanceParameter: Boolean = false + override val valueParameters: List = kConstructor.parameters + private val accessible: Boolean = constructor.isAccessible + private val bucketGenerator = BucketGenerator(valueParameters) + // This initialization process is heavy and will not be done until it is needed. + private val localConstructor: Constructor by lazy { + val parameterTypes = arrayOf( + *constructor.parameterTypes, + *Array(bucketGenerator.maskSize) { INT_PRIMITIVE_CLASS }, + DEFAULT_CONSTRUCTOR_MARKER + ) + + SpreadWrapper.getConstructor(constructor.declaringClass, parameterTypes) + .apply { isAccessible = true } + } + + init { + // Preserve the initial value of Accessibility, and make the entity Accessible. + constructor.isAccessible = true + } + + override fun checkAccessibility(ctxt: DeserializationContext) { + if ((!accessible && ctxt.config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) || + (accessible && ctxt.config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) { + return + } + + throw IllegalAccessException("Cannot access to Constructor, instead found ${constructor.declaringClass.name}") + } + + override fun generateBucket() = bucketGenerator.generate() + + override fun callBy(bucket: ArgumentBucket): T = when (bucket.isFullInitialized()) { + true -> SpreadWrapper.newInstance(constructor, bucket.values) + false -> SpreadWrapper.newInstance(localConstructor, bucket.getValuesOnDefault()) + } + + companion object { + private val DEFAULT_CONSTRUCTOR_MARKER: Class<*> = try { + Class.forName("kotlin.jvm.internal.DefaultConstructorMarker") + } catch (ex: ClassNotFoundException) { + throw IllegalStateException( + "DefaultConstructorMarker not on classpath. Make sure the Kotlin stdlib is on the classpath." + ) + } + } +} From 194d1f3af0f1673ee5c7f7399f8569fefc7a3310 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 13:58:24 +0900 Subject: [PATCH 06/13] add MethodInstantiator --- .../module/kotlin/MethodInstantiator.kt | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 src/main/kotlin/com/fasterxml/jackson/module/kotlin/MethodInstantiator.kt diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/MethodInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/MethodInstantiator.kt new file mode 100644 index 00000000..d33d36f6 --- /dev/null +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/MethodInstantiator.kt @@ -0,0 +1,74 @@ +package com.fasterxml.jackson.module.kotlin + +import com.fasterxml.jackson.databind.DeserializationContext +import com.fasterxml.jackson.databind.MapperFeature +import com.fasterxml.jackson.module.kotlin.Instantiator.Companion.INT_PRIMITIVE_CLASS +import java.lang.reflect.Method +import kotlin.reflect.KFunction +import kotlin.reflect.KParameter +import kotlin.reflect.full.valueParameters + +internal class MethodInstantiator( + kFunction: KFunction, + private val method: Method, + private val instance: Any, + companionAccessible: Boolean +) : Instantiator { + override val hasInstanceParameter: Boolean = true + override val valueParameters: List = kFunction.valueParameters + private val accessible: Boolean = companionAccessible && method.isAccessible + private val bucketGenerator = BucketGenerator(valueParameters) + + init { + method.isAccessible = true + } + + // This initialization process is heavy and will not be done until it is needed. + private val localMethod: Method by lazy { + val instanceClazz = instance::class.java + + val argumentTypes = arrayOf( + instanceClazz, + *method.parameterTypes, + *Array(bucketGenerator.maskSize) { INT_PRIMITIVE_CLASS }, + Object::class.java + ) + + SpreadWrapper.getDeclaredMethod(instanceClazz, "${method.name}\$default", argumentTypes) + .apply { isAccessible = true } + } + private val originalDefaultValues: Array by lazy { + // argument size = parameterSize + maskSize + instanceSize(= 1) + markerSize(= 1) + Array(valueParameters.size + bucketGenerator.maskSize + 2) { null }.apply { + this[0] = instance + } + } + + override fun checkAccessibility(ctxt: DeserializationContext) { + if ((!accessible && ctxt.config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) || + (accessible && ctxt.config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS))) { + return + } + + throw IllegalAccessException("Cannot access to Method, instead found ${method.name}") + } + + override fun generateBucket() = bucketGenerator.generate() + + @Suppress("UNCHECKED_CAST") + override fun callBy(bucket: ArgumentBucket) = when (bucket.isFullInitialized()) { + true -> SpreadWrapper.invoke(method, instance, bucket.values) + false -> { + // When calling a method defined in companion object with default arguments, + // the arguments are in the order of [instance, *args, *masks, null]. + // Since ArgumentBucket.getValuesOnDefault returns [*args, *masks, null], + // it should be repacked into an array including instance. + val values = originalDefaultValues.clone().apply { + bucket.getValuesOnDefault().forEachIndexed { index, value -> + this[index + 1] = value + } + } + SpreadWrapper.invoke(localMethod, null, values) + } + } as T +} From 5845e0bd155b74117a167922e6a1bf7819452603 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 14:02:56 +0900 Subject: [PATCH 07/13] add Instantiator fetch and cache logic to ReflectionCache. --- .../jackson/module/kotlin/ReflectionCache.kt | 61 ++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt index 355a7e95..bd5a0f9f 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ReflectionCache.kt @@ -3,14 +3,16 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor import com.fasterxml.jackson.databind.introspect.AnnotatedMember import com.fasterxml.jackson.databind.introspect.AnnotatedMethod +import com.fasterxml.jackson.databind.introspect.AnnotatedWithParams import com.fasterxml.jackson.databind.util.LRUMap import java.lang.reflect.Constructor import java.lang.reflect.Method import kotlin.reflect.KClass import kotlin.reflect.KFunction +import kotlin.reflect.full.extensionReceiverParameter +import kotlin.reflect.full.instanceParameter import kotlin.reflect.jvm.kotlinFunction - internal class ReflectionCache(reflectionCacheSize: Int) { sealed class BooleanTriState(val value: Boolean?) { class True : BooleanTriState(true) @@ -38,7 +40,8 @@ internal class ReflectionCache(reflectionCacheSize: Int) { private val javaConstructorIsCreatorAnnotated = LRUMap(reflectionCacheSize, reflectionCacheSize) private val javaMemberIsRequired = LRUMap(reflectionCacheSize, reflectionCacheSize) private val kotlinGeneratedMethod = LRUMap(reflectionCacheSize, reflectionCacheSize) - + private val javaConstructorToInstantiator = LRUMap, ConstructorInstantiator>(reflectionCacheSize, reflectionCacheSize) + private val javaMethodToInstantiator = LRUMap>(reflectionCacheSize, reflectionCacheSize) fun kotlinFromJava(key: Class): KClass = javaClassToKotlin.get(key) ?: key.kotlin.let { javaClassToKotlin.putIfAbsent(key, it) ?: it } @@ -57,4 +60,58 @@ internal class ReflectionCache(reflectionCacheSize: Int) { fun isKotlinGeneratedMethod(key: AnnotatedMethod, calc: (AnnotatedMethod) -> Boolean): Boolean = kotlinGeneratedMethod.get(key) ?: calc(key).let { kotlinGeneratedMethod.putIfAbsent(key, it) ?: it } + + private fun instantiatorFromJavaConstructor(key: Constructor): ConstructorInstantiator<*>? = javaConstructorToInstantiator.get(key) + ?: kotlinFromJava(key)?.let { + val instantiator = ConstructorInstantiator(it, key) + javaConstructorToInstantiator.putIfAbsent(key, instantiator) ?: instantiator + } + + private fun instantiatorFromJavaMethod(key: Method): MethodInstantiator<*>? = javaMethodToInstantiator.get(key) + ?: kotlinFromJava(key)?.takeIf { + // we shouldn't have an instance or receiver parameter and if we do, just go with default Java-ish behavior + it.extensionReceiverParameter == null + }?.let { callable -> + var companionInstance: Any? = null + var companionAccessible: Boolean? = null + + callable.instanceParameter!!.type.erasedType().kotlin + .takeIf { it.isCompanion } // abort, we have some unknown case here + ?.let { possibleCompanion -> + try { + companionInstance = possibleCompanion.objectInstance + companionAccessible = true + } catch (ex: IllegalAccessException) { + // fallback for when an odd access exception happens through Kotlin reflection + possibleCompanion.java.enclosingClass.fields + .firstOrNull { it.type.kotlin.isCompanion } + ?.let { + companionAccessible = it.isAccessible + it.isAccessible = true + + companionInstance = it.get(null) + } ?: throw ex + } + } + + companionInstance?.let { + MethodInstantiator(callable, key, it, companionAccessible!!).run { + javaMethodToInstantiator.putIfAbsent(key, this) ?: this + } + } + } + + /* + * return null if... + * - can't get kotlinFunction + * - contains extensionReceiverParameter + * - instance parameter is not companion object or can't get + */ + @Suppress("UNCHECKED_CAST") + fun instantiatorFromJava(_withArgsCreator: AnnotatedWithParams): Instantiator<*>? = when (_withArgsCreator) { + is AnnotatedConstructor -> instantiatorFromJavaConstructor(_withArgsCreator.annotated as Constructor) + is AnnotatedMethod -> instantiatorFromJavaMethod(_withArgsCreator.annotated as Method) + else -> + throw IllegalStateException("Expected a constructor or method to create a Kotlin object, instead found ${_withArgsCreator.annotated.javaClass.name}") + } } From e7d73ac5ba9da9a85da2898f17104a19d263a6c6 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 6 Jun 2021 14:53:52 +0900 Subject: [PATCH 08/13] add experimentalDeserializationBackend switch --- .../jackson/module/kotlin/KotlinFeature.kt | 3 ++- .../jackson/module/kotlin/KotlinModule.kt | 18 +++++++++++++++--- .../module/kotlin/KotlinValueInstantiator.kt | 16 +++++++++++++--- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt index d81644af..1884195d 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt @@ -8,7 +8,8 @@ enum class KotlinFeature(val enabledByDefault: Boolean) { NullToEmptyMap(enabledByDefault = false), NullIsSameAsDefault(enabledByDefault = false), SingletonSupport(enabledByDefault = false), - StrictNullChecks(enabledByDefault = false); + StrictNullChecks(enabledByDefault = false), + ExperimentalDeserializationBackend(enabledByDefault = false); internal val bitSet: BitSet = 2.0.pow(ordinal).toInt().toBitSet() } diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt index bca4f582..438277cc 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -31,6 +31,9 @@ fun Class<*>.isKotlinClass(): Boolean { * the default, collections which are typed to disallow null members * (e.g. List) may contain null values after deserialization. Enabling it * protects against this but has significant performance impact. + * @param experimentalDeserializationBackend + * Default: false. Whether to enable experimental deserialization backend. Enabling + * it significantly improve performance in certain use cases. */ class KotlinModule @Deprecated(level = DeprecationLevel.WARNING, message = "Use KotlinModule.Builder") constructor( val reflectionCacheSize: Int = 512, @@ -38,7 +41,8 @@ class KotlinModule @Deprecated(level = DeprecationLevel.WARNING, message = "Use val nullToEmptyMap: Boolean = false, val nullIsSameAsDefault: Boolean = false, val singletonSupport: SingletonSupport = DISABLED, - val strictNullChecks: Boolean = false + val strictNullChecks: Boolean = false, + val experimentalDeserializationBackend: Boolean = false ) : SimpleModule(PackageVersion.VERSION) { @Deprecated(level = DeprecationLevel.HIDDEN, message = "For ABI compatibility") constructor( @@ -77,7 +81,8 @@ class KotlinModule @Deprecated(level = DeprecationLevel.WARNING, message = "Use builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE else -> DISABLED }, - builder.isEnabled(StrictNullChecks) + builder.isEnabled(StrictNullChecks), + builder.isEnabled(KotlinFeature.ExperimentalDeserializationBackend) ) companion object { @@ -95,7 +100,14 @@ class KotlinModule @Deprecated(level = DeprecationLevel.WARNING, message = "Use val cache = ReflectionCache(reflectionCacheSize) - context.addValueInstantiators(KotlinInstantiators(cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, strictNullChecks)) + context.addValueInstantiators(KotlinInstantiators( + cache, + nullToEmptyCollection, + nullToEmptyMap, + nullIsSameAsDefault, + strictNullChecks, + experimentalDeserializationBackend + )) when (singletonSupport) { DISABLED -> Unit diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt index aed1ee84..9abf2658 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -29,7 +29,8 @@ internal class KotlinValueInstantiator( private val nullToEmptyCollection: Boolean, private val nullToEmptyMap: Boolean, private val nullIsSameAsDefault: Boolean, - private val strictNullChecks: Boolean + private val strictNullChecks: Boolean, + private val experimentalDeserializationBackend: Boolean ) : StdValueInstantiator(src) { @Suppress("UNCHECKED_CAST") override fun createFromObjectWith( @@ -188,7 +189,8 @@ internal class KotlinInstantiators( private val nullToEmptyCollection: Boolean, private val nullToEmptyMap: Boolean, private val nullIsSameAsDefault: Boolean, - private val strictNullChecks: Boolean + private val strictNullChecks: Boolean, + private val experimentalDeserializationBackend: Boolean ) : ValueInstantiators { override fun findValueInstantiator( deserConfig: DeserializationConfig, @@ -197,7 +199,15 @@ internal class KotlinInstantiators( ): ValueInstantiator { return if (beanDescriptor.beanClass.isKotlinClass()) { if (defaultInstantiator is StdValueInstantiator) { - KotlinValueInstantiator(defaultInstantiator, cache, nullToEmptyCollection, nullToEmptyMap, nullIsSameAsDefault, strictNullChecks) + KotlinValueInstantiator( + defaultInstantiator, + cache, + nullToEmptyCollection, + nullToEmptyMap, + nullIsSameAsDefault, + strictNullChecks, + experimentalDeserializationBackend + ) } else { // TODO: return defaultInstantiator and let default method parameters and nullability go unused? or die with exception: throw IllegalStateException("KotlinValueInstantiator requires that the default ValueInstantiator is StdValueInstantiator") From 01915e281475b20ca77970ac3826dcf25fc3ccbb Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 14:14:07 +0900 Subject: [PATCH 09/13] added new deserialization function and modified to call it separately --- .../module/kotlin/KotlinValueInstantiator.kt | 96 ++++++++++++++++++- 1 file changed, 95 insertions(+), 1 deletion(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt index 9abf2658..a775229d 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -32,8 +32,92 @@ internal class KotlinValueInstantiator( private val strictNullChecks: Boolean, private val experimentalDeserializationBackend: Boolean ) : StdValueInstantiator(src) { + private fun experimentalCreateFromObjectWith( + ctxt: DeserializationContext, + props: Array, + buffer: PropertyValueBuffer + ): Any? { + val instantiator: Instantiator<*> = cache.instantiatorFromJava(_withArgsCreator) + ?: return super.createFromObjectWith(ctxt, props, buffer) // we cannot reflect this method so do the default Java-ish behavior + + val bucket = instantiator.generateBucket() + + instantiator.valueParameters.forEachIndexed { idx, paramDef -> + val jsonProp = props[idx] + val isMissing = !buffer.hasParameter(jsonProp) + + if (isMissing && paramDef.isOptional) { + return@forEachIndexed + } + + var paramVal = if (!isMissing || paramDef.isPrimitive() || jsonProp.hasInjectableValueId()) { + val tempParamVal = buffer.getParameter(jsonProp) + if (nullIsSameAsDefault && tempParamVal == null && paramDef.isOptional) { + return@forEachIndexed + } + tempParamVal + } else { + // trying to get suitable "missing" value provided by deserializer + jsonProp.valueDeserializer?.getNullValue(ctxt) + } + + if (paramVal == null && ((nullToEmptyCollection && jsonProp.type.isCollectionLikeType) || (nullToEmptyMap && jsonProp.type.isMapLikeType))) { + paramVal = NullsAsEmptyProvider(jsonProp.valueDeserializer).getNullValue(ctxt) + } + + val isGenericTypeVar = paramDef.type.javaType is TypeVariable<*> + val isMissingAndRequired = paramVal == null && isMissing && jsonProp.isRequired + if (isMissingAndRequired || + (!isGenericTypeVar && paramVal == null && !paramDef.type.isMarkedNullable)) { + throw MissingKotlinParameterException( + parameter = paramDef, + processor = ctxt.parser, + msg = "Instantiation of ${this.valueTypeDesc} value failed for JSON property ${jsonProp.name} due to missing (therefore NULL) value for creator parameter ${paramDef.name} which is a non-nullable type" + ).wrapWithPath(this.valueClass, jsonProp.name) + } + + if (strictNullChecks && paramVal != null) { + var paramType: String? = null + var itemType: KType? = null + if (jsonProp.type.isCollectionLikeType && paramDef.type.arguments.getOrNull(0)?.type?.isMarkedNullable == false && (paramVal as Collection<*>).any { it == null }) { + paramType = "collection" + itemType = paramDef.type.arguments[0].type + } + + if (jsonProp.type.isMapLikeType && paramDef.type.arguments.getOrNull(1)?.type?.isMarkedNullable == false && (paramVal as Map<*, *>).any { it.value == null }) { + paramType = "map" + itemType = paramDef.type.arguments[1].type + } + + if (jsonProp.type.isArrayType && paramDef.type.arguments.getOrNull(0)?.type?.isMarkedNullable == false && (paramVal as Array<*>).any { it == null }) { + paramType = "array" + itemType = paramDef.type.arguments[0].type + } + + if (paramType != null && itemType != null) { + throw MissingKotlinParameterException( + parameter = paramDef, + processor = ctxt.parser, + msg = "Instantiation of $itemType $paramType failed for JSON property ${jsonProp.name} due to null value in a $paramType that does not allow null values" + ).wrapWithPath(this.valueClass, jsonProp.name) + } + } + + bucket[idx] = paramVal + } + + // TODO: Is it necessary to call them differently? Direct execution will perform better. + return if (bucket.isFullInitialized() && !instantiator.hasInstanceParameter) { + // we didn't do anything special with default parameters, do a normal call + super.createFromObjectWith(ctxt, bucket.values) + } else { + instantiator.checkAccessibility(ctxt) + instantiator.callBy(bucket) + } + } + @Suppress("UNCHECKED_CAST") - override fun createFromObjectWith( + private fun conventionalCreateFromObjectWith( ctxt: DeserializationContext, props: Array, buffer: PropertyValueBuffer @@ -174,6 +258,16 @@ internal class KotlinValueInstantiator( } + override fun createFromObjectWith( + ctxt: DeserializationContext, + props: Array, + buffer: PropertyValueBuffer + ): Any? = if (experimentalDeserializationBackend) { + experimentalCreateFromObjectWith(ctxt, props, buffer) + } else { + conventionalCreateFromObjectWith(ctxt, props, buffer) + } + private fun KParameter.isPrimitive(): Boolean { return when (val javaType = type.javaType) { is Class<*> -> javaType.isPrimitive From 2eba5100c286b9be2a6b25ad5b3c8c306eaaa9be Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 16 May 2021 14:14:17 +0900 Subject: [PATCH 10/13] fix format --- .../fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt index a775229d..59397a1c 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -157,7 +157,7 @@ internal class KotlinValueInstantiator( } catch (ex: IllegalAccessException) { // fallback for when an odd access exception happens through Kotlin reflection val companionField = possibleCompanion.java.enclosingClass.fields.firstOrNull { it.type.kotlin.isCompanion } - ?: throw ex + ?: throw ex val accessible = companionField.isAccessible if ((!accessible && ctxt.config.isEnabled(MapperFeature.CAN_OVERRIDE_ACCESS_MODIFIERS)) || (accessible && ctxt.config.isEnabled(MapperFeature.OVERRIDE_PUBLIC_ACCESS_MODIFIERS)) From 0fa847d64da0e08a38a6b2da7cd9900cad5e03f6 Mon Sep 17 00:00:00 2001 From: wrongwrong Date: Sun, 6 Jun 2021 14:58:57 +0900 Subject: [PATCH 11/13] added test for experimentalDeserializationBackend property --- .../kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt | 7 +++---- .../fasterxml/jackson/module/kotlin/KotlinModuleTest.kt | 5 +++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt index 759e2935..58f01479 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt @@ -2,11 +2,8 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonWriteFeature -import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault -import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection -import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap +import com.fasterxml.jackson.module.kotlin.KotlinFeature.* import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport -import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE import org.junit.Assert.assertNotNull import org.junit.Test @@ -37,6 +34,7 @@ class DslTest { enable(NullIsSameAsDefault) enable(SingletonSupport) enable(StrictNullChecks) + enable(ExperimentalDeserializationBackend) } assertNotNull(module) @@ -46,6 +44,7 @@ class DslTest { assertTrue(module.nullIsSameAsDefault) assertEquals(module.singletonSupport, CANONICALIZE) assertTrue(module.strictNullChecks) + assertTrue(module.experimentalDeserializationBackend) } @Test diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt index 079e6bbb..2412c45e 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt @@ -5,6 +5,7 @@ import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.KotlinFeature.ExperimentalDeserializationBackend import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED import org.junit.Assert.* @@ -26,6 +27,7 @@ class KotlinModuleTest { assertEquals(module.nullIsSameAsDefault, NullIsSameAsDefault.enabledByDefault) assertEquals(module.singletonSupport == CANONICALIZE, SingletonSupport.enabledByDefault) assertEquals(module.strictNullChecks, StrictNullChecks.enabledByDefault) + assertEquals(module.experimentalDeserializationBackend, ExperimentalDeserializationBackend.enabledByDefault) } @Test @@ -38,6 +40,7 @@ class KotlinModuleTest { assertFalse(module.nullIsSameAsDefault) assertEquals(DISABLED, module.singletonSupport) assertFalse(module.strictNullChecks) + assertFalse(module.experimentalDeserializationBackend) } @Test @@ -49,6 +52,7 @@ class KotlinModuleTest { enable(NullIsSameAsDefault) enable(SingletonSupport) enable(StrictNullChecks) + enable(ExperimentalDeserializationBackend) }.build() assertEquals(123, module.reflectionCacheSize) @@ -57,6 +61,7 @@ class KotlinModuleTest { assertTrue(module.nullIsSameAsDefault) assertEquals(CANONICALIZE, module.singletonSupport) assertTrue(module.strictNullChecks) + assertTrue(module.experimentalDeserializationBackend) } @Test From e96cabec7f1146b60742f9ff538a6d6136923e5d Mon Sep 17 00:00:00 2001 From: Drew Stephens Date: Fri, 15 Oct 2021 06:17:04 -0400 Subject: [PATCH 12/13] Clarify non-static inner classes --- .../fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt index 5963e016..dfc5db41 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/ConstructorInstantiator.kt @@ -7,7 +7,7 @@ import java.lang.reflect.Constructor import kotlin.reflect.KFunction import kotlin.reflect.KParameter -// This class does not support inner constructor. +// This class does not support constructors for non-static inner classes. internal class ConstructorInstantiator( kConstructor: KFunction, private val constructor: Constructor ) : Instantiator { From b1f648354fbd08493ef74fee0fd10fddd2bfe5a7 Mon Sep 17 00:00:00 2001 From: Drew Stephens Date: Fri, 15 Oct 2021 06:30:01 -0400 Subject: [PATCH 13/13] Fix comma; make eBD internal for testing access --- .../com/fasterxml/jackson/module/kotlin/KotlinFeature.kt | 4 ++-- .../com/fasterxml/jackson/module/kotlin/DslTest.kt | 7 ++++++- .../fasterxml/jackson/module/kotlin/KotlinModuleTest.kt | 9 +++++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt index 56739d3f..c3399da3 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt @@ -6,7 +6,7 @@ import kotlin.math.pow /** * @see KotlinModule.Builder */ -enum class KotlinFeature(private val enabledByDefault: Boolean) { +enum class KotlinFeature(internal val enabledByDefault: Boolean) { /** * This feature represents whether to deserialize `null` values for collection properties as empty collections. */ @@ -42,7 +42,7 @@ enum class KotlinFeature(private val enabledByDefault: Boolean) { * may contain null values after deserialization. * Enabling it protects against this but has significant performance impact. */ - StrictNullChecks(enabledByDefault = false); + StrictNullChecks(enabledByDefault = false), ExperimentalDeserializationBackend(enabledByDefault = false); diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt index a24a7f96..e072d953 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/DslTest.kt @@ -2,7 +2,12 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.core.json.JsonWriteFeature -import com.fasterxml.jackson.module.kotlin.KotlinFeature.* +import com.fasterxml.jackson.module.kotlin.KotlinFeature.ExperimentalDeserializationBackend +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap +import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport +import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE import org.junit.Assert.assertNotNull import org.junit.Test diff --git a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt index d82f0b3f..9a66a23f 100644 --- a/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt +++ b/src/test/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModuleTest.kt @@ -1,11 +1,16 @@ package com.fasterxml.jackson.module.kotlin -import com.fasterxml.jackson.module.kotlin.KotlinFeature.* import com.fasterxml.jackson.module.kotlin.KotlinFeature.ExperimentalDeserializationBackend +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullIsSameAsDefault +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyCollection +import com.fasterxml.jackson.module.kotlin.KotlinFeature.NullToEmptyMap import com.fasterxml.jackson.module.kotlin.KotlinFeature.SingletonSupport +import com.fasterxml.jackson.module.kotlin.KotlinFeature.StrictNullChecks import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED -import org.junit.Assert.* +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test class KotlinModuleTest {