diff --git a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Converters.kt b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Converters.kt index c7aae6c..c9c86db 100644 --- a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Converters.kt +++ b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Converters.kt @@ -23,11 +23,15 @@ package com.github.andrewoma.kwery.mapper import com.github.andrewoma.kwery.core.Row +import java.lang.reflect.ParameterizedType import java.math.BigDecimal import java.sql.Connection import java.sql.Date import java.sql.Time import java.sql.Timestamp +import java.util.* +import kotlin.reflect.KType +import kotlin.reflect.jvm.javaType open class Converter( val from: (Row, String) -> R, @@ -116,3 +120,32 @@ class EnumByNameConverter>(type: Class) : SimpleConverter( { row, c -> java.lang.Enum.valueOf(type, row.string(c)) }, { it.name } ) + +class EnumSetConverter>( + // exposing types intentionally, theoretically `type` may become public property of Converter + val kType: KType, + val eType: Class +): Converter>( + { row, s -> + val values = row.string(s) + if (values.isEmpty()) emptySet() + else values.split('|').mapTo(EnumSet.noneOf(eType)) { java.lang.Enum.valueOf(eType, it) } + }, + { _, set -> set.joinToString("|", transform = Enum::name) } +) { + + val type = kType.javaType + + init { + check(type === kType.javaType) + check(type is ParameterizedType) + type as ParameterizedType + check(type.rawType === Set::class.java) + check(type.actualTypeArguments[0] is Class<*>) + check(Enum::class.java.isAssignableFrom(type.actualTypeArguments[0] as Class<*>)) + eType.enumConstants.forEach { + check(!it.name.contains('|')) { "Hope this won't happen. Enum constant name contains |." } + } + } + +} diff --git a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Table.kt b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Table.kt index efc2e5d..6a48d22 100644 --- a/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Table.kt +++ b/mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Table.kt @@ -28,9 +28,13 @@ import com.github.andrewoma.kwery.core.Session import java.lang.reflect.ParameterizedType import java.util.* import kotlin.properties.ReadOnlyProperty -import kotlin.reflect.* +import kotlin.reflect.KClass +import kotlin.reflect.KProperty +import kotlin.reflect.KProperty1 +import kotlin.reflect.KType import kotlin.reflect.full.defaultType import kotlin.reflect.jvm.javaType +import kotlin.reflect.jvm.jvmErasure /** @@ -142,17 +146,38 @@ abstract class Table(val name: String, val config: TableConfigurati // Can't cast T to Enum due to recursive type, so cast to any enum to satisfy compiler private enum class DummyEnum - @Suppress("UNCHECKED_CAST", "IMPLICIT_CAST_TO_UNIT_OR_ANY", "CAST_NEVER_SUCCEEDS") protected fun converter(type: KType): Converter { // TODO ... converters are currently defined as Java classes as I can't figure out how to // convert a nullable KType into its non-nullable equivalent // Try udalov's workaround: (t.javaType as Class<*>).kotlin.defaultType` - val javaClass = type.javaType as Class - val converter = config.converters[javaClass] ?: if (javaClass.isEnum) EnumByNameConverter(javaClass as Class) as Converter else null + val javaType = type.javaType + return when (javaType) { + is Class<*> -> converterForClass(type) + is ParameterizedType -> converterForParameterized(type) + else -> error("Type $javaType is not supported.") + } + } + + @Suppress("UNCHECKED_CAST") + private fun converterForClass(type: KType): Converter { + val javaClass = type.javaType as Class<*> + val converter = config.converters [javaClass] + ?: if (javaClass.isEnum) EnumByNameConverter(javaClass as Class) as Converter else null + checkNotNull(converter) { "Converter undefined for type $type as $javaClass" } return (if (type.isMarkedNullable) optional(converter!! as Converter) else converter) as Converter } + @Suppress("UNCHECKED_CAST") + private fun converterForParameterized(type: KType): Converter = when (type.jvmErasure.java) { + Set::class.java -> { + val e = (type.javaType as ParameterizedType).actualTypeArguments[0] + if (e is Class<*> && e.isEnum) EnumSetConverter(type, e as Class) as Converter + else error("Sets of $e are not supported.") + } + else -> error("Parameterized type ${type.javaType} is not supported.") + } + @Suppress("UNCHECKED_CAST") protected fun default(type: KType): T { if (type.isMarkedNullable) return null as T diff --git a/mapper/src/test/kotlin/com/github/andrewoma/kwery/mappertest/ConverterTest.kt b/mapper/src/test/kotlin/com/github/andrewoma/kwery/mappertest/ConverterTest.kt new file mode 100644 index 0000000..32c67f0 --- /dev/null +++ b/mapper/src/test/kotlin/com/github/andrewoma/kwery/mappertest/ConverterTest.kt @@ -0,0 +1,51 @@ +package com.github.andrewoma.kwery.mappertest + +import com.github.andrewoma.kwery.core.Row +import com.github.andrewoma.kwery.mapper.Column +import com.github.andrewoma.kwery.mapper.Table +import com.github.andrewoma.kwery.mapper.Value +import org.junit.Test +import java.lang.reflect.InvocationHandler +import java.lang.reflect.Proxy +import java.sql.Connection +import java.sql.ResultSet +import java.util.* +import kotlin.reflect.KType +import kotlin.test.assertEquals + + +class ConverterTest { + + private val ih = InvocationHandler { _, _, _ -> error("unused") } + private val enumSetProp: Set = EnumSet.noneOf(Thread.State::class.java) + + @Test + fun enumSetConverterTest() { + val converter = TestTable.testConverter>(this::enumSetProp.returnType) + val connection = Proxy.newProxyInstance(javaClass.classLoader, arrayOf(Connection::class.java), ih) as Connection + + val empty = converter.to(connection, EnumSet.noneOf(Thread.State::class.java)) + assertEquals("", empty) + assertEquals(emptySet(), converter.from(Row(StringResultSet("")), "")) + + val single = converter.to(connection, EnumSet.of(Thread.State.RUNNABLE)) + assertEquals("RUNNABLE", single) + assertEquals(setOf(Thread.State.RUNNABLE), converter.from(Row(StringResultSet("RUNNABLE")), "")) + + val several = converter.to(connection, EnumSet.of(Thread.State.RUNNABLE, Thread.State.WAITING)) + assertEquals("RUNNABLE|WAITING", several) + assertEquals(setOf(Thread.State.RUNNABLE, Thread.State.WAITING), converter.from(Row(StringResultSet("RUNNABLE|WAITING")), "")) + } + + private object TestTable : Table("unused") { + override fun idColumns(id: Nothing?): Set, *>> = error("unused") + override fun create(value: Value): Any = error("unused") + fun testConverter(type: KType) = converter(type) + } + + private inner class StringResultSet(private val value: String) : + ResultSet by Proxy.newProxyInstance(StringResultSet::class.java.classLoader, arrayOf(ResultSet::class.java), ih) as ResultSet { + override fun getString(columnLabel: String?): String = value + } + +}