Skip to content

Commit

Permalink
Merge pull request #244 from ProjectMapK/develop
Browse files Browse the repository at this point in the history
Release 2024-11-23 16:34:29 +0000
  • Loading branch information
k163377 authored Nov 23, 2024
2 parents 2015dfa + 4409175 commit bd35bb8
Show file tree
Hide file tree
Showing 10 changed files with 127 additions and 87 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This project makes several disruptive changes to achieve more `Kotlin-like` beha
Details are summarized in [KogeraSpecificImplementations](./docs/KogeraSpecificImplementations.md).

# Compatibility
- `jackson 2.17.x`
- `jackson 2.18.x`
- `Java 8+`
- `Kotlin 1.8.22+`

Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ val jacksonVersion = libs.versions.jackson.get()
val generatedSrcPath = "${layout.buildDirectory.get()}/generated/kotlin"

group = groupStr
version = "${jacksonVersion}-beta14"
version = "${jacksonVersion}-beta15"

repositories {
mavenCentral()
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[versions]
kotlin = "1.8.22" # Mainly for CI, it can be rewritten by environment variable.
jackson = "2.17.3"
jackson = "2.18.1"

# test libs
junit = "5.11.3"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ public inline fun <reified T> ObjectMapper.readValue(src: ByteArray): T = readVa

public inline fun <reified T> ObjectMapper.treeToValue(n: TreeNode): T =
readValue(this.treeAsTokens(n), jacksonTypeRef<T>())
public inline fun <reified T> ObjectMapper.convertValue(from: Any): T = convertValue(from, jacksonTypeRef<T>())
public inline fun <reified T> ObjectMapper.convertValue(from: Any?): T = convertValue(from, jacksonTypeRef<T>())

public inline fun <reified T> ObjectReader.readValueTyped(jp: JsonParser): T = readValue(jp, jacksonTypeRef<T>())
public inline fun <reified T> ObjectReader.readValuesTyped(jp: JsonParser): Iterator<T> =
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.projectmapk.jackson.module.kogera.annotationIntrospector

import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.annotation.JsonSetter
import com.fasterxml.jackson.annotation.Nulls
import com.fasterxml.jackson.databind.JavaType
Expand All @@ -11,11 +12,14 @@ import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
import com.fasterxml.jackson.databind.introspect.AnnotatedParameter
import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.introspect.PotentialCreator
import com.fasterxml.jackson.databind.util.Converter
import io.github.projectmapk.jackson.module.kogera.JSON_K_UNBOX_CLASS
import io.github.projectmapk.jackson.module.kogera.KOTLIN_DURATION_CLASS
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.isUnboxableValueClass
import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass
import io.github.projectmapk.jackson.module.kogera.jmClass.JmConstructor
import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter
import io.github.projectmapk.jackson.module.kogera.ser.KotlinDurationValueToJavaDurationConverter
import io.github.projectmapk.jackson.module.kogera.ser.KotlinToJavaDurationConverter
Expand Down Expand Up @@ -120,13 +124,57 @@ internal class KotlinFallbackAnnotationIntrospector(
}
}
?: super.findSetterInfo(ann)

// If it is not a Kotlin class or an Enum, Creator is not used
private fun AnnotatedClass.creatableKotlinClass(): JmClass? = annotated
.takeIf { !it.isEnum }
?.let { cache.getJmClass(it) }

override fun findDefaultCreator(
config: MapperConfig<*>,
valueClass: AnnotatedClass,
declaredConstructors: List<PotentialCreator>,
declaredFactories: List<PotentialCreator>
): PotentialCreator? {
val jmClass = valueClass.creatableKotlinClass() ?: return null
val primarilyConstructor = jmClass.primarilyConstructor()
?.takeIf { it.valueParameters.isNotEmpty() }
?: return null
val isPossiblySingleString = isPossiblySingleString(primarilyConstructor, jmClass)

for (it in declaredConstructors) {
val javaConstructor = it.creator().annotated as Constructor<*>

if (primarilyConstructor.isMetadataFor(javaConstructor)) {
if (isPossibleSingleString(isPossiblySingleString, javaConstructor)) {
break
} else {
return it
}
}
}

return null
}
}

private fun JmValueParameter.isNullishTypeAt(index: Int): Boolean = arguments.getOrNull(index)?.let {
// If it is not a StarProjection, type is not null
it === KmTypeProjection.STAR || it.type!!.isNullable
} ?: true // If a type argument cannot be taken, treat it as nullable to avoid unexpected failure.
} != false // If a type argument cannot be taken, treat it as nullable to avoid unexpected failure.

private fun JmValueParameter.requireStrictNullCheck(type: JavaType): Boolean =
((type.isArrayType || type.isCollectionLikeType) && !this.isNullishTypeAt(0)) ||
(type.isMapLikeType && !this.isNullishTypeAt(1))

private fun JmClass.primarilyConstructor() = constructors.find { !it.isSecondary } ?: constructors.singleOrNull()

