Skip to content
Closed
15 changes: 14 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,16 @@
<artifactId>jackson-dataformat-xml</artifactId>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.mockk/mockk-jvm -->
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk-jvm</artifactId>
<version>1.13.4</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<plugins>
<plugin>
Expand All @@ -139,6 +145,13 @@
<goals>
<goal>compile</goal>
</goals>
<configuration>
<sourceDirs>
<source>${project.basedir}/target/generated-sources</source>
<source>${project.basedir}/src/main/java</source>
<source>${project.basedir}/src/main/kotlin</source>
</sourceDirs>
</configuration>
</execution>

<execution>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.fasterxml.jackson.module.kotlin;

import kotlin.reflect.KFunction;
import org.jetbrains.annotations.NotNull;

/**
* Wrapper to avoid costly calls using spread operator.
* @since 2.13
*/
class SpreadWrapper {
private SpreadWrapper() {}

static <T> T call(@NotNull KFunction<T> function, @NotNull Object[] args) {
return function.call(args);
}
}
151 changes: 151 additions & 0 deletions src/main/kotlin/com/fasterxml/jackson/module/kotlin/ArgumentBucket.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package com.fasterxml.jackson.module.kotlin

import kotlin.reflect.KParameter

/**
* Calculation of where the initialization state of KParameter.index is recorded in the masks.
* @return index / 32(= Int.SIZE_BITS)
*/
private fun getMaskIndex(index: Int) = index shr 5

/**
* Calculation of where the initialization state of KParameter.index is recorded in the bit of int.
* @return index % 32(= Int.SIZE_BITS)
*/
private fun getFlagIndex(index: Int) = index and 31

/**
* Generator for [ArgumentBucket].
* Refer to the documentation of [ArgumentBucket] and factory function for the contents of each argument.
*/
internal class BucketGenerator private constructor(
private val paramSize: Int,
private val originalValues: Array<Any?>,
private val originalMasks: IntArray,
private val originalInitializedCount: Int,
private val parameters: List<KParameter>
) {
fun generate(): ArgumentBucket = ArgumentBucket(
paramSize,
originalValues.clone(),
originalMasks.clone(),
originalInitializedCount,
parameters
)

companion object {
// -1 is a value where all bits are filled with 1
private const val FILLED_MASK: Int = -1

// The maximum size of the array is obtained by getMaskIndex(paramSize) + 1.
private fun getOriginalMasks(paramSize: Int): IntArray = IntArray(getMaskIndex(paramSize) + 1) { FILLED_MASK }

/**
* @return [BucketGenerator] when the target of the call is a constructor.
*/
fun forConstructor(parameters: List<KParameter>): BucketGenerator {
val paramSize = parameters.size
// Since the constructor does not require any instance parameters, do not operation the values.
return BucketGenerator(paramSize, arrayOfNulls(paramSize), getOriginalMasks(paramSize), 0, parameters)
}

/**
* @return [BucketGenerator] when the target of the call is a method.
*/
fun forMethod(parameters: List<KParameter>, instance: Any): BucketGenerator {
val paramSize = parameters.size

// Since the method requires instance parameter, it is necessary to perform several operations.

// In the jackson-module-kotlin process, instance parameters are always at the top,
// so they should be placed at the top of originalValues.
val originalValues = arrayOfNulls<Any?>(paramSize).apply { this[0] = instance }
// Since the instance parameters have already been initialized,
// the originalMasks must also be in the corresponding state.
val originalMasks = getOriginalMasks(paramSize).apply { this[0] = this[0] and 1.inv() }
// Since the instance parameters have already been initialized, the originalInitializedCount will be 1.
return BucketGenerator(paramSize, originalValues, originalMasks, 1, parameters)
}
}
}

