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

Set<Enum> support #28

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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<R>(
val from: (Row, String) -> R,
Expand Down Expand Up @@ -116,3 +120,32 @@ class EnumByNameConverter<T : Enum<T>>(type: Class<T>) : SimpleConverter<T>(
{ row, c -> java.lang.Enum.valueOf(type, row.string(c)) },
{ it.name }
)

class EnumSetConverter<E : Enum<E>>(
// exposing types intentionally, theoretically `type` may become public property of Converter
val kType: KType,
val eType: Class<E>
): Converter<Set<E>>(
{ 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<E>::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 |." }
}
}

}
33 changes: 29 additions & 4 deletions mapper/src/main/kotlin/com/github/andrewoma/kwery/mapper/Table.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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


/**
Expand Down Expand Up @@ -142,17 +146,38 @@ abstract class Table<T : Any, ID>(val name: String, val config: TableConfigurati
// Can't cast T to Enum<T> 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 <T> converter(type: KType): Converter<T> {
// 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<T>
val converter = config.converters[javaClass] ?: if (javaClass.isEnum) EnumByNameConverter(javaClass as Class<DummyEnum>) as Converter<T> 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 <T> converterForClass(type: KType): Converter<T> {
val javaClass = type.javaType as Class<*>
val converter = config.converters [javaClass]
?: if (javaClass.isEnum) EnumByNameConverter(javaClass as Class<DummyEnum>) as Converter<T> else null

checkNotNull(converter) { "Converter undefined for type $type as $javaClass" }
return (if (type.isMarkedNullable) optional(converter!! as Converter<Any>) else converter) as Converter<T>
}

@Suppress("UNCHECKED_CAST")
private fun <T> converterForParameterized(type: KType): Converter<T> = 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<DummyEnum>) as Converter<T>
else error("Sets of $e are not supported.")
}
else -> error("Parameterized type ${type.javaType} is not supported.")
}

@Suppress("UNCHECKED_CAST")
protected fun <T> default(type: KType): T {
if (type.isMarkedNullable) return null as T
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Thread.State> = EnumSet.noneOf(Thread.State::class.java)

@Test
fun enumSetConverterTest() {
val converter = TestTable.testConverter<Set<Thread.State>>(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<Any, Nothing?>("unused") {
override fun idColumns(id: Nothing?): Set<Pair<Column<Any, *>, *>> = error("unused")
override fun create(value: Value<Any>): Any = error("unused")
fun <T> testConverter(type: KType) = converter<T>(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
}

}