private fun isPossiblySingleString(
jmConstructor: JmConstructor,
jmClass: JmClass
) = jmConstructor.valueParameters.singleOrNull()?.let { it.isString && it.name !in jmClass.propertyNameSet } == true

private fun isPossibleSingleString(
isPossiblySingleString: Boolean,
javaConstructor: Constructor<*>
): Boolean = isPossiblySingleString && javaConstructor.parameters[0].annotations.none { it is JsonProperty }
Original file line number Diff line number Diff line change
@@ -1,11 +1,7 @@
package io.github.projectmapk.jackson.module.kogera.annotationIntrospector

import com.fasterxml.jackson.annotation.JsonCreator
import com.fasterxml.jackson.annotation.JsonProperty
import com.fasterxml.jackson.databind.JavaType
import com.fasterxml.jackson.databind.cfg.MapperConfig
import com.fasterxml.jackson.databind.introspect.Annotated
import com.fasterxml.jackson.databind.introspect.AnnotatedConstructor
import com.fasterxml.jackson.databind.introspect.AnnotatedField
import com.fasterxml.jackson.databind.introspect.AnnotatedMember
import com.fasterxml.jackson.databind.introspect.AnnotatedMethod
Expand All @@ -14,18 +10,13 @@ import com.fasterxml.jackson.databind.introspect.NopAnnotationIntrospector
import com.fasterxml.jackson.databind.jsontype.NamedType
import io.github.projectmapk.jackson.module.kogera.JSON_PROPERTY_CLASS
import io.github.projectmapk.jackson.module.kogera.ReflectionCache
import io.github.projectmapk.jackson.module.kogera.hasCreatorAnnotation
import io.github.projectmapk.jackson.module.kogera.jmClass.JmClass
import io.github.projectmapk.jackson.module.kogera.jmClass.JmProperty
import io.github.projectmapk.jackson.module.kogera.jmClass.JmValueParameter
import io.github.projectmapk.jackson.module.kogera.reconstructClass
import io.github.projectmapk.jackson.module.kogera.toSignature
import kotlinx.metadata.KmClassifier
import kotlinx.metadata.isNullable
import java.lang.reflect.Constructor
import java.lang.reflect.Executable
import java.lang.reflect.Method
import java.lang.reflect.Modifier

// AnnotationIntrospector that overrides the behavior of the default AnnotationIntrospector
// (in most cases, JacksonAnnotationIntrospector).
Expand Down Expand Up @@ -116,57 +107,4 @@ internal class KotlinPrimaryAnnotationIntrospector(
override fun findSubtypes(a: Annotated): List<NamedType>? = cache.getJmClass(a.rawType)?.let { jmClass ->
jmClass.sealedSubclasses.map { NamedType(it.reconstructClass()) }.ifEmpty { null }
}

// Return Mode.DEFAULT if ann is a Primary Constructor and the condition is satisfied.
// Currently, there is no way to define the priority of a Creator,
// so the presence or absence of a JsonCreator is included in the decision.
// The reason for overriding the JacksonAnnotationIntrospector is to reduce overhead.
// In rare cases, a problem may occur,
// but it is assumed that the problem can be solved by adjusting the order of module registration.
override fun findCreatorAnnotation(config: MapperConfig<*>, ann: Annotated): JsonCreator.Mode? {
(ann as? AnnotatedConstructor)?.takeIf { 0 < it.parameterCount } ?: return null

val declaringClass = ann.declaringClass
val jmClass = declaringClass.takeIf { !it.isEnum }
?.let { cache.getJmClass(it) }
?: return null

return JsonCreator.Mode.DEFAULT
.takeIf { ann.annotated.isPrimarilyConstructorOf(jmClass) && !hasCreator(declaringClass, jmClass) }
}
}

private fun Constructor<*>.isPrimarilyConstructorOf(jmClass: JmClass): Boolean = jmClass.findJmConstructor(this)
?.let { !it.isSecondary || jmClass.constructors.size == 1 }
?: false

private fun KmClassifier.isString(): Boolean = this is KmClassifier.Class && this.name == "kotlin/String"

private fun isPossibleSingleString(
kotlinParams: List<JmValueParameter>,
javaFunction: Executable,
propertyNames: Set<String>
): Boolean = kotlinParams.size == 1 &&
kotlinParams[0].let { it.name !in propertyNames && it.isString } &&
javaFunction.parameters[0].annotations.none { it is JsonProperty }

private fun hasCreatorConstructor(clazz: Class<*>, jmClass: JmClass, propertyNames: Set<String>): Boolean {
val kmConstructorMap = jmClass.constructors.associateBy { it.signature?.descriptor }

return clazz.constructors.any { constructor ->
val kmConstructor = kmConstructorMap[constructor.toSignature().descriptor] ?: return@any false

!isPossibleSingleString(kmConstructor.valueParameters, constructor, propertyNames) &&
constructor.hasCreatorAnnotation()
}
}

