com.github.andrewoma.kwery.mapper.Table.kt Maven / Gradle / Ivy
/*
* Copyright (c) 2015 Andrew O'Malley
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/
package com.github.andrewoma.kwery.mapper
import com.github.andrewoma.kommon.collection.hashMapOfExpectedSize
import com.github.andrewoma.kwery.core.Row
import com.github.andrewoma.kwery.core.Session
import com.github.andrewoma.kwery.mapper.util.camelToLowerUnderscore
import java.lang.reflect.ParameterizedType
import java.util.*
import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.*
import kotlin.reflect.jvm.javaType
/**
* Column defines a how to map an SQL column to and from an object property of type `T`
* within a containing class `C`.
*
* While columns can be added directly it is more common to use the `col` methods on `Table`
* to provide sensible defaults.
*/
data class Column(
/**
* A function to extract the property value from the containing object
*/
val property: (C) -> T,
// TODO ... Is this necessary - can it be left to construction?
/**
* If a value is not `nullable` a default value must be provided to allow construction
* of partially selected objects
*/
val defaultValue: T,
/**
* A converter between the SQL type and `T`
*/
val converter: Converter,
/**
* The name of the SQL column
*/
val name: String,
/**
* True if the column is part of the primary key
*/
val id: Boolean,
/**
* True if the column is used for versioning using optimistic locking
*/
val version: Boolean,
/**
* True if the column is selected in queries by default.
* Generally true, but is useful to exclude `BLOBs` and `CLOBs` in some cases.
*/
val selectByDefault: Boolean,
// TODO ... is there any way to detect nullable bound?
/**
* True if the column is nullable
*/
val isNullable: Boolean
) {
/**
* A type-safe variant of `to`
*/
infix fun of(value: T): Pair, T> = Pair(this, value)
/**
* A type-safe variant of `to` with an optional value
*/
@Suppress("BASE_WITH_NULLABLE_UPPER_BOUND")
infix fun optional(value: T?): Pair, T?> = Pair(this, value)
override fun toString(): String {
return "Column($name id=$id version=$version nullable=$isNullable)" // Prevent NPE in debugger on "property"
}
}
/**
* Value allows extraction of column values by column.
*/
interface Value {
infix fun of(column: Column): T
}
/**
* TableConfiguration defines configuration common to a set of tables.
*/
class TableConfiguration(
/**
* Defines default values for types when the column is not null, but is not selected.
* Defaults to `standardDefaults`
*/
val defaults: Map = standardDefaults + timeDefaults,
/**
* Defines converters from JDBC types to arbitrary Kotlin types.
* Defaults to `standardConverters` + `timeConverters`
*/
val converters: Map, Converter<*>> = standardConverters + timeConverters,
/**
* Defines the naming convention for converting `Column` names to SQL column names.
* Defaults to `camelToLowerUnderscore`
*/
val namingConvention: (String) -> String = camelToLowerUnderscore)
/**
* A `Table` maps directly to a single SQL table, with each SQL column defined explicitly.
*/
abstract class Table(val name: String, val config: TableConfiguration = TableConfiguration(), val sequence: String? = null) {
val allColumns: Set> = LinkedHashSet()
val defaultColumns: Set> by lazy { LinkedHashSet(allColumns.filter { it.selectByDefault }) }
val idColumns: Set> by lazy { LinkedHashSet(allColumns.filter { it.id }) }
val dataColumns: Set> by lazy { LinkedHashSet(allColumns.filterNot { it.id }) }
val versionColumn: Column? by lazy { allColumns.firstOrNull { it.version } }
val type: Class by lazy { lazyType!! }
private val columnName: (Column) -> String = { it.name }
private var initialised = false
private var lazyType: Class? = null
abstract fun create(value: Value): T
abstract fun idColumns(id: ID): Set, *>>
fun addColumn(column: Column): Column {
@Suppress("UNCHECKED_CAST")
(allColumns as MutableSet).add(column)
return column
}
private fun lazy(f: () -> T) = kotlin.lazy(LazyThreadSafetyMode.NONE) { initialise(); f() }
// Indirectly calls "get" on all delegated columns, which then adds them to the "allColumns"
private fun initialise() {
synchronized(this) {
if (initialised) return
val instance: T
try {
instance = create(object : Value {
override fun of(column: Column): R {
return column.defaultValue
}
})
} catch(e: NullPointerException) {
throw RuntimeException("A table field is declared as nullable but the mapped field is non-null?", e)
}
lazyType = instance.javaClass
initialised = true
}
}
// A delegated property that gets the column name from the property name unless it is defined
inner class DelegatedColumn(val template: Column, private var value: Column? = null) : ReadOnlyProperty> {
override fun getValue(thisRef: Any?, property: KProperty<*>): Column {
if (value == null) {
value = addColumn(template.copy(name = template.name.let { if (it != "") it else config.namingConvention(property.name) }))
}
return value!!
}
}
fun col(property: KProperty1,
id: Boolean = false,
version: Boolean = false,
notNull: Boolean = !property.returnType.isMarkedNullable,
default: R = default(property.returnType),
converter: Converter = converter(property.returnType),
name: String? = null,
selectByDefault: Boolean = true): DelegatedColumn {
val column = Column({ property.get(it) }, default, converter, name ?: "", id, version, selectByDefault, !notNull)
return DelegatedColumn(column)
}
fun col(property: KProperty1,
path: (T) -> C,
id: Boolean = false,
version: Boolean = false,
notNull: Boolean = !property.returnType.isMarkedNullable,
default: R = default(property.returnType),
converter: Converter = converter(property.returnType),
name: String? = null,
selectByDefault: Boolean = true
): DelegatedColumn {
val column = Column({ property.get(path(it)) }, default, converter, name ?: "", id, version, selectByDefault, !notNull)
return DelegatedColumn(column)
}
// 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 T 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")
protected fun default(type: KType): T {
if (type.isMarkedNullable) return null as T
val value = config.defaults[type]
if (value == null && type.isErasedType(List::class)) {
return emptyList() as T
}
checkNotNull(value) { "Default value undefined for type $type" }
return value as T
}
private fun KType.isErasedType(clazz: KClass<*>)
= this.javaType is ParameterizedType && (this.javaType as ParameterizedType).rawType == clazz.defaultType.javaType
fun copy(value: T, properties: Map, *>): T {
return create(object : Value {
override fun of(column: Column): R {
@Suppress("UNCHECKED_CAST")
return if (properties.contains(column)) properties[column] as R else column.property(value)
}
})
}
fun objectMap(session: Session, value: T, columns: Set> = defaultColumns, nf: (Column) -> String = columnName): Map {
val map = hashMapOfExpectedSize(columns.size)
for (column in columns) {
@Suppress("UNCHECKED_CAST")
val col = column as Column
map[nf(column)] = col.converter.to(session.connection, column.property(value))
}
return map
}
fun idMap(session: Session, id: ID, nf: (Column) -> String = columnName): Map {
val idCols = idColumns(id)
val map = hashMapOfExpectedSize(idCols.size)
for ((column, value) in idCols) {
@Suppress("UNCHECKED_CAST")
val col = column as Column
map[nf(column)] = col.converter.to(session.connection, value)
}
return map
}
fun rowMapper(columns: Set> = defaultColumns, nf: (Column) -> String = columnName): (Row) -> T {
return { row ->
create(object : Value {
override fun of(column: Column): R {
return if (columns.contains(column)) column.converter.from(row, nf(column)) else column.defaultValue
}
})
}
}
} © 2015 - 2025 Weber Informatics LLC | Privacy Policy