Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.smeup.dbnative

/**
* Resolves the most specific [ConnectionConfig] matching [fileName].
*/
fun findConnectionConfigFor(fileName: String, connectionsConfig: List<ConnectionConfig>): ConnectionConfig {
val configList = connectionsConfig.filter {
it.fileName.equals(fileName, ignoreCase = true) || it.fileName == "*" ||
fileName.uppercase().matches(Regex(it.fileName.uppercase().replace("*", ".*")))
}
require(configList.isNotEmpty()) {
"Wrong configuration. Not found a ConnectionConfig entry matching name: $fileName"
}
return configList.sortedWith(ConnectionConfigComparator())[0]
}

/**
* Orders connection patterns from most specific to least specific.
*/
class ConnectionConfigComparator : Comparator<ConnectionConfig> {
override fun compare(o1: ConnectionConfig?, o2: ConnectionConfig?): Int {
require(o1 != null)
require(o2 != null)
return when {
o1.fileName == "*" && o2.fileName != "*" -> 1
o1.fileName != "*" && o2.fileName == "*" -> -1
o1.fileName.contains("*") && o2.fileName.contains("*") -> o1.fileName.compareTo(o2.fileName)
o1.fileName.contains("*") && !o2.fileName.contains("*") -> 1
!o1.fileName.contains("*") && o2.fileName.contains("*") -> -1
else -> o1.fileName.compareTo(o2.fileName)
}
}
}
75 changes: 75 additions & 0 deletions base/src/main/kotlin/com/smeup/dbnative/ConnectionProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package com.smeup.dbnative

/**
* Provides thread-scoped access to [DBMManager] instances.
*/
object ConnectionProvider {

private val threadLocal = ThreadLocal<MutableMap<ConnectionConfig, DBMManager>>()

@Volatile private var config: DBNativeAccessConfig? = null
@Volatile private var managerFactory: ((ConnectionConfig) -> DBMManager)? = null

/**
* Configures the provider with connection matching rules and a manager factory.
*/
fun configure(config: DBNativeAccessConfig, factory: (ConnectionConfig) -> DBMManager) {
this.config = config
this.managerFactory = factory
}

/**
* Returns `true` if [configure] (or [configureWithPool]) has been called.
*/
fun isConfigured(): Boolean = config != null

/**
* Functional interface used by [withScope] to execute a scoped block.
*/
fun interface ScopedBlock {
@Throws(Exception::class)
fun execute()
}

/**
* Runs [block] in a thread-local scope and closes all managers created in that scope.
*/
@Throws(Exception::class)
fun withScope(block: ScopedBlock) {
requireNotNull(config) { "ConnectionProvider not configured" }
threadLocal.set(mutableMapOf())
try {
block.execute()
} finally {
val managers = threadLocal.get()
threadLocal.remove()
managers?.values?.forEach { it.close() }
}
}

/**
* Returns the current scope manager for [fileName], creating it on first use.
*/
fun currentManager(fileName: String): DBMManager {
val managers = requireNotNull(threadLocal.get()) { "No active scope on this thread" }
val cfg = requireNotNull(config)
val factory = requireNotNull(managerFactory)
val connectionConfig = findConnectionConfigFor(fileName, cfg.connectionsConfig)
return managers.getOrPut(connectionConfig) { factory(connectionConfig) }
}

/**
* Like [currentManager], but returns `null` if there is no active scope or no match.
*/
fun currentManagerOrNull(fileName: String): DBMManager? {
val managers = threadLocal.get() ?: return null
val cfg = config ?: return null
val factory = managerFactory ?: return null
return try {
val connectionConfig = findConnectionConfigFor(fileName, cfg.connectionsConfig)
managers.getOrPut(connectionConfig) { factory(connectionConfig) }
} catch (e: IllegalArgumentException) {
null
}
}
}
32 changes: 21 additions & 11 deletions base/src/main/kotlin/com/smeup/dbnative/DBNativeAccessConfig.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,37 @@ package com.smeup.dbnative

import com.smeup.dbnative.log.Logger

