diff --git a/pom.xml b/pom.xml index 3c9ee882..beafac69 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 + + + 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); + } +} 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() + } +} 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..dfc5db41 --- /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 constructors for non-static inner classes. +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." + ) + } + } +} 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!! + } +} 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 c8c54d25..c3399da3 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinFeature.kt @@ -1,12 +1,12 @@ package com.fasterxml.jackson.module.kotlin -import java.util.BitSet +import java.util.* 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,9 @@ 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); 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 176453b1..1e1f217c 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinModule.kt @@ -2,13 +2,10 @@ package com.fasterxml.jackson.module.kotlin import com.fasterxml.jackson.databind.MapperFeature import com.fasterxml.jackson.databind.module.SimpleModule -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.StrictNullChecks +import com.fasterxml.jackson.module.kotlin.KotlinFeature.* import com.fasterxml.jackson.module.kotlin.SingletonSupport.CANONICALIZE import com.fasterxml.jackson.module.kotlin.SingletonSupport.DISABLED -import java.util.BitSet +import java.util.* import kotlin.reflect.KClass private const val metadataFqName = "kotlin.Metadata" @@ -31,6 +28,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, @@ -52,7 +52,8 @@ class KotlinModule @Deprecated( 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(KotlinModule::class.java.name, PackageVersion.VERSION) { @Deprecated(level = DeprecationLevel.HIDDEN, message = "For ABI compatibility") constructor( @@ -91,7 +92,8 @@ class KotlinModule @Deprecated( builder.isEnabled(KotlinFeature.SingletonSupport) -> CANONICALIZE else -> DISABLED }, - builder.isEnabled(StrictNullChecks) + builder.isEnabled(StrictNullChecks), + builder.isEnabled(KotlinFeature.ExperimentalDeserializationBackend) ) companion object { @@ -109,7 +111,14 @@ class KotlinModule @Deprecated( 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..59397a1c 100644 --- a/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt +++ b/src/main/kotlin/com/fasterxml/jackson/module/kotlin/KotlinValueInstantiator.kt @@ -29,10 +29,95 @@ 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) { + 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 @@ -72,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)) @@ -173,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 @@ -188,7 +283,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 +293,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") 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 +} 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}") + } } 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 f5f7230d..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,6 +2,7 @@ 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.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 @@ -37,6 +38,7 @@ class DslTest { enable(NullIsSameAsDefault) enable(SingletonSupport) enable(StrictNullChecks) + enable(ExperimentalDeserializationBackend) } assertNotNull(module) @@ -46,6 +48,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 fe2e07cd..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,5 +1,6 @@ package com.fasterxml.jackson.module.kotlin +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 @@ -21,11 +22,12 @@ class KotlinModuleTest { val module = KotlinModule.Builder().build() assertEquals(module.reflectionCacheSize, 512) - assertFalse(module.nullToEmptyCollection) - assertFalse(module.nullToEmptyMap) - assertFalse(module.nullIsSameAsDefault) - assertEquals(module.singletonSupport, DISABLED) - assertFalse(module.strictNullChecks) + assertEquals(module.nullToEmptyCollection, NullToEmptyCollection.enabledByDefault) + assertEquals(module.nullToEmptyMap, NullToEmptyMap.enabledByDefault) + 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