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

Refactor KVI 2 #538

Closed
wants to merge 13 commits into from
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