/**
* Configuration for DB native access.
*
* @param connectionsConfig List of available connection configurations.
* @param logger Optional logger implementation.
*/
data class DBNativeAccessConfig (val connectionsConfig: List<ConnectionConfig>, val logger: Logger? = null){
constructor(connectionsConfig: List<ConnectionConfig>):this(connectionsConfig, null)
}

/**
* Create a new instance of connection configuratio for a single file or file groups.
* @param fileName File or file group identifier, wildcard "*" is admitted.
* I.E. file=*tablename is for all files starts with <code>tablename</code>
* @param url Connection url, protocol could be customized
* @param user The user
* @param password The password
* @param driver If needed
* @param impl DBMManager implementation. If doesn't specified is assumed by url
* @param properties Others connection properties
* Creates a connection configuration for a single file or a file group.
*
* @param fileName File or file-group identifier. The `*` wildcard is supported
* (for example, `*tablename` matches files ending with `tablename`).
* @param url Connection URL. The protocol can be customized.
* @param user Username.
* @param password Password.
* @param driver JDBC driver class name, when required.
* @param impl DB manager implementation. If not specified, it is inferred from `url`.
* @param properties Additional connection properties.
* @param poolConfig Connection pool configuration.
* */
data class ConnectionConfig (
data class ConnectionConfig @JvmOverloads constructor(
val fileName: String,
val url: String,
val user: String,
val password: String,
val driver: String? = null,
val impl: String? = null,
val properties : Map<String, String> = mutableMapOf())
val properties: Map<String, String> = mutableMapOf(),
val poolConfig: PoolConfig? = null
)

24 changes: 24 additions & 0 deletions base/src/main/kotlin/com/smeup/dbnative/PoolConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.smeup.dbnative

/**
* Connection pool tuning values used by SQL-backed managers.
*
* @param maximumPoolSize Maximum number of active connections.
* @param minimumIdle Minimum number of idle connections kept in the pool.
* @param connectionTimeoutMs Maximum wait time in milliseconds to borrow a connection.
* @param idleTimeoutMs Maximum idle time in milliseconds before a connection can be evicted.
* @param maxLifetimeMs Maximum lifetime in milliseconds for a pooled connection.
*/
data class PoolConfig @JvmOverloads constructor(
val maximumPoolSize: Int = 10,
val minimumIdle: Int = 2,
val connectionTimeoutMs: Long = 30_000,
val idleTimeoutMs: Long = 600_000,
val maxLifetimeMs: Long = 1_800_000,
// Set when the JDBC driver does not implement Connection.isValid() (e.g. AS400).
val connectionTestQuery: String? = null,
// Background keepalive interval in ms. HikariCP pings idle connections at this rate so they
// are already validated when borrowed — avoids per-borrow test-query round-trips over slow links.
// 0 = disabled (HikariCP default). Recommended value for AS400 over WAN: 60_000.
val keepaliveTimeMs: Long = 0
)
146 changes: 146 additions & 0 deletions base/src/test/kotlin/com/smeup/dbnative/ConnectionProviderTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package com.smeup.dbnative

import com.smeup.dbnative.file.DBFile
import com.smeup.dbnative.model.FileMetadata
import org.junit.After
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
import kotlin.test.assertNotNull
import kotlin.test.assertNull
import kotlin.test.assertSame
import kotlin.test.assertTrue

private val TEST_CONFIG = ConnectionConfig(
fileName = "*",
url = "class:com.smeup.dbnative.mock.MockDBManager",
user = "",
password = ""
)

private val TEST_CONFIG_B = ConnectionConfig(
fileName = "FILEB",
url = "class:com.smeup.dbnative.mock.MockDBManager",
user = "b",
password = "b"
)

private val ACCESS_CONFIG = DBNativeAccessConfig(listOf(TEST_CONFIG_B, TEST_CONFIG))

