--- /dev/null
+package net.pterodactylus.sone.database.h2
+
+import jakarta.inject.Inject
+import java.util.Locale
+import javax.sql.DataSource
+import kotlin.properties.ReadWriteProperty
+import kotlin.reflect.KProperty
+import net.pterodactylus.sone.core.Preferences
+import net.pterodactylus.sone.database.h2.jooq.tables.references.OPTIONS
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired
+import org.jooq.SQLDialect
+import org.jooq.impl.DSL
+
+class JdbcPreferences @Inject constructor(dataSource: DataSource, sqlDialect: SQLDialect) : Preferences {
+
+ override var insertionDelay: Int? by intOption(dataSource, sqlDialect)
+ override var postsPerPage: Int? by intOption(dataSource, sqlDialect)
+ override var imagesPerPage: Int? by intOption(dataSource, sqlDialect)
+ override var charactersPerPost: Int? by intOption(dataSource, sqlDialect)
+ override var postCutOffLength: Int? by intOption(dataSource, sqlDialect)
+ override var requireFullAccess: Boolean? by booleanOption(dataSource, sqlDialect)
+ override var fcpInterfaceActive: Boolean? by booleanOption(dataSource, sqlDialect)
+ override var fcpFullAccessRequired: FullAccessRequired? by enumOption(dataSource, sqlDialect, FullAccessRequired.entries::get)
+ override var strictFiltering: Boolean? by booleanOption(dataSource, sqlDialect)
+
+}
+
+private val KProperty<*>.asOptionName: String get() = name.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
+
+private fun intOption(dataSource: DataSource, sqlDialect: SQLDialect) = object : ReadWriteProperty<Preferences, Int?> {
+
+ override operator fun getValue(thisRef: Preferences, property: KProperty<*>): Int? = DSL.using(dataSource, sqlDialect)
+ .select(OPTIONS.VALUE).from(OPTIONS)
+ .where(OPTIONS.KEY.eq(property.asOptionName))
+ .fetch().getValue(0, OPTIONS.VALUE)
+
+ override fun setValue(thisRef: Preferences, property: KProperty<*>, value: Int?) = DSL.using(dataSource, sqlDialect)
+ .insertInto(OPTIONS, OPTIONS.KEY, OPTIONS.VALUE).values(property.asOptionName, value)
+ .onDuplicateKeyUpdate().set(OPTIONS.VALUE, value)
+ .execute().let { }
+
+}
+
+private fun <E : Enum<E>> enumOption(dataSource: DataSource, sqlDialect: SQLDialect, toEnum: (Int) -> E) = object : ReadWriteProperty<Preferences, E?> {
+
+ override operator fun getValue(thisRef: Preferences, property: KProperty<*>): E? = DSL.using(dataSource, sqlDialect)
+ .select(OPTIONS.VALUE).from(OPTIONS)
+ .where(OPTIONS.KEY.eq(property.asOptionName))
+ .fetch().getValue(0, OPTIONS.VALUE)
+ ?.let(toEnum)
+
+ override fun setValue(thisRef: Preferences, property: KProperty<*>, value: E?) = DSL.using(dataSource, sqlDialect)
+ .insertInto(OPTIONS, OPTIONS.KEY, OPTIONS.VALUE).values(property.asOptionName, value?.ordinal)
+ .onDuplicateKeyUpdate().set(OPTIONS.VALUE, value?.ordinal)
+ .execute().let { }
+
+}
+
+private fun booleanOption(dataSource: DataSource, sqlDialect: SQLDialect) = object : ReadWriteProperty<Preferences, Boolean?> {
+
+ override operator fun getValue(thisRef: Preferences, property: KProperty<*>): Boolean? = DSL.using(dataSource, sqlDialect)
+ .select(OPTIONS.VALUE).from(OPTIONS)
+ .where(OPTIONS.KEY.eq(property.asOptionName))
+ .fetch().getValue(0, OPTIONS.VALUE)?.let { it != 0 }
+
+ override fun setValue(thisRef: Preferences, property: KProperty<*>, value: Boolean?) = DSL.using(dataSource, sqlDialect)
+ .insertInto(OPTIONS, OPTIONS.KEY, OPTIONS.VALUE).values(property.asOptionName, value?.toInt())
+ .onDuplicateKeyUpdate().set(OPTIONS.VALUE, value?.toInt())
+ .execute().let { }
+
+}
+
+private fun Boolean.toInt() = if (this) 1 else 0
--- /dev/null
+package net.pterodactylus.sone.database.h2
+
+import com.google.inject.Guice.createInjector
+import javax.sql.DataSource
+import kotlin.test.BeforeTest
+import kotlin.test.Test
+import net.pterodactylus.sone.database.h2.jooq.tables.references.OPTIONS
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.ALWAYS
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.NO
+import net.pterodactylus.sone.fcp.FcpInterface.FullAccessRequired.WRITING
+import net.pterodactylus.sone.test.getInstance
+import net.pterodactylus.sone.test.getMigrationSql
+import net.pterodactylus.sone.test.isProvidedBy
+import net.pterodactylus.sone.test.jOOQ
+import net.pterodactylus.sone.test.optionsTableMigrations
+import net.pterodactylus.sone.test.randomInMemoryDataSource
+import org.hamcrest.MatcherAssert.assertThat
+import org.hamcrest.Matchers.equalTo
+import org.jooq.SQLDialect
+
+class JdbcPreferencesTest {
+
+ @Test
+ fun `jdbc preferences can be created by guice`() {
+ val injector = createInjector(
+ DataSource::class.isProvidedBy(dataSource),
+ SQLDialect::class.isProvidedBy(SQLDialect.H2)
+ )
+ injector.getInstance<JdbcPreferences>()
+ }
+
+ @Test
+ fun `jdbc preferences can read current insertion delay`() {
+ verifyIntValueIsReadCorrectly("InsertionDelay", 58, jdbcPreferences::insertionDelay::get)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if insertion delay is null`() {
+ verifyIntValueIsReadCorrectly("InsertionDelay", null, jdbcPreferences::insertionDelay::get)
+ }
+
+ @Test
+ fun `jdbc preferences can write insertion delay`() {
+ verifyIntValueIsWrittenCorrectly("InsertionDelay", jdbcPreferences::insertionDelay::set, 47)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for insertion delay`() {
+ verifyIntValueIsWrittenCorrectly("InsertionDelay", jdbcPreferences::insertionDelay::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences can read current posts per page`() {
+ verifyIntValueIsReadCorrectly("PostsPerPage", 17, jdbcPreferences::postsPerPage::get)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if posts per page is null`() {
+ verifyIntValueIsReadCorrectly("PostsPerPage", null, jdbcPreferences::postsPerPage::get)
+ }
+
+ @Test
+ fun `jdbc preferences can write posts per page`() {
+ verifyIntValueIsWrittenCorrectly("PostsPerPage", jdbcPreferences::postsPerPage::set, 19)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for posts per page`() {
+ verifyIntValueIsWrittenCorrectly("PostsPerPage", jdbcPreferences::postsPerPage::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences can read current images per page`() {
+ verifyIntValueIsReadCorrectly("ImagesPerPage", 23, jdbcPreferences::imagesPerPage::get)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if images per page is null`() {
+ verifyIntValueIsReadCorrectly("ImagesPerPage", null, jdbcPreferences::imagesPerPage::get)
+ }
+
+ @Test
+ fun `jdbc preferences can write images per page`() {
+ verifyIntValueIsWrittenCorrectly("ImagesPerPage", jdbcPreferences::imagesPerPage::set, 27)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for images per page`() {
+ verifyIntValueIsWrittenCorrectly("ImagesPerPage", jdbcPreferences::imagesPerPage::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences can read current characters per post`() {
+ verifyIntValueIsReadCorrectly("CharactersPerPost", 31, jdbcPreferences::charactersPerPost::get)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if characters per post is null`() {
+ verifyIntValueIsReadCorrectly("CharactersPerPost", null, jdbcPreferences::charactersPerPost::get)
+ }
+
+ @Test
+ fun `jdbc preferences can write characters per post`() {
+ verifyIntValueIsWrittenCorrectly("CharactersPerPost", jdbcPreferences::charactersPerPost::set, 37)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for characters per post`() {
+ verifyIntValueIsWrittenCorrectly("CharactersPerPost", jdbcPreferences::charactersPerPost::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences can read current post cut-off length`() {
+ verifyIntValueIsReadCorrectly("PostCutOffLength", 41, jdbcPreferences::postCutOffLength::get)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if post cut-off length is null`() {
+ verifyIntValueIsReadCorrectly("PostCutOffLength", null, jdbcPreferences::postCutOffLength::get)
+ }
+
+ @Test
+ fun `jdbc preferences can write post cut-off length`() {
+ verifyIntValueIsWrittenCorrectly("PostCutOffLength", jdbcPreferences::postCutOffLength::set, 47)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for post cut-off length`() {
+ verifyIntValueIsWrittenCorrectly("PostCutOffLength", jdbcPreferences::postCutOffLength::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences reads 0 for require full access flag as false`() {
+ verifyBooleanValueIsReadCorrectly("RequireFullAccess", 0, jdbcPreferences::requireFullAccess::get, false)
+ }
+
+ @Test
+ fun `jdbc preferences reads 1 for require full access flag as true`() {
+ verifyBooleanValueIsReadCorrectly("RequireFullAccess", 1, jdbcPreferences::requireFullAccess::get, true)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if require full access is null`() {
+ verifyBooleanValueIsReadCorrectly("RequireFullAccess", null, jdbcPreferences::requireFullAccess::get, null)
+ }
+
+ @Test
+ fun `jdbc preferences can write require full access flag when false`() {
+ verifyBooleanValueIsWrittenCorrectly("RequireFullAccess", false, jdbcPreferences::requireFullAccess::set, 0)
+ }
+
+ @Test
+ fun `jdbc preferences can write require full access flag when true`() {
+ verifyBooleanValueIsWrittenCorrectly("RequireFullAccess", true, jdbcPreferences::requireFullAccess::set, 1)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for require full access`() {
+ verifyBooleanValueIsWrittenCorrectly("RequireFullAccess", null, jdbcPreferences::requireFullAccess::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences reads 0 for fcp interface active flag as false`() {
+ verifyBooleanValueIsReadCorrectly("FcpInterfaceActive", 0, jdbcPreferences::fcpInterfaceActive::get, false)
+ }
+
+ @Test
+ fun `jdbc preferences reads 1 for fcp interface active flag as true`() {
+ verifyBooleanValueIsReadCorrectly("FcpInterfaceActive", 1, jdbcPreferences::fcpInterfaceActive::get, true)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if fcp interface active is null`() {
+ verifyBooleanValueIsReadCorrectly("FcpInterfaceActive", null, jdbcPreferences::fcpInterfaceActive::get, null)
+ }
+
+ @Test
+ fun `jdbc preferences can write fcp interface active flag when false`() {
+ verifyBooleanValueIsWrittenCorrectly("FcpInterfaceActive", false, jdbcPreferences::fcpInterfaceActive::set, 0)
+ }
+
+ @Test
+ fun `jdbc preferences can write fcp interface active flag when true`() {
+ verifyBooleanValueIsWrittenCorrectly("FcpInterfaceActive", true, jdbcPreferences::fcpInterfaceActive::set, 1)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for fcp interface active`() {
+ verifyBooleanValueIsWrittenCorrectly("FcpInterfaceActive", null, jdbcPreferences::fcpInterfaceActive::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences reads 0 for fcp full-access required as no`() {
+ verifyEnumValueIsReadCorrectly("FcpFullAccessRequired", 0, jdbcPreferences::fcpFullAccessRequired::get, NO)
+ }
+
+ @Test
+ fun `jdbc preferences reads 1 for fcp full-access required as writing`() {
+ verifyEnumValueIsReadCorrectly("FcpFullAccessRequired", 1, jdbcPreferences::fcpFullAccessRequired::get, WRITING)
+ }
+
+ @Test
+ fun `jdbc preferences reads 2 for fcp full-access required as always`() {
+ verifyEnumValueIsReadCorrectly("FcpFullAccessRequired", 2, jdbcPreferences::fcpFullAccessRequired::get, ALWAYS)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if fcp full-access required is null`() {
+ verifyEnumValueIsReadCorrectly("FcpFullAccessRequired", null, jdbcPreferences::fcpFullAccessRequired::get, null)
+ }
+
+ @Test
+ fun `jdbc preferences writes no for fcp full-access requires as 0`() {
+ verifyEnumValueIsWrittenCorrectly("FcpFullAccessRequired", NO, jdbcPreferences::fcpFullAccessRequired::set, 0)
+ }
+
+ @Test
+ fun `jdbc preferences writes writing for fcp full-access requires as 1`() {
+ verifyEnumValueIsWrittenCorrectly("FcpFullAccessRequired", WRITING, jdbcPreferences::fcpFullAccessRequired::set, 1)
+ }
+
+ @Test
+ fun `jdbc preferences writes always for fcp full-access requires as 2`() {
+ verifyEnumValueIsWrittenCorrectly("FcpFullAccessRequired", ALWAYS, jdbcPreferences::fcpFullAccessRequired::set, 2)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for fcp full-access required`() {
+ verifyEnumValueIsWrittenCorrectly("FcpFullAccessRequired", null, jdbcPreferences::fcpFullAccessRequired::set, null)
+ }
+
+ @Test
+ fun `jdbc preferences reads 0 for strict filtering flag as false`() {
+ verifyBooleanValueIsReadCorrectly("StrictFiltering", 0, jdbcPreferences::strictFiltering::get, false)
+ }
+
+ @Test
+ fun `jdbc preferences reads 1 for strict filtering flag as true`() {
+ verifyBooleanValueIsReadCorrectly("StrictFiltering", 1, jdbcPreferences::strictFiltering::get, true)
+ }
+
+ @Test
+ fun `jdbc preferences returns null if strict filtering is null`() {
+ verifyBooleanValueIsReadCorrectly("StrictFiltering", null, jdbcPreferences::strictFiltering::get, null)
+ }
+
+ @Test
+ fun `jdbc preferences can write strict filtering flag when false`() {
+ verifyBooleanValueIsWrittenCorrectly("StrictFiltering", false, jdbcPreferences::strictFiltering::set, 0)
+ }
+
+ @Test
+ fun `jdbc preferences can write strict filtering flag when true`() {
+ verifyBooleanValueIsWrittenCorrectly("StrictFiltering", true, jdbcPreferences::strictFiltering::set, 1)
+ }
+
+ @Test
+ fun `jdbc preferences can write null for strict filtering`() {
+ verifyBooleanValueIsWrittenCorrectly("StrictFiltering", null, jdbcPreferences::strictFiltering::set, null)
+ }
+
+ @BeforeTest
+ fun createOptionsTable() {
+ jOOQ.transaction { configuration ->
+ getMigrationSql(*optionsTableMigrations).forEach(configuration.dsl()::execute)
+ }
+ }
+
+ private fun readConfigOption(key: String) =
+ jOOQ.select(OPTIONS.VALUE).from(OPTIONS).where(OPTIONS.KEY.eq(key)).fetchOne(OPTIONS.VALUE)
+
+ private fun insertConfigOption(key: String, value: Int?) =
+ jOOQ.update(OPTIONS).set(OPTIONS.VALUE, value).where(OPTIONS.KEY.eq(key)).execute()
+
+ private fun verifyIntValueIsReadCorrectly(propertyKey: String, intValue: Int?, getter: () -> Int?) {
+ insertConfigOption(propertyKey, intValue)
+ assertThat(getter(), equalTo(intValue))
+ }
+
+ private fun verifyIntValueIsWrittenCorrectly(propertyKey: String, setter: (Int?) -> Unit, intValue: Int?) {
+ setter(intValue)
+ assertThat(readConfigOption(propertyKey), equalTo(intValue))
+ }
+
+ private fun verifyBooleanValueIsReadCorrectly(propertyKey: String, databaseIntValue: Int?, getter: () -> Boolean?, expectedValue: Boolean?) {
+ insertConfigOption(propertyKey, databaseIntValue)
+ assertThat(getter(), equalTo(expectedValue))
+ }
+
+ private fun verifyBooleanValueIsWrittenCorrectly(propertyKey: String, booleanValue: Boolean?, setter: (Boolean?) -> Unit, databaseIntValue: Int?) {
+ setter(booleanValue)
+ assertThat(readConfigOption(propertyKey), equalTo(databaseIntValue))
+ }
+
+ @Suppress("SameParameterValue")
+ private fun <E> verifyEnumValueIsReadCorrectly(propertyKey: String, databaseIntValue: Int?, getter: () -> E?, expectedValue: E?) {
+ insertConfigOption(propertyKey, databaseIntValue)
+ assertThat(getter(), equalTo(expectedValue))
+ }
+
+ @Suppress("SameParameterValue")
+ private fun <E> verifyEnumValueIsWrittenCorrectly(propertyKey: String, enumValue: E?, setter: (E?) -> Unit, databaseIntValue: Int?) {
+ setter(enumValue)
+ assertThat(readConfigOption(propertyKey), equalTo(databaseIntValue))
+ }
+
+ private val dataSource = randomInMemoryDataSource
+ private val jOOQ = jOOQ(dataSource)
+ private val jdbcPreferences = JdbcPreferences(dataSource, SQLDialect.H2)
+
+}