// In the original, `isPossibleSingleString` comparison was disabled,
// and if enabled, the behavior would have changed, so the comparison is skipped.
private fun hasCreatorFunction(clazz: Class<*>): Boolean = clazz.declaredMethods
.any { Modifier.isStatic(it.modifiers) && it.hasCreatorAnnotation() }

private fun hasCreator(clazz: Class<*>, jmClass: JmClass): Boolean {
val propertyNames = jmClass.propertyNameSet
return hasCreatorConstructor(clazz, jmClass, propertyNames) || hasCreatorFunction(clazz)
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.projectmapk.jackson.module.kogera.jmClass

import io.github.projectmapk.jackson.module.kogera.reconstructClassOrNull
import io.github.projectmapk.jackson.module.kogera.toDescBuilder
import io.github.projectmapk.jackson.module.kogera.toKmClass
import io.github.projectmapk.jackson.module.kogera.toSignature
import kotlinx.metadata.ClassKind
Expand Down Expand Up @@ -106,25 +105,8 @@ private class JmClassImpl(
companionPropName?.let { JmClass.CompanionObject(clazz, it) }
}

override fun findJmConstructor(constructor: Constructor<*>): JmConstructor? {
val descHead = constructor.parameterTypes.toDescBuilder()
val len = descHead.length
val desc = CharArray(len + 1).apply {
descHead.getChars(0, len, this, 0)
this[len] = 'V'
}.let { String(it) }

// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
val valueDesc = descHead
.replace(len - 1, len, "Lkotlin/jvm/internal/DefaultConstructorMarker;)V")
.toString()

// Constructors always have the same name, so only desc is compared
return constructors.find {
val targetDesc = it.signature?.descriptor
targetDesc == desc || targetDesc == valueDesc
}
}
override fun findJmConstructor(constructor: Constructor<*>): JmConstructor? =
constructors.find { it.isMetadataFor(constructor) }

// Field name always matches property name
override fun findPropertyByField(field: Field): JmProperty? = allPropsMap[field.name]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package io.github.projectmapk.jackson.module.kogera.jmClass

import io.github.projectmapk.jackson.module.kogera.toDescBuilder
import kotlinx.metadata.KmConstructor
import kotlinx.metadata.isSecondary
import kotlinx.metadata.jvm.JvmMethodSignature
import kotlinx.metadata.jvm.signature
import java.lang.reflect.Constructor

internal data class JmConstructor(
val isSecondary: Boolean,
Expand All @@ -15,4 +17,22 @@ internal data class JmConstructor(
signature = constructor.signature,
valueParameters = constructor.valueParameters.map { JmValueParameter(it) }
)

// Only constructors that take a value class as an argument have a DefaultConstructorMarker on the Signature.
private fun StringBuilder.valueDesc(len: Int) =
replace(len - 1, len, "Lkotlin/jvm/internal/DefaultConstructorMarker;)V").toString()

fun isMetadataFor(constructor: Constructor<*>): Boolean {
val targetDesc = signature?.descriptor

val descHead = constructor.parameterTypes.toDescBuilder()
val len = descHead.length
val desc = CharArray(len + 1).apply {
descHead.getChars(0, len, this, 0)
this[len] = 'V'
}.let { String(it) }

// Constructors always have the same name, so only desc is compared
return targetDesc == desc || targetDesc == descHead.valueDesc(len)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package io.github.projectmapk.jackson.module.kogera.zPorted.test.github

import com.fasterxml.jackson.databind.json.JsonMapper
import io.github.projectmapk.jackson.module.kogera.KotlinFeature
import io.github.projectmapk.jackson.module.kogera.KotlinModule
import io.github.projectmapk.jackson.module.kogera.convertValue
import org.junit.jupiter.api.Assertions.assertNull
import org.junit.jupiter.api.Test

class GitHub757 {
@Test
fun test() {
val kotlinModule = KotlinModule.Builder()
.enable(KotlinFeature.StrictNullChecks)
.build()
val mapper = JsonMapper.builder()
.addModule(kotlinModule)
.build()
val convertValue = mapper.convertValue<String?>(null)
assertNull(convertValue)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package io.github.projectmapk.jackson.module.kogera.zPorted.test.github

import com.fasterxml.jackson.annotation.JsonTypeInfo
import com.fasterxml.jackson.databind.ObjectMapper
import io.github.projectmapk.jackson.module.kogera.readValue
import io.github.projectmapk.jackson.module.kogera.registerKotlinModule
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test

@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "_type")
private sealed class BaseClass

private data class ChildClass(val text: String) : BaseClass()

class GitHub844 {
@Test
fun test() {
val json = """
{
"_type": "ChildClass",
"text": "Test"
}
"""

val jacksonObjectMapper = ObjectMapper().registerKotlinModule()
val message = jacksonObjectMapper.readValue<BaseClass>(json)

assertEquals(ChildClass("Test"), message)
}
}

0 comments on commit bd35bb8

Please sign in to comment.