diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index fc7816b3..13484a09 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -13,4 +13,8 @@ dependencies { implementation("info.solidsoft.gradle.pitest:gradle-pitest-plugin:1.15.0") implementation("org.gradle:test-retry-gradle-plugin:1.6.0") implementation(libs.forbiddenapis) + implementation(libs.detekt) + val kotlinVersion = "1.9.23" + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") + implementation("org.jetbrains.kotlin:kotlin-allopen:$kotlinVersion") } diff --git a/buildSrc/src/main/kotlin/pg-index-health.kotlin-application.gradle.kts b/buildSrc/src/main/kotlin/pg-index-health.kotlin-application.gradle.kts new file mode 100644 index 00000000..3926b0ea --- /dev/null +++ b/buildSrc/src/main/kotlin/pg-index-health.kotlin-application.gradle.kts @@ -0,0 +1,43 @@ +import io.gitlab.arturbosch.detekt.Detekt +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("org.jetbrains.kotlin.jvm") + id("org.jetbrains.kotlin.plugin.spring") + id("pg-index-health.java-compilation") + id("io.gitlab.arturbosch.detekt") + id("pg-index-health.forbidden-apis") +} + +private val versionCatalog = extensions.getByType().named("libs") + +dependencies { + implementation("org.jetbrains.kotlin:kotlin-reflect") + + versionCatalog.findLibrary("detekt-formatting").ifPresent { + detektPlugins(it) + } + versionCatalog.findLibrary("detekt-libraries").ifPresent { + detektPlugins(it) + } +} + +tasks.withType { + kotlinOptions { + freeCompilerArgs += "-Xjsr305=strict" + jvmTarget = "11" + } +} + +detekt { + toolVersion = versionCatalog.findVersion("detekt").get().requiredVersion + config.setFrom(file("${rootDir}/config/detekt/detekt.yml")) + buildUponDefaultConfig = true +} + +tasks.withType().configureEach { + reports { + xml.required.set(true) + html.required.set(true) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 5deafde9..b5b217a8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -28,8 +28,8 @@ detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-form detekt-libraries = { group = "io.gitlab.arturbosch.detekt", name = "detekt-rules-libraries", version.ref = "detekt" } testcontainers-bom = { group = "org.testcontainers", name = "testcontainers-bom", version.ref = "testcontainers" } forbiddenapis = { group = "de.thetaphi", name = "forbiddenapis", version.ref = "forbiddenapis" } +detekt = { group = "io.gitlab.arturbosch.detekt", name = "detekt-gradle-plugin", version.ref = "detekt" } [plugins] spring-boot-gradlePlugin = { id = "org.springframework.boot", version.ref = "spring-boot" } spring-dependency-management = { id = "io.spring.dependency-management", version = "1.1.6" } -detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 3eb510fe..8e68bd52 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,3 +20,5 @@ include("pg-index-health-logger") include("pg-index-health-core") include("spring-boot-integration:postgres-tc-url-demo-app") findProject(":spring-boot-integration:postgres-tc-url-demo-app")?.name = "postgres-tc-url-demo-app" +include("spring-boot-integration:kotlin-custom-ds-demo-app") +findProject(":spring-boot-integration:kotlin-custom-ds-demo-app")?.name = "kotlin-custom-ds-demo-app" diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/build.gradle.kts b/spring-boot-integration/kotlin-custom-ds-demo-app/build.gradle.kts new file mode 100644 index 00000000..fb17d593 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("pg-index-health.kotlin-application") + alias(libs.plugins.spring.boot.gradlePlugin) + alias(libs.plugins.spring.dependency.management) +} + +ext["commons-lang3.version"] = libs.versions.commons.lang3.get() +ext["assertj.version"] = libs.versions.assertj.get() +// ext["mockito.version"] = libs.versions.mockito.get() +ext["junit-jupiter.version"] = libs.versions.junit.get() + +dependencies { + implementation(platform(libs.testcontainers.bom)) + implementation("org.testcontainers:postgresql") + implementation(libs.spring.boot.starter.data.jdbc) + implementation("org.liquibase:liquibase-core:4.30.0") + implementation("com.github.blagerweij:liquibase-sessionlock:1.6.9") + + runtimeOnly(libs.postgresql) + + testImplementation(libs.spring.boot.starter.test) + testImplementation(project(":spring-boot-integration:pg-index-health-test-starter")) +} diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplication.kt b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplication.kt new file mode 100644 index 00000000..b1604a19 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplication.kt @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2019-2024. Ivan Vakhrushev and others. + * https://github.com/mfvanek/pg-index-health + * + * This file is a part of "pg-index-health" - a Java library for + * analyzing and maintaining indexes health in PostgreSQL databases. + * + * Licensed under the Apache License 2.0 + */ + +package io.github.mfvanek.pg.spring.postgres.kt.custom.ds + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication + +@SpringBootApplication +class PostgresCustomDataSourceDemoApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/config/DataSourceConfiguration.kt b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/config/DataSourceConfiguration.kt new file mode 100644 index 00000000..4f89c651 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/config/DataSourceConfiguration.kt @@ -0,0 +1,34 @@ +/* + * Copyright (c) 2019-2024. Ivan Vakhrushev and others. + * https://github.com/mfvanek/pg-index-health + * + * This file is a part of "pg-index-health" - a Java library for + * analyzing and maintaining indexes health in PostgreSQL databases. + * + * Licensed under the Apache License 2.0 + */ + +package io.github.mfvanek.pg.spring.postgres.kt.custom.ds.config + +import com.zaxxer.hikari.HikariConfig +import com.zaxxer.hikari.HikariDataSource +import org.springframework.boot.autoconfigure.liquibase.LiquibaseDataSource +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Primary + +@Configuration +class DataSourceConfiguration { + + @Configuration + @ConfigurationProperties("pgih.custom.datasource") + class CustomDataSourceProperties : HikariConfig() + + @Bean + @Primary + @LiquibaseDataSource + fun pgihCustomDataSource(properties: CustomDataSourceProperties): HikariDataSource { + return HikariDataSource(properties) + } +} diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/application.yaml b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/application.yaml new file mode 100644 index 00000000..8eb3f4d5 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/application.yaml @@ -0,0 +1,15 @@ +debug: false + +spring: + main: + banner-mode: off + liquibase: + change-log: classpath:/changelog/changelog.yaml + default-schema: custom_ds_schema + +pgih: + custom: + datasource: + jdbc-url: jdbc:tc:postgresql:17.2:///demo_for_pg_index_health_starter?TC_INITSCRIPT=init.sql + driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver + maximum-pool-size: 5 diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/changelog.yaml b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/changelog.yaml new file mode 100644 index 00000000..5e241f3c --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/changelog.yaml @@ -0,0 +1,3 @@ +databaseChangeLog: + - include: + file: changelog/sql/warehouse.sql diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/sql/warehouse.sql b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/sql/warehouse.sql new file mode 100644 index 00000000..d600eb21 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/changelog/sql/warehouse.sql @@ -0,0 +1,12 @@ +--liquibase formatted sql + +--changeset ivan.vakhrushev:2024.12.04:warehouse.table +create table if not exists warehouse +( + id bigint primary key generated always as identity, + name varchar(255) not null +); + +comment on table warehouse is 'Information about the warehouses'; +comment on column warehouse.id is 'Unique identifier of the warehouse'; +comment on column warehouse.name is 'Human readable name of the warehouse'; diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/init.sql b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/init.sql new file mode 100644 index 00000000..88f1f431 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/main/resources/init.sql @@ -0,0 +1 @@ +create schema if not exists custom_ds_schema; diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtRunTest.kt b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtRunTest.kt new file mode 100644 index 00000000..03953d83 --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtRunTest.kt @@ -0,0 +1,22 @@ +package io.github.mfvanek.pg.spring.postgres.kt.custom.ds + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatCode +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith +import org.springframework.boot.test.system.CapturedOutput +import org.springframework.boot.test.system.OutputCaptureExtension + +@ExtendWith(OutputCaptureExtension::class) +internal class PostgresCustomDataSourceDemoApplicationKtRunTest { + + @Test + fun applicationShouldRun(output: CapturedOutput) { + assertThatCode { main(arrayOf("--spring.profiles.active=test")) } + .doesNotThrowAnyException() + assertThat(output.all) + .contains("Starting PostgresCustomDataSourceDemoApplicationKt using Java") + .contains("Container is started (JDBC URL: jdbc:postgresql://localhost:") + .contains("Started PostgresCustomDataSourceDemoApplicationKt in") + } +} diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtTest.kt b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtTest.kt new file mode 100644 index 00000000..87f327bc --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/custom/ds/PostgresCustomDataSourceDemoApplicationKtTest.kt @@ -0,0 +1,60 @@ +package io.github.mfvanek.pg.spring.postgres.kt.custom.ds + +import com.zaxxer.hikari.HikariDataSource +import io.github.mfvanek.pg.connection.PgConnection +import io.github.mfvanek.pg.core.checks.common.DatabaseCheckOnHost +import io.github.mfvanek.pg.core.checks.common.Diagnostic +import io.github.mfvanek.pg.model.context.PgContext +import io.github.mfvanek.pg.model.dbobject.DbObject +import io.github.mfvanek.pg.model.predicates.SkipLiquibaseTablesPredicate +import io.github.mfvanek.pg.model.table.Table +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.ApplicationContext +import org.springframework.test.context.ActiveProfiles + +@ActiveProfiles("test") +@SpringBootTest +internal class PostgresCustomDataSourceDemoApplicationKtTest { + + @Autowired + private lateinit var applicationContext: ApplicationContext + + @Autowired + private lateinit var checks: List> + + @Test + fun contextLoadsAndContainsPgIndexHealthBeans() { + assertThat(applicationContext.getBean("pgihCustomDataSource")) + .isInstanceOf(HikariDataSource::class.java) + + assertThat(applicationContext.getBean("pgConnection")) + .isInstanceOf(PgConnection::class.java) + } + + @Test + fun checksShouldWork() { + assertThat(checks) + .hasSameSizeAs(Diagnostic.entries.toTypedArray()) + + checks + .filter { it.isStatic } + .forEach { check -> + val ctx = PgContext.of("custom_ds_schema") + // Due to the use of spring.liquibase.default-schema, all names are resolved without a schema + val listAssert = assertThat(check.check(ctx, SkipLiquibaseTablesPredicate.ofPublic())) + .`as`(check.diagnostic.name) + + when (check.diagnostic) { + Diagnostic.TABLES_NOT_LINKED_TO_OTHERS -> + listAssert + .hasSize(1) + .containsExactly(Table.of("warehouse", 0L)) + + else -> listAssert.isEmpty() + } + } + } +} diff --git a/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/resources/application-test.yaml b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/resources/application-test.yaml new file mode 100644 index 00000000..7482924f --- /dev/null +++ b/spring-boot-integration/kotlin-custom-ds-demo-app/src/test/resources/application-test.yaml @@ -0,0 +1,3 @@ +pg.index.health.test: + datasource-bean-name: pgihCustomDataSource + datasource-url-property-name: pgih.custom.datasource.jdbc-url diff --git a/spring-boot-integration/kotlin-demo-app/build.gradle.kts b/spring-boot-integration/kotlin-demo-app/build.gradle.kts index 70b1f477..1f6af68d 100644 --- a/spring-boot-integration/kotlin-demo-app/build.gradle.kts +++ b/spring-boot-integration/kotlin-demo-app/build.gradle.kts @@ -1,15 +1,7 @@ -import io.gitlab.arturbosch.detekt.Detekt -import org.jetbrains.kotlin.gradle.tasks.KotlinCompile - plugins { - val kotlinVersion = "1.9.23" - kotlin("jvm") version kotlinVersion - kotlin("plugin.spring") version kotlinVersion - id("pg-index-health.java-compilation") + id("pg-index-health.kotlin-application") alias(libs.plugins.spring.boot.gradlePlugin) alias(libs.plugins.spring.dependency.management) - alias(libs.plugins.detekt) - id("pg-index-health.forbidden-apis") } ext["commons-lang3.version"] = libs.versions.commons.lang3.get() @@ -18,7 +10,6 @@ ext["assertj.version"] = libs.versions.assertj.get() ext["junit-jupiter.version"] = libs.versions.junit.get() dependencies { - implementation("org.jetbrains.kotlin:kotlin-reflect") implementation(project(":pg-index-health-testing")) implementation(libs.spring.boot.starter.data.jdbc) implementation(platform(libs.testcontainers.bom)) @@ -28,27 +19,4 @@ dependencies { testImplementation(libs.spring.boot.starter.test) testImplementation(project(":spring-boot-integration:pg-index-health-test-starter")) - - detektPlugins(libs.detekt.formatting) - detektPlugins(libs.detekt.libraries) -} - -tasks.withType { - kotlinOptions { - freeCompilerArgs += "-Xjsr305=strict" - jvmTarget = "11" - } -} - -detekt { - toolVersion = libs.versions.detekt.get() - config.setFrom(file("${rootDir}/config/detekt/detekt.yml")) - buildUponDefaultConfig = true -} - -tasks.withType().configureEach { - reports { - xml.required.set(true) - html.required.set(true) - } } diff --git a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt index 2a5b698d..8eeedc73 100644 --- a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt +++ b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationKtTest.kt @@ -25,7 +25,7 @@ import org.springframework.test.context.ActiveProfiles @ActiveProfiles("test") @SpringBootTest -class PostgresDemoApplicationKtTest { +internal class PostgresDemoApplicationKtTest { @Autowired private lateinit var applicationContext: ApplicationContext @@ -37,7 +37,7 @@ class PostgresDemoApplicationKtTest { private lateinit var checks: List> @Test - fun contextLoadsAndDoesNotContainPgIndexHealthBeans() { + fun contextLoadsAndContainsPgIndexHealthBeans() { assertThat(applicationContext.getBean("dataSource")) .isInstanceOf(HikariDataSource::class.java) diff --git a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationRunKtTest.kt b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationRunKtTest.kt index 60a751d8..f308069f 100644 --- a/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationRunKtTest.kt +++ b/spring-boot-integration/kotlin-demo-app/src/test/kotlin/io/github/mfvanek/pg/spring/postgres/kt/PostgresDemoApplicationRunKtTest.kt @@ -18,7 +18,7 @@ import org.springframework.boot.test.system.CapturedOutput import org.springframework.boot.test.system.OutputCaptureExtension @ExtendWith(OutputCaptureExtension::class) -class PostgresDemoApplicationRunKtTest { +internal class PostgresDemoApplicationRunKtTest { @Test fun applicationShouldRun(output: CapturedOutput) { diff --git a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java index 85bc166c..a6c4bd78 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfiguration.java @@ -12,8 +12,7 @@ import io.github.mfvanek.pg.connection.PgConnection; import io.github.mfvanek.pg.connection.PgConnectionImpl; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.beans.factory.annotation.Value; +import org.springframework.beans.factory.BeanFactory; import org.springframework.boot.autoconfigure.AutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -23,7 +22,9 @@ import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; +import org.springframework.core.env.Environment; +import javax.annotation.Nonnull; import javax.sql.DataSource; /** @@ -40,17 +41,42 @@ public class DatabaseStructureHealthAutoConfiguration { /** - * {@link PgConnection} bean. + * Configuration properties for the database structure health check. + */ + private final DatabaseStructureHealthProperties properties; + + /** + * Constructs a new instance of {@code DatabaseStructureHealthAutoConfiguration}. + * + * @param properties the {@link DatabaseStructureHealthProperties} containing + * the configuration for this auto-configuration (must not be null) + */ + public DatabaseStructureHealthAutoConfiguration(@Nonnull final DatabaseStructureHealthProperties properties) { + this.properties = properties; + } + + /** + * Creates and configures a {@link PgConnection} bean. + *

+ * This bean is created only if: + *

    + *
  • A {@link DataSource} bean is available in the application context.
  • + *
  • No other {@link PgConnection} bean is already defined.
  • + *
+ * The {@link DataSource} bean and database URL property are resolved dynamically + * based on the configured {@link DatabaseStructureHealthProperties}. * - * @param dataSource {@link DataSource} instance - * @param databaseUrl connection string to database - * @return {@link PgConnection} instance + * @param beanFactory the {@link BeanFactory} instance used to retrieve the {@link DataSource} bean + * @param environment the {@link Environment} instance used to resolve the database URL property + * @return a configured {@link PgConnection} instance */ @Bean - @ConditionalOnBean(name = "dataSource") + @ConditionalOnBean(DataSource.class) @ConditionalOnMissingBean - public PgConnection pgConnection(@Qualifier("dataSource") final DataSource dataSource, - @Value("${spring.datasource.url:#{null}}") final String databaseUrl) { + public PgConnection pgConnection(@Nonnull final BeanFactory beanFactory, + @Nonnull final Environment environment) { + final DataSource dataSource = beanFactory.getBean(properties.getDatasourceBeanName(), DataSource.class); + final String databaseUrl = environment.getProperty(properties.getDatasourceUrlPropertyName()); return PgConnectionImpl.ofUrl(dataSource, databaseUrl); } } diff --git a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthCondition.java b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthCondition.java index 09877d20..2d090042 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthCondition.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthCondition.java @@ -18,6 +18,7 @@ import org.springframework.context.annotation.ConditionContext; import org.springframework.core.type.AnnotatedTypeMetadata; +import javax.annotation.Nonnull; import javax.annotation.Nullable; /** @@ -25,28 +26,35 @@ */ public class DatabaseStructureHealthCondition extends SpringBootCondition { - private static final String PROPERTY_NAME = "spring.datasource.url"; - /** * {@inheritDoc} */ @Override public ConditionOutcome getMatchOutcome(final ConditionContext context, final AnnotatedTypeMetadata metadata) { final ConditionMessage.Builder message = ConditionMessage.forCondition("pg.index.health.test PostgreSQL condition"); - final String jdbcUrl = getJdbcUrl(context); + final String datasourceUrlPropertyName = getDatasourceUrlPropertyName(context); + final String jdbcUrl = getJdbcUrl(context, datasourceUrlPropertyName); if (jdbcUrl != null && !jdbcUrl.isBlank()) { if (jdbcUrl.startsWith(PgUrlParser.URL_HEADER) || jdbcUrl.startsWith(PgUrlParser.TESTCONTAINERS_PG_URL_PREFIX)) { return ConditionOutcome.match(message.foundExactly("found PostgreSQL connection " + jdbcUrl)); } return ConditionOutcome.noMatch(message.notAvailable("not PostgreSQL connection")); } - return ConditionOutcome.match(message.didNotFind(PROPERTY_NAME).items()); + return ConditionOutcome.match(message.didNotFind(datasourceUrlPropertyName).items()); } @Nullable - private static String getJdbcUrl(final ConditionContext context) { + private static String getJdbcUrl(@Nonnull final ConditionContext context, + @Nonnull final String datasourceUrlPropertyName) { return Binder.get(context.getEnvironment()) - .bind(PROPERTY_NAME, String.class) + .bind(datasourceUrlPropertyName, String.class) .orElse(null); } + + @Nonnull + private static String getDatasourceUrlPropertyName(@Nonnull final ConditionContext context) { + return Binder.get(context.getEnvironment()) + .bind("pg.index.health.test.datasource-url-property-name", String.class) + .orElse(DatabaseStructureHealthProperties.STANDARD_DATASOURCE_URL_PROPERTY_NAME); + } } diff --git a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthProperties.java b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthProperties.java index e2e30a7d..3d7d8884 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthProperties.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/main/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthProperties.java @@ -10,10 +10,13 @@ package io.github.mfvanek.pg.spring; +import io.github.mfvanek.pg.model.validation.Validators; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConstructorBinding; import org.springframework.boot.context.properties.bind.DefaultValue; +import javax.annotation.concurrent.Immutable; + /** * Represents properties for managing pg-index-health-test-starter configuration. * @@ -22,31 +25,95 @@ */ @ConstructorBinding @ConfigurationProperties(prefix = "pg.index.health.test") +@Immutable public class DatabaseStructureHealthProperties { /** - * Allows to manually disable starter even it presents on classpath. + * The standard name of the primary {@link javax.sql.DataSource} bean. + *

+ * This constant is used as the default name when no custom datasource bean name + * is specified in the configuration. + *

+ */ + public static final String STANDARD_DATASOURCE_BEAN_NAME = "dataSource"; + + /** + * The standard property name for the datasource URL in Spring configuration. + *

+ * This constant is used as the default key for retrieving the datasource URL + * from the {@link org.springframework.core.env.Environment}. + *

+ */ + public static final String STANDARD_DATASOURCE_URL_PROPERTY_NAME = "spring.datasource.url"; + + /** + * Indicates whether the starter is enabled, even if it is present on the classpath. + * This allows for manual control over autoconfiguration. + *

+ * Default value: {@code true}. + *

*/ private final boolean enabled; /** - * Constructs a {@code DatabaseStructureHealthProperties} instance. + * The name of the datasource bean to use for health checks. + *

+ * Default value: {@code "dataSource"}. + *

+ */ + private final String datasourceBeanName; + + /** + * The name of the datasource URL property used in the configuration. + *

+ * Default value: {@code "spring.datasource.url"}. + *

+ */ + private final String datasourceUrlPropertyName; + + /** + * Constructs a new {@code DatabaseStructureHealthProperties} instance with the specified values. * - * @param enabled enabled or disabled autoconfiguration + * @param enabled whether the autoconfiguration is enabled (default: {@code true}) + * @param datasourceBeanName the name of the datasource bean (default: {@code "dataSource"}, must not be blank) + * @param datasourceUrlPropertyName the name of the datasource URL property (default: {@code "spring.datasource.url"}, must not be blank) + * @throws IllegalArgumentException if {@code datasourceBeanName} or {@code datasourceUrlPropertyName} is blank */ - public DatabaseStructureHealthProperties(@DefaultValue("true") final boolean enabled) { + public DatabaseStructureHealthProperties(@DefaultValue("true") final boolean enabled, + @DefaultValue(STANDARD_DATASOURCE_BEAN_NAME) final String datasourceBeanName, + @DefaultValue(STANDARD_DATASOURCE_URL_PROPERTY_NAME) final String datasourceUrlPropertyName) { this.enabled = enabled; + this.datasourceBeanName = Validators.notBlank(datasourceBeanName, "datasourceBeanName"); + this.datasourceUrlPropertyName = Validators.notBlank(datasourceUrlPropertyName, "datasourceUrlPropertyName"); } /** - * Retrieves the state of autoconfiguration: enabled or disabled. + * Checks if the autoconfiguration is enabled. * - * @return true if starter enabled otherwise false + * @return {@code true} if the starter is enabled; {@code false} otherwise */ public boolean isEnabled() { return enabled; } + /** + * Retrieves the name of the datasource bean to be used. + * + * @return the name of the datasource bean (default: {@code "dataSource"}) + */ + public String getDatasourceBeanName() { + return datasourceBeanName; + } + + /** + * Retrieves the name of the datasource URL property. + * + * @return the name of the datasource URL property (default: {@code "spring.datasource.url"}) + */ + public String getDatasourceUrlPropertyName() { + return datasourceUrlPropertyName; + } + /** * {@inheritDoc} */ @@ -54,6 +121,8 @@ public boolean isEnabled() { public String toString() { return DatabaseStructureHealthProperties.class.getSimpleName() + '{' + "enabled=" + enabled + + ", datasourceBeanName='" + datasourceBeanName + '\'' + + ", datasourceUrlPropertyName='" + datasourceUrlPropertyName + '\'' + '}'; } } diff --git a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java index ab4d8c70..fabfd6ca 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/AutoConfigurationTestBase.java @@ -25,6 +25,7 @@ import javax.annotation.Nonnull; import javax.sql.DataSource; +import static io.github.mfvanek.pg.spring.DatabaseStructureHealthProperties.STANDARD_DATASOURCE_BEAN_NAME; import static org.assertj.core.api.Assertions.assertThat; abstract class AutoConfigurationTestBase { @@ -62,8 +63,13 @@ abstract class AutoConfigurationTestBase { protected static final Class[] EXPECTED_TYPES = {PgConnection.class, DatabaseCheckOnHost.class, StatisticsMaintenanceOnHost.class, ConfigurationMaintenanceOnHost.class}; protected static final DataSource DATA_SOURCE_MOCK = Mockito.mock(DataSource.class); - protected final Predicate beanNamesFilter = b -> !b.startsWith("org.springframework") && !b.startsWith("pg.index.health.test") && - !b.endsWith("AutoConfiguration") && !"dataSource".equals(b); + private static final String CUSTOM_DATASOURCE_BEAN_NAME = "customDataSource"; + + protected final Predicate beanNamesFilter = b -> !b.startsWith("org.springframework") && + !b.startsWith("pg.index.health.test") && + !b.endsWith("AutoConfiguration") && + !STANDARD_DATASOURCE_BEAN_NAME.equals(b) && + !CUSTOM_DATASOURCE_BEAN_NAME.equals(b); private final ApplicationContextRunner contextRunner = new ApplicationContextRunner(); @Nonnull @@ -73,7 +79,12 @@ protected ApplicationContextRunner assertWithTestConfig() { protected static void initialize(@Nonnull final C applicationContext) { final GenericApplicationContext context = (GenericApplicationContext) applicationContext; - context.registerBean("dataSource", DataSource.class, () -> DATA_SOURCE_MOCK); + context.registerBean(STANDARD_DATASOURCE_BEAN_NAME, DataSource.class, () -> DATA_SOURCE_MOCK); + } + + protected static void initializeCustom(@Nonnull final C applicationContext) { + final GenericApplicationContext context = (GenericApplicationContext) applicationContext; + context.registerBean(CUSTOM_DATASOURCE_BEAN_NAME, DataSource.class, () -> DATA_SOURCE_MOCK); } @Nonnull diff --git a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationTest.java b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationTest.java index f3810b19..a1ad9574 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationTest.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthAutoConfigurationTest.java @@ -55,6 +55,19 @@ void withDataSource() { }); } + @Test + void withCustomDataSource() { + assertWithTestConfig() + .withPropertyValues("custom.datasource.url=jdbc:postgresql://localhost:5432", + "pg.index.health.test.datasource-bean-name=customDataSource", + "pg.index.health.test.datasource-url-property-name=custom.datasource.url") + .withInitializer(AutoConfigurationTestBase::initializeCustom) + .run(context -> { + assertThatBeansPresent(context); + assertThatBeansAreNotNullBean(context); + }); + } + @Test void withDataSourceButWithoutConnectionString() throws SQLException { try (Connection connectionMock = Mockito.mock(Connection.class)) { @@ -70,6 +83,23 @@ void withDataSourceButWithoutConnectionString() throws SQLException { } } + @Test + void withCustomDataSourceButWithoutConnectionString() throws SQLException { + try (Connection connectionMock = Mockito.mock(Connection.class)) { + setMocks(connectionMock); + + assertWithTestConfig() + .withInitializer(AutoConfigurationTestBase::initializeCustom) + .withPropertyValues("pg.index.health.test.datasource-bean-name=customDataSource", + "pg.index.health.test.datasource-url-property-name=custom.datasource.url") + .run(context -> { + assertThatBeansPresent(context); + assertThatBeansAreNotNullBean(context); + assertThatPgConnectionIsValid(context); + }); + } + } + @Test void withDataSourceAndEmptyConnectionString() throws SQLException { try (Connection connectionMock = Mockito.mock(Connection.class)) { @@ -86,6 +116,24 @@ void withDataSourceAndEmptyConnectionString() throws SQLException { } } + @Test + void withCustomDataSourceAndEmptyConnectionString() throws SQLException { + try (Connection connectionMock = Mockito.mock(Connection.class)) { + setMocks(connectionMock); + + assertWithTestConfig() + .withPropertyValues("custom.datasource.url=", + "pg.index.health.test.datasource-bean-name=customDataSource", + "pg.index.health.test.datasource-url-property-name=custom.datasource.url") + .withInitializer(AutoConfigurationTestBase::initializeCustom) + .run(context -> { + assertThatBeansPresent(context); + assertThatBeansAreNotNullBean(context); + assertThatPgConnectionIsValid(context); + }); + } + } + @Test void withDataSourceAndWrongConnectionString() { assertWithTestConfig() @@ -101,6 +149,23 @@ void withDataSourceAndWrongConnectionString() { }); } + @Test + void withCustomDataSourceAndWrongConnectionString() { + assertWithTestConfig() + .withPropertyValues("custom.datasource.url=jdbc:mysql://localhost/test", + "pg.index.health.test.datasource-bean-name=customDataSource", + "pg.index.health.test.datasource-url-property-name=custom.datasource.url") + .withInitializer(AutoConfigurationTestBase::initializeCustom) + .run(context -> { + assertThat(context.getBeansOfType(DatabaseStructureHealthProperties.class)) + .isEmpty(); + assertThat(context.getBeanDefinitionNames()) + .isNotEmpty() + .filteredOn(beanNamesFilter) + .isEmpty(); + }); + } + @Test void withDataSourceAndTestcontainersConnectionString() throws SQLException { try (Connection connectionMock = Mockito.mock(Connection.class)) { diff --git a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthPropertiesTest.java b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthPropertiesTest.java index f15a5044..e5979217 100644 --- a/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthPropertiesTest.java +++ b/spring-boot-integration/pg-index-health-test-starter/src/test/java/io/github/mfvanek/pg/spring/DatabaseStructureHealthPropertiesTest.java @@ -12,22 +12,65 @@ import org.junit.jupiter.api.Test; +import static io.github.mfvanek.pg.spring.DatabaseStructureHealthProperties.STANDARD_DATASOURCE_BEAN_NAME; +import static io.github.mfvanek.pg.spring.DatabaseStructureHealthProperties.STANDARD_DATASOURCE_URL_PROPERTY_NAME; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; class DatabaseStructureHealthPropertiesTest { @Test - void getterShouldWork() { - final DatabaseStructureHealthProperties propertiesEnabled = new DatabaseStructureHealthProperties(true); + void getterShouldWorkWhenEnabled() { + final DatabaseStructureHealthProperties propertiesEnabled = + new DatabaseStructureHealthProperties(true, STANDARD_DATASOURCE_BEAN_NAME, STANDARD_DATASOURCE_URL_PROPERTY_NAME); assertThat(propertiesEnabled.isEnabled()) .isTrue(); + assertThat(propertiesEnabled.getDatasourceBeanName()) + .isEqualTo(STANDARD_DATASOURCE_BEAN_NAME); + assertThat(propertiesEnabled.getDatasourceUrlPropertyName()) + .isEqualTo(STANDARD_DATASOURCE_URL_PROPERTY_NAME); assertThat(propertiesEnabled) - .hasToString("DatabaseStructureHealthProperties{enabled=true}"); + .hasToString("DatabaseStructureHealthProperties{enabled=true, datasourceBeanName='dataSource', datasourceUrlPropertyName='spring.datasource.url'}"); + } - final DatabaseStructureHealthProperties propertiesDisabled = new DatabaseStructureHealthProperties(false); + @Test + void getterShouldWorkWhenDisabled() { + final DatabaseStructureHealthProperties propertiesDisabled = + new DatabaseStructureHealthProperties(false, "customDataSource", "custom.datasource.url"); assertThat(propertiesDisabled.isEnabled()) .isFalse(); + assertThat(propertiesDisabled.getDatasourceBeanName()) + .isEqualTo("customDataSource"); + assertThat(propertiesDisabled.getDatasourceUrlPropertyName()) + .isEqualTo("custom.datasource.url"); assertThat(propertiesDisabled) - .hasToString("DatabaseStructureHealthProperties{enabled=false}"); + .hasToString("DatabaseStructureHealthProperties{enabled=false, datasourceBeanName='customDataSource', datasourceUrlPropertyName='custom.datasource.url'}"); + } + + @Test + void shouldThrowExceptionWhenInvalidArgumentsArePassed() { + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("datasourceBeanName cannot be null"); + + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, "", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("datasourceBeanName cannot be blank"); + + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, " ", null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("datasourceBeanName cannot be blank"); + + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, "beanName", null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("datasourceUrlPropertyName cannot be null"); + + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, "beanName", "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("datasourceUrlPropertyName cannot be blank"); + + assertThatThrownBy(() -> new DatabaseStructureHealthProperties(true, "beanName", " ")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("datasourceUrlPropertyName cannot be blank"); } }