/**
* Unit tests for [ConnectionProvider] scoped lifecycle and lookup behavior.
*/
class ConnectionProviderTest {

@Before
fun setUp() {
ConnectionProvider.configure(ACCESS_CONFIG) { connConfig -> SpyDBMManager(connConfig) }
}

@After
fun tearDown() {
// Reset state between tests by re-configuring with a no-op factory;
// the real clean-up is handled by withScope's finally block.
}

@Test
fun withScope_createsManagerOnDemand() {
ConnectionProvider.withScope {
val manager = ConnectionProvider.currentManager("FILEA")
assertNotNull(manager)
}
}

@Test
fun withScope_sameManagerForSameFile() {
ConnectionProvider.withScope {
val m1 = ConnectionProvider.currentManager("FILEA")
val m2 = ConnectionProvider.currentManager("FILEA")
assertSame(m1, m2)
}
}

@Test
fun withScope_differentManagerForDifferentConfig() {
ConnectionProvider.withScope {
val mA = ConnectionProvider.currentManager("FILEA")
val mB = ConnectionProvider.currentManager("FILEB")
assertTrue(mA !== mB)
}
}

@Test
fun withScope_closesManagersOnExit() {
val spies = mutableListOf<SpyDBMManager>()
ConnectionProvider.configure(ACCESS_CONFIG) { connConfig ->
SpyDBMManager(connConfig).also { spies.add(it) }
}
ConnectionProvider.withScope {
ConnectionProvider.currentManager("FILEA")
ConnectionProvider.currentManager("FILEB")
}
assertTrue(spies.isNotEmpty())
assertTrue(spies.all { it.closed })
}

@Test
fun currentManagerOrNull_returnsNullOutsideScope() {
val result = ConnectionProvider.currentManagerOrNull("FILEA")
assertNull(result)
}

@Test
fun currentManagerOrNull_returnsNullForUnknownFile() {
val isolatedConfig = DBNativeAccessConfig(listOf(TEST_CONFIG_B))
ConnectionProvider.configure(isolatedConfig) { connConfig -> SpyDBMManager(connConfig) }
ConnectionProvider.withScope {
val result = ConnectionProvider.currentManagerOrNull("UNKNOWN_NO_WILDCARD")
assertNull(result)
}
}

@Test
fun withScope_throwsWhenNotConfigured() {
// Temporarily point to a fresh unconfigured-looking provider by using a local object
// We test via reflection-like approach: re-create a scenario where configure was not called.
// Since ConnectionProvider is a singleton, we configure it with null-equivalent by
// configuring with an empty config and verifying the require message.
val emptyConfig = DBNativeAccessConfig(listOf())
ConnectionProvider.configure(emptyConfig) { SpyDBMManager(it) }

assertFailsWith<IllegalArgumentException> {
ConnectionProvider.withScope {
ConnectionProvider.currentManager("ANYTHING")
}
}
}

@Test
fun withScope_scopeIsThreadLocal() {
var managerSeenFromOtherThread: DBMManager? = null
ConnectionProvider.withScope {
val thread = Thread {
managerSeenFromOtherThread = ConnectionProvider.currentManagerOrNull("FILEA")
}
thread.start()
thread.join()
}
assertNull(managerSeenFromOtherThread)
}
}

/**
* Test double used to verify manager creation and closure semantics.
*/
class SpyDBMManager(override val connectionConfig: ConnectionConfig) : DBMManager {
var closed = false

override fun existFile(name: String) = false
override fun registerMetadata(metadata: FileMetadata, overwrite: Boolean) {}
override fun metadataOf(name: String): FileMetadata = throw NotImplementedError()
override fun openFile(name: String): DBFile = throw NotImplementedError()
override fun closeFile(name: String) {}
override fun unregisterMetadata(name: String) {}
override fun validateConfig() {}
override fun close() { closed = true }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.smeup.dbnative.manager

import com.smeup.dbnative.ConnectionProvider
import com.smeup.dbnative.DBNativeAccessConfig
import com.smeup.dbnative.log.Logger

/**
* Configures [ConnectionProvider] using the manager module factory.
*
* @param config Native access configuration.
* @param logger Optional logger forwarded to manager creation.
*/
fun ConnectionProvider.configure(config: DBNativeAccessConfig, logger: Logger? = null) {
configure(config) { connConfig -> createDBManager(connConfig, logger) }
}
Loading