All Downloads are FREE. Search and download functionalities are using the official Maven repository.

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