/**
* Class for managing arguments and their initialization state.
* [masks] is used to manage the initialization state of arguments.
* For the [masks] bit, 0 means initialized and 1 means uninitialized.
*
* At this point, this management method may not necessarily be ideal,
* but the reason that using this method is to simplify changes like @see <a href="https://github.com/FasterXML/jackson-module-kotlin/pull/439">#439</a>.
*
* @property paramSize Cache of [parameters].size.
* @property actualValues Arguments arranged in order in the manner of a bucket sort.
* @property masks Initialization state of arguments.
* @property initializedCount Number of initialized parameters.
* @property parameters Parameters of the KFunction to be called.
*/
internal class ArgumentBucket(
private val paramSize: Int,
val actualValues: Array<Any?>,
private val masks: IntArray,
private var initializedCount: Int,
private val parameters: List<KParameter>
): Map<KParameter, Any?> {
class Entry internal constructor(
override val key: KParameter,
override var value: Any?
) : Map.Entry<KParameter, Any?>

/**
* If the argument corresponding to KParameter.index is initialized, true is returned.
*/
private fun isInitialized(index: Int): Boolean = masks[getMaskIndex(index)]
.let { (it and BIT_FLAGS[getFlagIndex(index)]) == it }

override val entries: Set<Map.Entry<KParameter, Any?>>
get() = parameters.fold(HashSet()) { acc, cur ->
val index = cur.index
acc.apply { if (isInitialized(index)) add(Entry(parameters[index], actualValues[index])) }
}
override val keys: Set<KParameter>
get() = parameters.fold(HashSet()) { acc, cur -> acc.apply { if (isInitialized(cur.index)) add(cur) } }
override val size: Int
get() = initializedCount
override val values: Collection<Any?>
get() = actualValues.filterIndexed { index, _ -> isInitialized(index) }

override fun containsKey(key: KParameter): Boolean = isInitialized(key.index)

override fun containsValue(value: Any?): Boolean =
(0 until paramSize).any { isInitialized(it) && value == actualValues[it] }

override fun get(key: KParameter): Any? = actualValues[key.index]

override fun isEmpty(): Boolean = initializedCount == 0

/**
* Set the value to KParameter.index.
* However, if the corresponding index has already been initialized, nothing is done.
*/
operator fun set(index: Int, value: Any?) {
val maskIndex = getMaskIndex(index)
val flagIndex = getFlagIndex(index)

val updatedMask = masks[maskIndex] and BIT_FLAGS[flagIndex]

if (updatedMask != masks[maskIndex]) {
actualValues[index] = value
masks[maskIndex] = updatedMask
initializedCount++
}
}

/**
* Return true if all arguments are [set].
*/
fun isFullInitialized(): Boolean = initializedCount == paramSize

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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import kotlin.reflect.jvm.isAccessible

internal class ConstructorValueCreator<T>(override val callable: KFunction<T>) : ValueCreator<T>() {
override val accessible: Boolean = callable.isAccessible
override val bucketGenerator: BucketGenerator = BucketGenerator.forConstructor(callable.parameters)

init {
// To prevent the call from failing, save the initial value and then rewrite the flag.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,7 @@ internal class KotlinValueInstantiator(
): Any? {
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 argumentBucket: ArgumentBucket = valueCreator.generateBucket()

valueCreator.valueParameters.forEachIndexed { idx, paramDef ->
val jsonProp = props[idx]
Expand Down Expand Up @@ -115,24 +97,15 @@ internal class KotlinValueInstantiator(
}
}

jsonParamValueList[numCallableParameters] = paramVal
callableParameters[numCallableParameters] = paramDef
numCallableParameters++
argumentBucket[paramDef.index] = paramVal
}

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

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

}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
package com.fasterxml.jackson.module.kotlin

import kotlin.reflect.KFunction
import kotlin.reflect.KParameter
import kotlin.reflect.full.extensionReceiverParameter
import kotlin.reflect.full.instanceParameter
import kotlin.reflect.jvm.isAccessible

internal class MethodValueCreator<T> private constructor(
override val callable: KFunction<T>,
override val accessible: Boolean,
val companionObjectInstance: Any
companionObjectInstance: Any
) : ValueCreator<T>() {
val instanceParameter: KParameter = callable.instanceParameter!!
override val bucketGenerator: BucketGenerator =
BucketGenerator.forMethod(callable.parameters, companionObjectInstance)

companion object {
fun <T> of(callable: KFunction<T>): MethodValueCreator<T>? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ internal sealed class ValueCreator<T> {
*/
protected abstract val accessible: Boolean

/**
* A generator that generates an [ArgumentBucket] for binding arguments.
*/
protected abstract val bucketGenerator: BucketGenerator

/**
* ValueParameters of the KFunction to be called.
*/
Expand All @@ -29,6 +34,11 @@ internal sealed class ValueCreator<T> {
// @see #584
val valueParameters: List<KParameter> get() = callable.valueParameters

/**
* @return An [ArgumentBucket] to bind arguments to.
*/
fun generateBucket(): ArgumentBucket = bucketGenerator.generate()

/**
* Checking process to see if access from context is possible.
* @throws IllegalAccessException
Expand All @@ -45,5 +55,8 @@ internal sealed class ValueCreator<T> {
/**
* Function call with default values enabled.
*/
fun callBy(args: Map<KParameter, Any?>): T = callable.callBy(args)
fun callBy(args: ArgumentBucket): T = if (args.isFullInitialized())
SpreadWrapper.call(callable, args.actualValues)
else
callable.callBy(args)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.fasterxml.jackson.module.kotlin

import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertFalse
import kotlin.test.assertNull
import kotlin.test.assertTrue

internal class ArgumentBucketTest {
data class Data(val foo: Int, val bar: Int, val baz: Int)
private val parameters = ::Data.parameters
private val generator = BucketGenerator.forConstructor(parameters)

@Test
fun setTest() {
val bucket = generator.generate()

assertTrue(bucket.isEmpty())
assertNull(bucket[parameters[0]])

// set will succeed.
bucket[0] = 0
assertEquals(1, bucket.size)
assertEquals(0, bucket[parameters[0]])

// If set the same key multiple times, the original value will not be rewritten.
bucket[0] = 1
assertEquals(1, bucket.size)
assertEquals(0, bucket[parameters[0]])
}

@Test
fun isFullInitializedTest() {
val bucket = generator.generate()

assertFalse(bucket.isFullInitialized())

(parameters.indices).forEach { bucket[it] = it }

assertTrue(bucket.isFullInitialized())
}

@Test
fun containsValueTest() {
val bucket = generator.generate()

assertFalse(bucket.containsValue(null))
bucket[0] = null
assertTrue(bucket.containsValue(null))

assertFalse(bucket.containsValue(1))
bucket[1] = 1
assertTrue(bucket.containsValue(1))
}
}
Loading