kotlin.reflect.jvm.internal.calls.AnnotationConstructorCaller.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of kotlin-reflect Show documentation
Show all versions of kotlin-reflect Show documentation
Kotlin Full Reflection Library
/*
* Copyright 2010-2018 JetBrains s.r.o. and Kotlin Programming Language contributors.
* Use of this source code is governed by the Apache 2.0 license that can be found in the license/LICENSE.txt file.
*/
package kotlin.reflect.jvm.internal.calls
import org.jetbrains.kotlin.descriptors.runtime.structure.wrapperByPrimitive
import java.lang.reflect.Proxy
import java.lang.reflect.Type
import kotlin.reflect.KClass
import kotlin.reflect.jvm.internal.KotlinReflectionInternalError
import java.lang.reflect.Method as ReflectMethod
internal class AnnotationConstructorCaller(
private val jClass: Class<*>,
private val parameterNames: List,
private val callMode: CallMode,
origin: Origin,
private val methods: List = parameterNames.map { name -> jClass.getDeclaredMethod(name) }
) : Caller {
override val member: Nothing?
get() = null
override val returnType: Type
get() = jClass
override val parameterTypes: List = methods.map { it.genericReturnType }
enum class CallMode { CALL_BY_NAME, POSITIONAL_CALL }
enum class Origin { JAVA, KOTLIN }
// Transform primitive int to java.lang.Integer because actual arguments passed here will be boxed and Class#isInstance should succeed
private val erasedParameterTypes: List> = methods.map { method -> method.returnType.let { it.wrapperByPrimitive ?: it } }
private val defaultValues: List = methods.map { method -> method.defaultValue }
init {
// TODO: consider lifting this restriction once KT-8957 is implemented
if (callMode == CallMode.POSITIONAL_CALL && origin == Origin.JAVA && (parameterNames - "value").isNotEmpty()) {
throw UnsupportedOperationException(
"Positional call of a Java annotation constructor is allowed only if there are no parameters " +
"or one parameter named \"value\". This restriction exists because Java annotations (in contrast to Kotlin)" +
"do not impose any order on their arguments. Use KCallable#callBy instead."
)
}
}
override fun call(args: Array<*>): Any? {
checkArguments(args)
val values = args.mapIndexed { index, arg ->
val value =
if (arg == null && callMode == CallMode.CALL_BY_NAME) defaultValues[index]
else arg.transformKotlinToJvm(erasedParameterTypes[index])
value ?: throwIllegalArgumentType(index, parameterNames[index], erasedParameterTypes[index])
}
return createAnnotationInstance(jClass, parameterNames.zip(values).toMap(), methods)
}
}
/**
* Transforms a Kotlin value to the one required by the JVM, e.g. KClass<*> -> Class<*> or Array> -> Array>.
* Returns `null` in case when no transformation is possible (an argument of an incorrect type was passed).
*/
private fun Any?.transformKotlinToJvm(expectedType: Class<*>): Any? {
@Suppress("UNCHECKED_CAST")
val result = when (this) {
is Class<*> -> return null
is KClass<*> -> this.java
is Array<*> -> when {
this.isArrayOf>() -> return null
this.isArrayOf>() -> (this as Array>).map(KClass<*>::java).toTypedArray()
else -> this
}
else -> this
}
return if (expectedType.isInstance(result)) result else null
}
private fun throwIllegalArgumentType(index: Int, name: String, expectedJvmType: Class<*>): Nothing {
val kotlinClass = when {
expectedJvmType == Class::class.java -> KClass::class
expectedJvmType.isArray && expectedJvmType.componentType == Class::class.java ->
@Suppress("CLASS_LITERAL_LHS_NOT_A_CLASS") Array>::class // Workaround KT-13924
else -> expectedJvmType.kotlin
}
// For arrays, also render the type argument in the message, e.g. "... not of the required type kotlin.Array"
val typeString =
if (kotlinClass.qualifiedName == Array::class.qualifiedName)
"${kotlinClass.qualifiedName}<${kotlinClass.java.componentType.kotlin.qualifiedName}>"
else kotlinClass.qualifiedName
throw IllegalArgumentException("Argument #$index $name is not of the required type $typeString")
}
internal fun createAnnotationInstance(
annotationClass: Class,
values: Map,
methods: List = values.keys.map { name -> annotationClass.getDeclaredMethod(name) }
): T {
fun equals(other: Any?): Boolean =
(other as? Annotation)?.annotationClass?.java == annotationClass &&
methods.all { method ->
val ours = values[method.name]
val theirs = method(other)
when (ours) {
is BooleanArray -> ours contentEquals theirs as BooleanArray
is CharArray -> ours contentEquals theirs as CharArray
is ByteArray -> ours contentEquals theirs as ByteArray
is ShortArray -> ours contentEquals theirs as ShortArray
is IntArray -> ours contentEquals theirs as IntArray
is FloatArray -> ours contentEquals theirs as FloatArray
is LongArray -> ours contentEquals theirs as LongArray
is DoubleArray -> ours contentEquals theirs as DoubleArray
is Array<*> -> ours contentEquals theirs as Array<*>
else -> ours == theirs
}
}
val hashCode by lazy {
values.entries.sumOf { entry ->
val (key, value) = entry
val valueHash = when (value) {
is BooleanArray -> value.contentHashCode()
is CharArray -> value.contentHashCode()
is ByteArray -> value.contentHashCode()
is ShortArray -> value.contentHashCode()
is IntArray -> value.contentHashCode()
is FloatArray -> value.contentHashCode()
is LongArray -> value.contentHashCode()
is DoubleArray -> value.contentHashCode()
is Array<*> -> value.contentHashCode()
else -> value.hashCode()
}
127 * key.hashCode() xor valueHash
}
}
val toString by lazy {
buildString {
append('@')
append(annotationClass.canonicalName)
values.entries.joinTo(this, separator = ", ", prefix = "(", postfix = ")") { entry ->
val (key, value) = entry
val valueString = when (value) {
is BooleanArray -> value.contentToString()
is CharArray -> value.contentToString()
is ByteArray -> value.contentToString()
is ShortArray -> value.contentToString()
is IntArray -> value.contentToString()
is FloatArray -> value.contentToString()
is LongArray -> value.contentToString()
is DoubleArray -> value.contentToString()
is Array<*> -> value.contentToString()
else -> value.toString()
}
"$key=$valueString"
}
}
}
val result = Proxy.newProxyInstance(annotationClass.classLoader, arrayOf(annotationClass)) { _, method, args ->
when (val name = method.name) {
"annotationType" -> annotationClass
"toString" -> toString
"hashCode" -> hashCode
else -> when {
name == "equals" && args?.size == 1 -> equals(args.single())
values.containsKey(name) -> values[name]
else -> throw KotlinReflectionInternalError("Method is not supported: $method (args: ${args.orEmpty().toList()})")
}
}
}
@Suppress("UNCHECKED_CAST")
return result as T
}