refactoring: move Query/Function logic into Requester class

This commit is contained in:
2019-06-14 17:09:52 +02:00
parent baa2976e80
commit 00d2fa335d
8 changed files with 235 additions and 160 deletions

View File

@@ -11,39 +11,148 @@ import kotlin.text.RegexOption.IGNORE_CASE
import kotlin.text.RegexOption.MULTILINE
class Connection(
private val database: String,
private val username: String,
private val password: String,
private val host: String = "localhost",
private val port: Int = 5432,
private val database: String = "dc-project",
private val username: String = "dc-project",
private val password: String = "dc-project",
queriesDirectory: File? = null,
functionsDirectory: File? = null
private val port: Int = 5432
) {
private val queries = mutableMapOf<String, Query>()
private val functions = mutableMapOf<String, Function>()
private lateinit var connection: ConnectionPool<PostgreSQLConnection>
private val serializer = Serializer()
init {
if (queriesDirectory === null) {
val resource = this::class.java.getResource("/sql/query")
if (resource !== null) {
fetchQueries(File(resource.toURI()))
fun connect(): ConnectionPool<PostgreSQLConnection> {
if (!::connection.isInitialized || !connection.isConnected()) {
connection = PostgreSQLConnectionBuilder.createConnectionPool(
"jdbc:postgresql://$host:$port/$database?user=$username&password=$password"
)
}
} else {
fetchQueries(queriesDirectory)
return connection
}
if (functionsDirectory === null) {
val resource = this::class.java.getResource("/sql/function")
if (resource !== null) {
fetchFunctions(File(resource.toURI()))
}
fun <T, R : EntityI<T?>?> selectOne(sql: String, typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
val future = connect().sendPreparedStatement(sql, values)
val json = future.get().rows[0].getString(0)
return if (json === null) {
null
} else {
fetchFunctions(functionsDirectory)
serializer.deserialize(json, typeReference)
}
}
inline fun <T, reified R : EntityI<T?>?> selectOne(sql: String, values: List<Any?> = emptyList()): R? = selectOne(sql, object: TypeReference<R>() {}, values)
fun <T, R : List<EntityI<T?>?>> select(sql: String, typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R {
val future = connect().sendPreparedStatement(sql, values)
val json = future.get().rows[0].getString(0)
return if (json === null) {
listOf<EntityI<T?>?>() as R
} else {
serializer.deserializeList(json, typeReference)
}
}
inline fun <T, reified R : List<EntityI<T?>?>> select(sql: String, values: List<Any?> = emptyList()): R = select(sql, object : TypeReference<R>() {}, values)
}
class Requester (
private val connection: Connection,
queries: List<Query> = listOf(),
functions: List<Function> = listOf())
{
private val queries = mutableMapOf<String, Query>()
private val functions = mutableMapOf<String, Function>()
fun addQuery(name: String, query: Query): Requester {
queries[name] = query
return this
}
fun addQuery(name: String, sql: String): Requester {
queries[name] = Query(sql, connection)
return this
}
fun addQuery(queriesDirectory: File): Requester {
queriesDirectory.walk().filter { it.isDirectory }.forEach { directory ->
val path = directory.name
directory.walk().filter { it.isFile }.forEach { file ->
val sql = file.readText()
val fullpath = "$path/${file.nameWithoutExtension}"
queries[fullpath] = Query(sql, connection)
}
}
return this
}
fun addFunction(function: Function): Requester {
functions[function.name] = function
return this
}
fun addFunction(sql: String): Requester {
getDefinitions(sql).forEach {
functions[it.name] = it
}
return this
}
fun addFunction(functionsDirectory: File): Requester {
functionsDirectory.walk().filter {
it.isDirectory
}.forEach { directory ->
directory.walk().filter {
it.isFile
}.forEach { file ->
val fileContent = file.readText()
addFunction(fileContent)
}
}
return this
}
private fun getDefinitions(functionContent: String): List<Function> {
val functionRegex = """create .*(procedure|function) *(?<name>[^(\s]+)\s*\((?<params>(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?<return>RETURNS *[^ ]+)?"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
val paramsRegex = """\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?(?<name>[^\s,)]+\s+)?(?<type>[^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?)\s*(,|$)"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
return functionRegex.findAll(functionContent).map { queryMatch ->
val functionName = queryMatch.groups["name"]?.value?.trim()
val functionParameters = queryMatch.groups["params"]?.value?.trim()
val returns = queryMatch.groups["return"]?.value?.trim()
/* Create parameters definition */
val parameters = if (functionParameters !== null) {
val matchesParams = paramsRegex.findAll(functionParameters)
matchesParams.map { paramsMatch ->
Function.Parameter(
paramsMatch.groups["name"]!!.value.trim(),
paramsMatch.groups["type"]!!.value.trim(),
paramsMatch.groups["direction"]?.value?.trim())
}.toList()
} else {
listOf()
}
Function(functionName!!, parameters, connection)
}.toList()
}
fun getFunction(name: String): Function {
if (functions[name] === null) {
throw Exception("No function defined for $name")
}
return functions[name]!!
}
fun getQuery(path: String): Query {
if (queries[path] === null) {
throw Exception("No query defined for $path")
}
return queries[path]!!
}
class Query(private val sql: String, private val connection : Connection) {
override fun toString(): String {
return sql
@@ -87,125 +196,64 @@ class Connection(
return name
}
fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
val args = values.joinToString()
val sql = "SELECT * FROM $name ($args)"
fun <T, R : EntityI<T?>?> selectOne(typeReference: TypeReference<R>, values: List<String?> = emptyList()): R? {
val placeholder = List(values.size) {"?"}.joinToString(separator=", ")
val sql = "SELECT * FROM $name (${placeholder})"
return connection.selectOne(sql, typeReference, values)
}
inline fun <T, reified R: EntityI<T?>?> selectOne(values: List<Any?> = emptyList()): R? = selectOne(object: TypeReference<R>() {}, values)
inline fun <T, reified R: EntityI<T?>?> selectOne(values: List<String?> = emptyList()): R? = selectOne(object: TypeReference<R>() {}, values)
fun <T, R : List<EntityI<T?>?>> select(typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
val args = values.joinToString()
val sql = "SELECT * FROM $name ($args)"
val placeholder = List(values.size) {"?"}.joinToString(separator=", ")
val sql = "SELECT * FROM $name ($placeholder)"
return connection.select(sql, typeReference, values)
}
inline fun <T, reified R: List<EntityI<T?>?>> select(values: List<Any?> = emptyList()): R? = select(object: TypeReference<R>() {}, values)
}
}
fun connect(): ConnectionPool<PostgreSQLConnection> {
if (!::connection.isInitialized || !connection.isConnected()) {
connection = PostgreSQLConnectionBuilder.createConnectionPool(
"jdbc:postgresql://$host:$port/$database?user=$username&password=$password"
class RequesterFactory(
private val host: String = "localhost",
private val port: Int = 5432,
private val database: String = "dc-project",
private val username: String = "dc-project",
private val password: String = "dc-project",
private val queriesDirectory: File? = null,
private val functionsDirectory: File? = null
)
}
return connection
}
fun <T, R : EntityI<T?>?> selectOne(sql: String, typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R? {
val future = connect().sendPreparedStatement(sql, values)
val json = future.get().rows[0].getString(0)
return if (json === null) {
null
} else {
serializer.deserialize<T, R>(json, typeReference)
}
}
inline fun <T, reified R : EntityI<T?>?> selectOne(sql: String, values: List<Any?> = emptyList()): R? = selectOne(sql, object: TypeReference<R>() {}, values)
fun <T, R : List<EntityI<T?>?>> select(sql: String, typeReference: TypeReference<R>, values: List<Any?> = emptyList()): R {
val future = connect().sendPreparedStatement(sql, values)
val json = future.get().rows[0].getString(0)
return if (json === null) {
listOf<EntityI<T?>?>() as R
} else {
serializer.deserializeList(json, typeReference)
}
}
inline fun <T, reified R : List<EntityI<T?>?>> select(sql: String, values: List<Any?> = emptyList()): R = select(sql, object : TypeReference<R>() {}, values)
private fun fetchQueries(queriesDirectory: File) {
queriesDirectory.walk().filter { it.isDirectory }.forEach { directory ->
val path = directory.name
directory.walk().filter { it.isFile }.forEach { file ->
val sql = file.readText()
val fullpath = "$path/${file.nameWithoutExtension}"
queries[fullpath] = Query(sql, this)
}
}
}
private fun fetchFunctions(functionsDirectory: File) {
functionsDirectory.walk().filter {
it.isDirectory
}.forEach { directory ->
directory.walk().filter {
it.isFile
}.forEach { file ->
val fileContent = file.readText()
getDefinitions(fileContent).forEach {
functions[it.name] = it
}
}
}
}
private fun getDefinitions(functionContent: String): List<Function>
{
val functionRegex = """create .*(procedure|function) *(?<name>[^(\s]+)\s*\((?<params>(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?<return>RETURNS *[^ ]+)?"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
fun createRequester(): Requester
{
val con = Connection(host = host, port = port, database = database, username = username, password = password)
val req = Requester(con)
val paramsRegex = """\s*(?<param>((?<direction>IN|OUT|INOUT|VARIADIC)?\s+)?(?<name>[^\s,)]+\s+)?(?<type>[^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?)\s*(,|$)"""
.toRegex(setOf(IGNORE_CASE, MULTILINE))
return initRequester(req)
}
return functionRegex.findAll(functionContent).map { queryMatch ->
val functionName = queryMatch.groups["name"]?.value?.trim()
val functionParameters = queryMatch.groups["params"]?.value?.trim()
val returns = queryMatch.groups["return"]?.value?.trim()
/* Create parameters definition */
val parameters = if (functionParameters !== null) {
val matchesParams = paramsRegex.findAll(functionParameters)
matchesParams.map { paramsMatch ->
Function.Parameter(
paramsMatch.groups["name"]!!.value.trim(),
paramsMatch.groups["type"]!!.value.trim(),
paramsMatch.groups["direction"]?.value?.trim())
}.toList()
private fun initRequester(req: Requester): Requester
{
if (queriesDirectory === null) {
val resource = this::class.java.getResource("/sql/query")
if (resource !== null) {
req.addQuery(File(resource.toURI()))
}
} else {
listOf()
req.addQuery(queriesDirectory)
}
Function(functionName!!, parameters, this)
}.toList()
if (functionsDirectory === null) {
val resource = this::class.java.getResource("/sql/function")
if (resource !== null) {
req.addFunction(File(resource.toURI()))
}
} else {
req.addFunction(functionsDirectory)
}
fun getFunction(name: String): Function {
if (functions[name] === null) {
throw Exception("No function defined for $name")
}
return functions[name]!!
}
fun getQuery(path: String): Query {
if (queries[path] === null) {
throw Exception("No query defined for $path")
}
return queries[path]!!
return req
}
}

View File

@@ -1,7 +1,7 @@
package fr.postgresjson.repository
import fr.postgresjson.Serializer
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.EntityCollection
import fr.postgresjson.entity.EntityI
import kotlin.reflect.KClass
@@ -12,11 +12,11 @@ interface RepositoryI<T, E : EntityI<T?>> {
abstract class Repository<T, E : EntityI<T?>>(override val entityName: KClass<E>) : RepositoryI<T, E> {
abstract var connection: Connection
abstract var requester: Requester
abstract fun getClassName(): String
fun <T> findById(id: T): EntityI<T?>? {
val sql = this.connection.getQuery(entityName.toString())
val sql = requester.getQuery(entityName.toString())
return when (val e = EntityCollection().get(id)) {
null -> {
// TODO create Request

View File

@@ -2,39 +2,23 @@ package fr.postgresjson
import fr.postgresjson.connexion.Connection
import fr.postgresjson.entity.IdEntity
import org.junit.jupiter.api.*
import org.junit.jupiter.api.Assertions.assertTrue
import java.io.File
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class ConnectionTest() {
class ConnectionTest(): TestAbstract() {
private class ObjTest(var name: String): IdEntity()
private class ObjTest2(var title: String, var test: ObjTest?): IdEntity()
private lateinit var connection: Connection
fun getConnextion(): Connection {
return Connection(database = "test", username = "test", password = "test")
}
@BeforeAll
fun beforeAll() {
val initSQL = File(this::class.java.getResource("/fixtures/init.sql").toURI())
val promise = getConnextion().connect().sendQuery(initSQL.readText())
promise.join()
}
@BeforeEach
fun before() {
connection = getConnextion()
}
@AfterAll
fun afterAll() {
val downSQL = File(this::class.java.getResource("/fixtures/down.sql").toURI())
getConnextion().connect().sendQuery(downSQL.readText()).join()
}
@Test
fun getObject() {
val obj: ObjTest? = connection.selectOne<Int?, ObjTest>("select to_json(a) from test a limit 1")

View File

@@ -1,27 +1,33 @@
package fr.postgresjson
import fr.postgresjson.connexion.Connection
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.IdEntity
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.io.File
class RequestTest {
class RequestTest: TestAbstract() {
class ObjTest(var name:String): IdEntity(1)
@Test
fun getRequestFromFile() {
fun getQueryFromFile() {
val resources = File(this::class.java.getResource("/sql/query").toURI())
val objTest: ObjTest? = Connection(queriesDirectory = resources).getQuery("Test/selectOne").selectOne()
assertTrue(objTest!!.id == 2)
assertTrue(objTest.name == "test")
val objTest: ObjTest? = Requester(getConnextion())
.addQuery(resources)
.getQuery("Test/selectOne")
.selectOne()
assertEquals(objTest!!.id, 2)
assertEquals(objTest.name, "test")
}
@Test
fun getRequestFromFunction() {
fun getFunctionFromFile() {
val resources = File(this::class.java.getResource("/sql/function").toURI())
val objTest: ObjTest? = Connection(functionsDirectory = resources).getFunction("test_function").selectOne()
assertTrue(objTest!!.id == 2)
assertTrue(objTest.name == "test")
val objTest: ObjTest? = Requester(getConnextion())
.addFunction(resources)
.getFunction("test_function")
.selectOne(listOf("ploop", "plip"))
assertEquals(objTest!!.id, 3)
assertEquals(objTest.name, "test")
}
}

View File

@@ -8,7 +8,7 @@ import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
internal class SerializerTest {
internal class SerializerTest: TestAbstract() {
private class ObjTest(var val1: String, var val2: Int) : IdEntity(1)
private val serializer = Serializer()

View File

@@ -0,0 +1,28 @@
package fr.postgresjson
import fr.postgresjson.connexion.Connection
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.TestInstance.Lifecycle.PER_CLASS
import java.io.File
@TestInstance(PER_CLASS)
abstract class TestAbstract {
protected fun getConnextion(): Connection {
return Connection(database = "test", username = "test", password = "test")
}
@BeforeAll
fun beforeAll() {
val initSQL = File(this::class.java.getResource("/fixtures/init.sql").toURI())
val promise = getConnextion().connect().sendQuery(initSQL.readText())
promise.join()
}
@AfterAll
fun afterAll() {
val downSQL = File(this::class.java.getResource("/fixtures/down.sql").toURI())
getConnextion().connect().sendQuery(downSQL.readText()).join()
}
}

View File

@@ -24,3 +24,12 @@ INSERT INTO test (id, name) VALUES (1, 'plop') ON CONFLICT DO NOTHING;
INSERT INTO test2 (id, title, test_id) VALUES (1, 'plop', 1) ON CONFLICT DO NOTHING;
INSERT INTO test2 (id, title, test_id) VALUES (2, 'plip', 1) ON CONFLICT DO NOTHING;
INSERT INTO test2 (id, title, test_id) VALUES (3, 'ttt', null) ON CONFLICT DO NOTHING;
CREATE OR REPLACE FUNCTION test_function (name text default 'plop', IN hi text default 'hello', out result json)
LANGUAGE plpgsql
AS
$$
BEGIN
result = json_build_object('id', 3, 'name', 'test');
END;
$$

View File

@@ -1,8 +1,8 @@
CREATE OR REPLACE FUNCTION test_function (name text, IN hi text default 'hello', out result json)
CREATE OR REPLACE FUNCTION test_function (name text default 'plop', IN hi text default 'hello', out result json)
LANGUAGE plpgsql
AS
$$
BEGIN
result = json_build_object('id', 2, 'name', 'test');
result = json_build_object('id', 3, 'name', 'test');
END;
$$