From 73a4953ae95860f2115e9aeca546fdc8b9087975 Mon Sep 17 00:00:00 2001 From: Dominic Fox <40790090+distributedleetravis@users.noreply.github.com> Date: Fri, 19 Oct 2018 15:53:47 +0100 Subject: [PATCH] CORDA-2099: Define TypeIdentifier (#4081) * Corda-2099: Define TypeIdentifier * Comments, naming and formatting tweaks --- .../internal/model/TypeIdentifier.kt | 145 ++++++++++++++++++ .../internal/model/TypeIdentifierTests.kt | 53 +++++++ 2 files changed, 198 insertions(+) create mode 100644 serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt create mode 100644 serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt diff --git a/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt new file mode 100644 index 0000000000..3e4816b0c6 --- /dev/null +++ b/serialization/src/main/kotlin/net/corda/serialization/internal/model/TypeIdentifier.kt @@ -0,0 +1,145 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import java.lang.reflect.* + +/** + * Used as a key for retrieving cached type information. We need slightly more information than the bare classname, + * and slightly less information than is captured by Java's [Type] (We drop type variance because in practice we resolve + * wildcards to their upper bounds, e.g. `? extends Foo` to `Foo`). We also need an identifier we can use even when the + * identified type is not visible from the current classloader. + * + * These identifiers act as the anchor for comparison between remote type information (prior to matching it to an actual + * local class) and local type information. + * + * [TypeIdentifier] provides a family of type identifiers, together with a [prettyPrint] method for displaying them. + */ +sealed class TypeIdentifier { + + /** + * The name of the type. + */ + abstract val name: String + + /** + * Obtain a nicely-formatted representation of the identified type, for help with debugging. + */ + fun prettyPrint(simplifyClassNames: Boolean = true): String = when(this) { + is TypeIdentifier.Unknown -> "?" + is TypeIdentifier.Top -> "*" + is TypeIdentifier.Unparameterised -> name.simplifyClassNameIfRequired(simplifyClassNames) + is TypeIdentifier.Erased -> "${name.simplifyClassNameIfRequired(simplifyClassNames)} (erased)" + is TypeIdentifier.ArrayOf -> "${componentType.prettyPrint()}[]" + is TypeIdentifier.Parameterised -> + name.simplifyClassNameIfRequired(simplifyClassNames) + parameters.joinToString(", ", "<", ">") { + it.prettyPrint() + } + } + + private fun String.simplifyClassNameIfRequired(simplifyClassNames: Boolean): String = + if (simplifyClassNames) split(".", "$").last() else this + + companion object { + /** + * Obtain the [TypeIdentifier] for an erased Java class. + * + * @param type The class to get a [TypeIdentifier] for. + */ + fun forClass(type: Class<*>): TypeIdentifier = when { + type.name == "java.lang.Object" -> Top + type.isArray -> ArrayOf(forClass(type.componentType)) + type.typeParameters.isEmpty() -> Unparameterised(type.name) + else -> Erased(type.name) + } + + /** + * Obtain the [TypeIdentifier] for a Java [Type] (typically obtained by calling one of + * [java.lang.reflect.Parameter.getAnnotatedType], + * [java.lang.reflect.Field.getGenericType] or + * [java.lang.reflect.Method.getGenericReturnType]). Wildcard types and type variables are converted to [Unknown]. + * + * @param type The [Type] to obtain a [TypeIdentifier] for. + * @param resolutionContext Optionally, a [Type] which can be used to resolve type variables, for example a + * class implementing a parameterised interface and specifying values for type variables which are referred to + * by methods defined in the interface. + */ + fun forGenericType(type: Type, resolutionContext: Type = type): TypeIdentifier = when(type) { + is ParameterizedType -> Parameterised((type.rawType as Class<*>).name, type.actualTypeArguments.map { + forGenericType(it.resolveAgainst(resolutionContext)) + }) + is Class<*> -> forClass(type) + is GenericArrayType -> ArrayOf(forGenericType(type.genericComponentType.resolveAgainst(resolutionContext))) + else -> Unknown + } + } + + /** + * The [TypeIdentifier] of [Any] / [java.lang.Object]. + */ + object Top : TypeIdentifier() { + override val name get() = "*" + override fun toString() = "Top" + } + + /** + * The [TypeIdentifier] of an unbounded wildcard. + */ + object Unknown : TypeIdentifier() { + override val name get() = "?" + override fun toString() = "Unknown" + } + + /** + * Identifies a class with no type parameters. + */ + data class Unparameterised(override val name: String) : TypeIdentifier() { + override fun toString() = "Unparameterised($name)" + } + + /** + * Identifies a parameterised class such as List, for which we cannot obtain the type parameters at runtime + * because they have been erased. + */ + data class Erased(override val name: String) : TypeIdentifier() { + override fun toString() = "Erased($name)" + } + + /** + * Identifies a type which is an array of some other type. + * + * @param componentType The [TypeIdentifier] of the component type of this array. + */ + data class ArrayOf(val componentType: TypeIdentifier) : TypeIdentifier() { + override val name get() = componentType.name + "[]" + override fun toString() = "ArrayOf(${componentType.prettyPrint()})" + } + + /** + * A parameterised class such as Map for which we have resolved type parameter values. + * + * @param parameters [TypeIdentifier]s for each of the resolved type parameter values of this type. + */ + data class Parameterised(override val name: String, val parameters: List) : TypeIdentifier() { + override fun toString() = "Parameterised(${prettyPrint()})" + } +} + +internal fun Type.resolveAgainst(context: Type): Type = when (this) { + is WildcardType -> this.upperBound + is ParameterizedType, + is TypeVariable<*> -> TypeToken.of(context).resolveType(this).type.upperBound + else -> this +} + +private val Type.upperBound: Type + get() = when (this) { + is TypeVariable<*> -> when { + this.bounds.isEmpty() || this.bounds.size > 1 -> this + else -> this.bounds[0] + } + is WildcardType -> when { + this.upperBounds.isEmpty() || this.upperBounds.size > 1 -> this + else -> this.upperBounds[0] + } + else -> this + } \ No newline at end of file diff --git a/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt new file mode 100644 index 0000000000..f07f88526a --- /dev/null +++ b/serialization/src/test/kotlin/net/corda/serialization/internal/model/TypeIdentifierTests.kt @@ -0,0 +1,53 @@ +package net.corda.serialization.internal.model + +import com.google.common.reflect.TypeToken +import net.corda.serialization.internal.model.TypeIdentifier.* +import org.junit.Test +import java.lang.reflect.Type +import kotlin.test.assertEquals + +class TypeIdentifierTests { + + @Test + fun `primitive types and arrays`() { + assertIdentified(Int::class.javaPrimitiveType!!, "int") + assertIdentified("Integer") + assertIdentified("int[]") + assertIdentified>("Integer[]") + } + + @Test + fun `erased and unerased`() { + assertIdentified(List::class.java, "List (erased)") + assertIdentified>("List") + } + + @Test + fun `nested parameterised`() { + assertIdentified>>("List>") + } + + interface HasArray { + val array: Array> + } + + class HasStringArray(override val array: Array>): HasArray + + @Test + fun `resolved against an owning type`() { + val fieldType = HasArray::class.java.getDeclaredMethod("getArray").genericReturnType + assertIdentified(fieldType, "List<*>[]") + + assertEquals( + "List[]", + TypeIdentifier.forGenericType(fieldType, HasStringArray::class.java).prettyPrint()) + } + + private fun assertIdentified(type: Type, expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(type).prettyPrint()) + + private inline fun assertIdentified(expected: String) = + assertEquals(expected, TypeIdentifier.forGenericType(typeOf()).prettyPrint()) + + private inline fun typeOf() = object : TypeToken() {}.type +} \ No newline at end of file