Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix to use metadata jvm #14

Merged
merged 12 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ repositories {
dependencies {
implementation(kotlin("stdlib"))
implementation(kotlin("reflect"))
implementation("org.jetbrains.kotlinx:kotlinx-metadata-jvm:0.5.0")
implementation(platform("com.fasterxml.jackson:jackson-bom:2.13.3"))

// https://mvnrepository.com/artifact/com.fasterxml.jackson.core/jackson-databind
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ package com.fasterxml.jackson.module.kotlin
import com.fasterxml.jackson.core.JsonParser
import com.fasterxml.jackson.databind.JsonMappingException
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import kotlin.reflect.KParameter
import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueParameter

/**
* Specialized [JsonMappingException] sub-class used to indicate that a mandatory Kotlin constructor
* parameter was missing or null.
*/
public class MissingKotlinParameterException internal constructor(
public val parameter: KParameter,
internal val parameter: ValueParameter,
processor: JsonParser? = null,
msg: String
) : MismatchedInputException(processor, msg)
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
package com.fasterxml.jackson.module.kotlin

import com.fasterxml.jackson.databind.JsonMappingException
import kotlinx.metadata.KmClass
import kotlinx.metadata.KmValueParameter
import kotlinx.metadata.jvm.JvmMethodSignature
import kotlinx.metadata.jvm.KotlinClassHeader
import kotlinx.metadata.jvm.KotlinClassMetadata
import java.lang.reflect.Constructor
import java.lang.reflect.Method
import java.util.*
import kotlin.reflect.KType
import kotlin.reflect.jvm.jvmErasure
Expand All @@ -26,3 +33,57 @@ internal fun Class<*>.isKotlinClass(): Boolean = declaredAnnotations.any { it is
internal fun Class<*>.isUnboxableValueClass() = annotations.any { it is JvmInline }

internal fun KType.erasedType(): Class<out Any> = this.jvmErasure.java

internal fun Class<*>.toKmClass(): KmClass = annotations
.filterIsInstance<Metadata>()
.first()
.let {
KotlinClassMetadata.read(
KotlinClassHeader(
it.kind,
it.metadataVersion,
it.data1,
it.data2,
it.extraString,
it.packageName,
it.extraInt
)
) as KotlinClassMetadata.Class
}.toKmClass()

private val primitiveClassToDesc by lazy {
mapOf(
Byte::class.javaPrimitiveType to 'B',
Char::class.javaPrimitiveType to 'C',
Double::class.javaPrimitiveType to 'D',
Float::class.javaPrimitiveType to 'F',
Int::class.javaPrimitiveType to 'I',
Long::class.javaPrimitiveType to 'J',
Short::class.javaPrimitiveType to 'S',
Boolean::class.javaPrimitiveType to 'Z',
Void::class.javaPrimitiveType to 'V'
)
}

private val Class<*>.descriptor: String
get() = when {
isPrimitive -> primitiveClassToDesc.getValue(this).toString()
isArray -> "[${componentType.descriptor}"
else -> "L${name.replace('.', '/')};"
}

internal fun Array<Class<*>>.toDescString(): String =
joinToString(separator = "", prefix = "(", postfix = ")") { it.descriptor }

internal fun Constructor<*>.toSignature(): JvmMethodSignature =
JvmMethodSignature("<init>", parameterTypes.toDescString() + "V")

internal fun Method.toSignature(): JvmMethodSignature =
JvmMethodSignature(this.name, parameterTypes.toDescString() + this.returnType.descriptor)

internal fun List<KmValueParameter>.hasVarargParam(): Boolean =
lastOrNull()?.let { it.varargElementType != null } ?: false

internal val defaultConstructorMarker: Class<*> by lazy {
Class.forName("kotlin.jvm.internal.DefaultConstructorMarker")
}
Original file line number Diff line number Diff line change
Expand Up @@ -65,17 +65,17 @@ internal class ReflectionCache(reflectionCacheSize: Int) {
val constructor = _withArgsCreator.annotated as Constructor<Any>

javaConstructorToValueCreator.get(constructor)
?: kotlinFromJava(constructor)?.let {
val value = ConstructorValueCreator(it)
?: run {
val value = ConstructorValueCreator(constructor)
javaConstructorToValueCreator.putIfAbsent(constructor, value) ?: value
}
}
is AnnotatedMethod -> {
val method = _withArgsCreator.annotated as Method

javaMethodToValueCreator.get(method)
?: kotlinFromJava(method)?.let {
val value = MethodValueCreator.of(it)
?: kotlin.run {
val value = MethodValueCreator<Any?>(method)
javaMethodToValueCreator.putIfAbsent(method, value) ?: value
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,11 @@ import com.fasterxml.jackson.databind.deser.impl.PropertyValueBuffer
import com.fasterxml.jackson.databind.deser.std.StdValueInstantiator
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import com.fasterxml.jackson.module.kotlin.ReflectionCache
import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ConstructorValueCreator
import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.MethodValueCreator
import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueCreator
import com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.ValueParameter
import com.fasterxml.jackson.module.kotlin.isKotlinClass
import com.fasterxml.jackson.module.kotlin.wrapWithPath
import java.lang.reflect.TypeVariable
import kotlin.reflect.KParameter
import kotlin.reflect.KType
import kotlin.reflect.jvm.javaType

internal class KotlinValueInstantiator(
Expand All @@ -29,6 +26,10 @@ internal class KotlinValueInstantiator(
private val nullIsSameAsDefault: Boolean,
private val strictNullChecks: Boolean
) : StdValueInstantiator(src) {
// If the collection type argument cannot be obtained, treat it as nullable
// @see com.fasterxml.jackson.module.kotlin._ported.test.StrictNullChecksTest#testListOfGenericWithNullValue
private fun ValueParameter.isNullishTypeAt(index: Int) = arguments.getOrNull(index)?.isNullable ?: true

override fun createFromObjectWith(
ctxt: DeserializationContext,
props: Array<out SettableBeanProperty>,
Expand All @@ -37,24 +38,7 @@ internal class KotlinValueInstantiator(
val valueCreator: ValueCreator<*> = cache.valueCreatorFromJava(_withArgsCreator)
?: return super.createFromObjectWith(ctxt, props, buffer)

val propCount: Int
var numCallableParameters: Int
val callableParameters: Array<KParameter?>
val jsonParamValueList: Array<Any?>

if (valueCreator is MethodValueCreator) {
propCount = props.size + 1
numCallableParameters = 1
callableParameters = arrayOfNulls<KParameter>(propCount)
.apply { this[0] = valueCreator.instanceParameter }
jsonParamValueList = arrayOfNulls<Any>(propCount)
.apply { this[0] = valueCreator.companionObjectInstance }
} else {
propCount = props.size
numCallableParameters = 0
callableParameters = arrayOfNulls(propCount)
jsonParamValueList = arrayOfNulls(propCount)
}
val bucket = valueCreator.generateBucket()

valueCreator.valueParameters.forEachIndexed { idx, paramDef ->
val jsonProp = props[idx]
Expand All @@ -64,14 +48,14 @@ internal class KotlinValueInstantiator(
return@forEachIndexed
}

var paramVal = if (!isMissing || paramDef.isPrimitive() || jsonProp.hasInjectableValueId()) {
var paramVal = if (!isMissing || paramDef.isPrimitive || jsonProp.hasInjectableValueId()) {
val tempParamVal = buffer.getParameter(jsonProp)
if (nullIsSameAsDefault && tempParamVal == null && paramDef.isOptional) {
return@forEachIndexed
}
tempParamVal
} else {
if (paramDef.type.isMarkedNullable) {
if (paramDef.isNullable) {
// do not try to create any object if it is nullable and the value is missing
null
} else {
Expand All @@ -84,11 +68,8 @@ internal class KotlinValueInstantiator(
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)
) {
if (isMissingAndRequired || (!paramDef.isGenericType && paramVal == null && !paramDef.isNullable)) {
throw MissingKotlinParameterException(
parameter = paramDef,
processor = ctxt.parser,
Expand All @@ -97,24 +78,17 @@ internal class KotlinValueInstantiator(
}

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) {
// If an error occurs, Argument.name is always non-null
// @see com.fasterxml.jackson.module.kotlin.deser.value_instantiator.creator.Argument
when {
paramVal is Collection<*> && !paramDef.isNullishTypeAt(0) && paramVal.any { it == null } ->
"collection" to paramDef.arguments[0].name!!
paramVal is Array<*> && !paramDef.isNullishTypeAt(0) && paramVal.any { it == null } ->
"array" to paramDef.arguments[0].name!!
paramVal is Map<*, *> && !paramDef.isNullishTypeAt(1) && paramVal.values.any { it == null } ->
"map" to paramDef.arguments[1].name!!
else -> null
}?.let { (paramType, itemType) ->
throw MissingKotlinParameterException(
parameter = paramDef,
processor = ctxt.parser,
Expand All @@ -123,25 +97,11 @@ internal class KotlinValueInstantiator(
}
}

jsonParamValueList[numCallableParameters] = paramVal
callableParameters[numCallableParameters] = paramDef
numCallableParameters++
bucket[idx] = paramVal
}

return if (numCallableParameters == jsonParamValueList.size && valueCreator is ConstructorValueCreator) {
// we didn't do anything special with default parameters, do a normal call
super.createFromObjectWith(ctxt, jsonParamValueList)
} else {
valueCreator.checkAccessibility(ctxt)

val callableParametersByName = linkedMapOf<KParameter, Any?>()
callableParameters.mapIndexed { idx, paramDef ->
if (paramDef != null) {
callableParametersByName[paramDef] = jsonParamValueList[idx]
}
}
valueCreator.callBy(callableParametersByName)
}
valueCreator.checkAccessibility(ctxt)
return valueCreator.callBy(bucket)
}

private fun KParameter.isPrimitive(): Boolean {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package com.fasterxml.jackson.module.kotlin.deser.value_instantiator.argument_bucket

import java.lang.reflect.Array as ReflectArray

private fun defaultPrimitiveValue(type: Class<*>): Any = when (type) {
Boolean::class.java -> false
Char::class.java -> 0.toChar()
Byte::class.java -> 0.toByte()
Short::class.java -> 0.toShort()
Int::class.java -> 0
Float::class.java -> 0f
Long::class.java -> 0L
Double::class.java -> 0.0
Void.TYPE -> throw IllegalStateException("Parameter with void type is illegal")
else -> throw UnsupportedOperationException("Unknown primitive: $type")
}

private fun defaultEmptyArray(arrayType: Class<*>): Any =
ReflectArray.newInstance(arrayType.componentType, 0)

// @see https://github.com/JetBrains/kotlin/blob/4c925d05883a8073e6732bca95bf575beb031a59/core/reflection.jvm/src/kotlin/reflect/jvm/internal/KCallableImpl.kt#L114
internal class BucketGenerator(
parameterTypes: List<Class<*>>,
hasVarargParam: Boolean
) {
private val valueParameterSize: Int = parameterTypes.size
private val originalAbsentArgs: Array<Any?>

init {
val maskSize = (parameterTypes.size + Integer.SIZE - 1) / Integer.SIZE

// Array containing the actual function arguments, masks, and +1 for DefaultConstructorMarker or MethodHandle.
originalAbsentArgs = arrayOfNulls<Any?>(valueParameterSize + maskSize + 1)

// Set values of primitive arguments to the boxed default values (such as 0, 0.0, false) instead of nulls.
parameterTypes.forEachIndexed { i, clazz ->
if (clazz.isPrimitive) {
originalAbsentArgs[i] = defaultPrimitiveValue(clazz)
}
}

if (hasVarargParam) {
// vararg argument is always at the end of the arguments.
val i = valueParameterSize - 1
originalAbsentArgs[i] = defaultEmptyArray(parameterTypes[i])
}

for (i in 0 until maskSize) {
originalAbsentArgs[valueParameterSize + i] = -1
}
}

fun generate(): ArgumentBucket = ArgumentBucket(valueParameterSize, originalAbsentArgs.clone())
}

internal class ArgumentBucket(
private val valueParameterSize: Int,
private val arguments: Array<Any?>
) {
companion object {
// List of Int with only 1 bit enabled.
private val BIT_FLAGS: List<Int> = IntArray(Int.SIZE_BITS) { (1 shl it).inv() }.asList()
}

private var count = 0

operator fun set(index: Int, arg: Any?) {
// Since there is no multiple initialization in the use case, the key check is omitted.
arguments[index] = arg

val maskIndex = valueParameterSize + (index / Integer.SIZE)
arguments[maskIndex] = (arguments[maskIndex] as Int) and BIT_FLAGS[index % Integer.SIZE]

count++
}

val isFullInitialized: Boolean get() = count == valueParameterSize

fun getArgs(): Array<Any?> = arguments.copyOf(valueParameterSize)
fun getDefaultArgs(): Array<Any?> = arguments
}
Loading