diff --git a/src/main/kotlin/fr/postgresjson/connexion/Connection.kt b/src/main/kotlin/fr/postgresjson/connexion/Connection.kt index a1b482a..1c33eab 100644 --- a/src/main/kotlin/fr/postgresjson/connexion/Connection.kt +++ b/src/main/kotlin/fr/postgresjson/connexion/Connection.kt @@ -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() - private val functions = mutableMapOf() private lateinit var connection: ConnectionPool private val serializer = Serializer() - init { - if (queriesDirectory === null) { - val resource = this::class.java.getResource("/sql/query") - if (resource !== null) { - fetchQueries(File(resource.toURI())) - } - } else { - fetchQueries(queriesDirectory) + fun connect(): ConnectionPool { + if (!::connection.isInitialized || !connection.isConnected()) { + connection = PostgreSQLConnectionBuilder.createConnectionPool( + "jdbc:postgresql://$host:$port/$database?user=$username&password=$password" + ) } + return connection + } - if (functionsDirectory === null) { - val resource = this::class.java.getResource("/sql/function") - if (resource !== null) { - fetchFunctions(File(resource.toURI())) - } + fun ?> selectOne(sql: String, typeReference: TypeReference, values: List = 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 ?> selectOne(sql: String, values: List = emptyList()): R? = selectOne(sql, object: TypeReference() {}, values) + + fun ?>> select(sql: String, typeReference: TypeReference, values: List = emptyList()): R { + val future = connect().sendPreparedStatement(sql, values) + val json = future.get().rows[0].getString(0) + return if (json === null) { + listOf?>() as R + } else { + serializer.deserializeList(json, typeReference) + } + } + + inline fun ?>> select(sql: String, values: List = emptyList()): R = select(sql, object : TypeReference() {}, values) +} + +class Requester ( + private val connection: Connection, + queries: List = listOf(), + functions: List = listOf()) +{ + private val queries = mutableMapOf() + private val functions = mutableMapOf() + + 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 { + val functionRegex = """create .*(procedure|function) *(?[^(\s]+)\s*\((?(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?RETURNS *[^ ]+)?""" + .toRegex(setOf(IGNORE_CASE, MULTILINE)) + + val paramsRegex = """\s*(?((?IN|OUT|INOUT|VARIADIC)?\s+)?(?[^\s,)]+\s+)?(?[^\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 ?> selectOne(typeReference: TypeReference, values: List = emptyList()): R? { - val args = values.joinToString() - val sql = "SELECT * FROM $name ($args)" + fun ?> selectOne(typeReference: TypeReference, values: List = emptyList()): R? { + val placeholder = List(values.size) {"?"}.joinToString(separator=", ") + val sql = "SELECT * FROM $name (${placeholder})" return connection.selectOne(sql, typeReference, values) } - inline fun ?> selectOne(values: List = emptyList()): R? = selectOne(object: TypeReference() {}, values) + inline fun ?> selectOne(values: List = emptyList()): R? = selectOne(object: TypeReference() {}, values) fun ?>> select(typeReference: TypeReference, values: List = 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 ?>> select(values: List = emptyList()): R? = select(object: TypeReference() {}, values) } +} - fun connect(): ConnectionPool { - if (!::connection.isInitialized || !connection.isConnected()) { - connection = PostgreSQLConnectionBuilder.createConnectionPool( - "jdbc:postgresql://$host:$port/$database?user=$username&password=$password" - ) - } - return connection - } - - fun ?> selectOne(sql: String, typeReference: TypeReference, values: List = emptyList()): R? { - val future = connect().sendPreparedStatement(sql, values) - val json = future.get().rows[0].getString(0) - return if (json === null) { - null - } else { - serializer.deserialize(json, typeReference) - } - } - - inline fun ?> selectOne(sql: String, values: List = emptyList()): R? = selectOne(sql, object: TypeReference() {}, values) - - fun ?>> select(sql: String, typeReference: TypeReference, values: List = emptyList()): R { - val future = connect().sendPreparedStatement(sql, values) - val json = future.get().rows[0].getString(0) - return if (json === null) { - listOf?>() as R - } else { - serializer.deserializeList(json, typeReference) - } - } - - inline fun ?>> select(sql: String, values: List = emptyList()): R = select(sql, object : TypeReference() {}, 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 +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 +) +{ + fun createRequester(): Requester { - val functionRegex = """create .*(procedure|function) *(?[^(\s]+)\s*\((?(\s*((IN|OUT|INOUT|VARIADIC)?\s+)?([^\s,)]+\s+)?([^\s,)]+)(\s+(?:default\s|=)\s*[^\s,)]+)?\s*(,|(?=\))))*)\) *(?RETURNS *[^ ]+)?""" - .toRegex(setOf(IGNORE_CASE, MULTILINE)) + val con = Connection(host = host, port = port, database = database, username = username, password = password) + val req = Requester(con) - val paramsRegex = """\s*(?((?IN|OUT|INOUT|VARIADIC)?\s+)?(?[^\s,)]+\s+)?(?[^\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() - } else { - listOf() + 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())) } - - Function(functionName!!, parameters, this) - }.toList() - } - - fun getFunction(name: String): Function { - if (functions[name] === null) { - throw Exception("No function defined for $name") + } else { + req.addQuery(queriesDirectory) } - return functions[name]!! - } - fun getQuery(path: String): Query { - if (queries[path] === null) { - throw Exception("No query defined for $path") + if (functionsDirectory === null) { + val resource = this::class.java.getResource("/sql/function") + if (resource !== null) { + req.addFunction(File(resource.toURI())) + } + } else { + req.addFunction(functionsDirectory) } - return queries[path]!! + + return req } -} \ No newline at end of file +} diff --git a/src/main/kotlin/fr/postgresjson/repository/Repository.kt b/src/main/kotlin/fr/postgresjson/repository/Repository.kt index 0ca2428..8473c82 100644 --- a/src/main/kotlin/fr/postgresjson/repository/Repository.kt +++ b/src/main/kotlin/fr/postgresjson/repository/Repository.kt @@ -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> { abstract class Repository>(override val entityName: KClass) : RepositoryI { - abstract var connection: Connection + abstract var requester: Requester abstract fun getClassName(): String fun findById(id: T): EntityI? { - val sql = this.connection.getQuery(entityName.toString()) + val sql = requester.getQuery(entityName.toString()) return when (val e = EntityCollection().get(id)) { null -> { // TODO create Request diff --git a/src/test/kotlin/fr/postgresjson/ConnectionTest.kt b/src/test/kotlin/fr/postgresjson/ConnectionTest.kt index ece901e..7f1f19f 100644 --- a/src/test/kotlin/fr/postgresjson/ConnectionTest.kt +++ b/src/test/kotlin/fr/postgresjson/ConnectionTest.kt @@ -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("select to_json(a) from test a limit 1") diff --git a/src/test/kotlin/fr/postgresjson/RequestTest.kt b/src/test/kotlin/fr/postgresjson/RequestTest.kt index 68ad941..efc3e8e 100644 --- a/src/test/kotlin/fr/postgresjson/RequestTest.kt +++ b/src/test/kotlin/fr/postgresjson/RequestTest.kt @@ -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") } } \ No newline at end of file diff --git a/src/test/kotlin/fr/postgresjson/SerializerTest.kt b/src/test/kotlin/fr/postgresjson/SerializerTest.kt index d957375..e8ace3e 100644 --- a/src/test/kotlin/fr/postgresjson/SerializerTest.kt +++ b/src/test/kotlin/fr/postgresjson/SerializerTest.kt @@ -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() diff --git a/src/test/kotlin/fr/postgresjson/TestAbstract.kt b/src/test/kotlin/fr/postgresjson/TestAbstract.kt new file mode 100644 index 0000000..2fbca6b --- /dev/null +++ b/src/test/kotlin/fr/postgresjson/TestAbstract.kt @@ -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() + } +} \ No newline at end of file diff --git a/src/test/resources/fixtures/init.sql b/src/test/resources/fixtures/init.sql index 4b226a3..2e4699f 100644 --- a/src/test/resources/fixtures/init.sql +++ b/src/test/resources/fixtures/init.sql @@ -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; +$$ diff --git a/src/test/resources/sql/function/Test/function_test.sql b/src/test/resources/sql/function/Test/function_test.sql index e79bc63..8e81032 100644 --- a/src/test/resources/sql/function/Test/function_test.sql +++ b/src/test/resources/sql/function/Test/function_test.sql @@ -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; $$ \ No newline at end of file