diff --git a/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/PgConnectionImpl.java b/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/PgConnectionImpl.java index 5cb2cf45..39930238 100644 --- a/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/PgConnectionImpl.java +++ b/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/PgConnectionImpl.java @@ -10,10 +10,16 @@ package io.github.mfvanek.pg.connection; +import io.github.mfvanek.pg.connection.exception.PgSqlException; import io.github.mfvanek.pg.connection.host.PgHost; +import io.github.mfvanek.pg.connection.host.PgHostImpl; +import io.github.mfvanek.pg.connection.host.PgUrlParser; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Objects; import javax.annotation.Nonnull; +import javax.annotation.Nullable; import javax.sql.DataSource; /** @@ -28,7 +34,7 @@ public class PgConnectionImpl implements PgConnection { private final PgHost host; private PgConnectionImpl(@Nonnull final DataSource dataSource, @Nonnull final PgHost host) { - this.dataSource = Objects.requireNonNull(dataSource, "dataSource cannot be null"); + this.dataSource = validateDataSource(dataSource); this.host = Objects.requireNonNull(host, "host cannot be null"); } @@ -50,20 +56,6 @@ public PgHost getHost() { return host; } - /** - * Constructs a {@code PgConnection} object with given dataSource and host. - * - * @param dataSource a factory for connections to the physical database - * @param host information about database host - * @return {@code PgConnection} - * @see DataSource - * @see PgHost - */ - @Nonnull - public static PgConnection of(@Nonnull final DataSource dataSource, @Nonnull final PgHost host) { - return new PgConnectionImpl(dataSource, host); - } - /** * {@inheritDoc} */ @@ -99,4 +91,54 @@ public String toString() { "host=" + host + '}'; } + + /** + * Constructs a {@code PgConnection} object with given dataSource and host. + * + * @param dataSource a factory for connections to the physical database; should be non-null. + * @param host information about database host; should be non-null. + * @return {@code PgConnection} + * @see DataSource + * @see PgHost + */ + @Nonnull + public static PgConnection of(@Nonnull final DataSource dataSource, @Nonnull final PgHost host) { + return new PgConnectionImpl(dataSource, host); + } + + /** + * Constructs a {@code PgConnection} object with given dataSource and connection string. + * + * @param dataSource a factory for connections to the physical database; should be non-null. + * @param databaseUrl a connection string to the physical database; can be obtained from connection metadata. + * @return {@code PgConnection} object + * @see Connection#getMetaData() + * @see java.sql.DatabaseMetaData + * @since 0.14.2 + */ + @Nonnull + public static PgConnection ofUrl(@Nonnull final DataSource dataSource, @Nullable final String databaseUrl) { + final PgHost host; + if (needToGetUrlFromMetaData(databaseUrl)) { + try (Connection connection = validateDataSource(dataSource).getConnection()) { + host = PgHostImpl.ofUrl(connection.getMetaData().getURL()); + } catch (SQLException ex) { + throw new PgSqlException(ex); + } + } else { + host = PgHostImpl.ofUrl(databaseUrl); + } + return new PgConnectionImpl(dataSource, host); + } + + @Nonnull + private static DataSource validateDataSource(@Nonnull final DataSource dataSource) { + return Objects.requireNonNull(dataSource, "dataSource cannot be null"); + } + + private static boolean needToGetUrlFromMetaData(@Nullable final String databaseUrl) { + return databaseUrl == null || + databaseUrl.isBlank() || + databaseUrl.startsWith(PgUrlParser.TESTCONTAINERS_PG_URL_PREFIX); + } } diff --git a/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/host/PgUrlParser.java b/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/host/PgUrlParser.java index 2244238d..42182667 100644 --- a/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/host/PgUrlParser.java +++ b/pg-index-health-jdbc-connection/src/main/java/io/github/mfvanek/pg/connection/host/PgUrlParser.java @@ -31,6 +31,19 @@ public final class PgUrlParser { */ public static final String URL_HEADER = "jdbc:postgresql://"; + /** + * The URL prefix used in Testcontainers to initialize PostgreSQL containers. + *

+ * Testcontainers provides a special JDBC URL format that allows for on-the-fly creation and management + * of PostgreSQL database containers during tests. This prefix is part of the JDBC URL and signals + * Testcontainers to handle the lifecycle of the container automatically. + *

+ * + * @see Testcontainers JDBC Support + * @since 0.14.2 + */ + public static final String TESTCONTAINERS_PG_URL_PREFIX = "jdbc:tc:postgresql:"; + private static final Map DEFAULT_URL_PARAMETERS = Map.ofEntries( Map.entry("targetServerType", "primary"), Map.entry("hostRecheckSeconds", "2"), diff --git a/pg-index-health-jdbc-connection/src/test/java/io/github/mfvanek/pg/connection/PgConnectionImplTest.java b/pg-index-health-jdbc-connection/src/test/java/io/github/mfvanek/pg/connection/PgConnectionImplTest.java index 65a5b5c2..6b59a037 100644 --- a/pg-index-health-jdbc-connection/src/test/java/io/github/mfvanek/pg/connection/PgConnectionImplTest.java +++ b/pg-index-health-jdbc-connection/src/test/java/io/github/mfvanek/pg/connection/PgConnectionImplTest.java @@ -10,6 +10,7 @@ package io.github.mfvanek.pg.connection; +import io.github.mfvanek.pg.connection.exception.PgSqlException; import io.github.mfvanek.pg.connection.host.PgHost; import io.github.mfvanek.pg.connection.host.PgHostImpl; import io.github.mfvanek.pg.connection.support.DatabaseAwareTestBase; @@ -17,6 +18,8 @@ import org.junit.jupiter.api.Test; import org.mockito.Mockito; +import java.sql.Connection; +import java.sql.SQLException; import java.util.Locale; import javax.sql.DataSource; @@ -46,10 +49,18 @@ void isPrimaryForAnyHost() { @SuppressWarnings("ConstantConditions") @Test void withInvalidArguments() { + assertThatThrownBy(() -> PgConnectionImpl.of(null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("dataSource cannot be null"); + final DataSource dataSource = getDataSource(); assertThatThrownBy(() -> PgConnectionImpl.of(dataSource, null)) .isInstanceOf(NullPointerException.class) .hasMessage("host cannot be null"); + + assertThatThrownBy(() -> PgConnectionImpl.ofUrl(null, null)) + .isInstanceOf(NullPointerException.class) + .hasMessage("dataSource cannot be null"); } @SuppressWarnings("ConstantConditions") @@ -57,7 +68,7 @@ void withInvalidArguments() { void equalsAndHashCode() { final PgHost host = PgHostImpl.ofUrl("jdbc:postgresql://first:6432"); final PgConnection first = PgConnectionImpl.of(getDataSource(), host); - final PgConnection theSame = PgConnectionImpl.of(getDataSource(), host); + final PgConnection theSame = PgConnectionImpl.ofUrl(getDataSource(), "jdbc:postgresql://first:6432"); final PgConnection second = PgConnectionImpl.of(getDataSource(), PgHostImpl.ofUrl("jdbc:postgresql://second:5432")); assertThat(first.equals(null)).isFalse(); @@ -107,4 +118,38 @@ void twoConnectionsDifferentSameHostWithDifferentPortsConsideredNotEqual() { assertThat(firstPgConnection).isNotEqualTo(secondPgConnection); } + + @Test + void shouldGetUrlFromConnectionMetadata() { + final PgConnection first = PgConnectionImpl.ofUrl(getDataSource(), null); + assertThat(first.getHost()) + .isNotNull() + .isEqualTo(getHost()); + + final PgConnection second = PgConnectionImpl.ofUrl(getDataSource(), " "); + assertThat(second.getHost()) + .isNotNull() + .isEqualTo(getHost()); + + final PgConnection third = PgConnectionImpl.ofUrl(getDataSource(), "jdbc:tc:postgresql:17.2:///demo"); + assertThat(third.getHost()) + .isNotNull() + .isEqualTo(getHost()); + } + + @Test + void withExceptionWhileObtainingUrlFromMetadata() throws SQLException { + final DataSource dataSourceMock = Mockito.mock(DataSource.class); + try (Connection connectionMock = Mockito.mock(Connection.class)) { + Mockito.when(dataSourceMock.getConnection()) + .thenReturn(connectionMock); + Mockito.when(connectionMock.getMetaData()) + .thenThrow(new SQLException("Unable to obtain connection from metadata")); + + assertThatThrownBy(() -> PgConnectionImpl.ofUrl(dataSourceMock, null)) + .isInstanceOf(PgSqlException.class) + .hasMessage("Unable to obtain connection from metadata") + .hasCauseInstanceOf(SQLException.class); + } + } } diff --git a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/units/MemoryUnit.java b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/units/MemoryUnit.java index b4ee450e..2b56e9a8 100644 --- a/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/units/MemoryUnit.java +++ b/pg-index-health-model/src/main/java/io/github/mfvanek/pg/model/units/MemoryUnit.java @@ -20,8 +20,19 @@ */ public enum MemoryUnit { + /** + * Represents a kilobyte, equivalent to 1,024 bytes. + */ KB(1024L, "kilobyte"), + + /** + * Represents a megabyte, equivalent to 1,024 kilobytes or 1,048,576 bytes. + */ MB(1024L * 1024L, "megabyte"), + + /** + * Represents a gigabyte, equivalent to 1,024 megabytes or 1,073,741,824 bytes. + */ GB(1024L * 1024L * 1024L, "gigabyte"); private final long dimension; 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 db099501..85bc166c 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,9 +12,6 @@ import io.github.mfvanek.pg.connection.PgConnection; import io.github.mfvanek.pg.connection.PgConnectionImpl; -import io.github.mfvanek.pg.connection.exception.PgSqlException; -import io.github.mfvanek.pg.connection.host.PgHost; -import io.github.mfvanek.pg.connection.host.PgHostImpl; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.AutoConfiguration; @@ -27,10 +24,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; -import java.sql.Connection; -import java.sql.SQLException; -import java.util.Objects; -import javax.annotation.Nullable; import javax.sql.DataSource; /** @@ -58,22 +51,6 @@ public class DatabaseStructureHealthAutoConfiguration { @ConditionalOnMissingBean public PgConnection pgConnection(@Qualifier("dataSource") final DataSource dataSource, @Value("${spring.datasource.url:#{null}}") final String databaseUrl) { - final PgHost host; - if (needToGetUrlFromMetaData(databaseUrl)) { - try (Connection connection = dataSource.getConnection()) { - host = PgHostImpl.ofUrl(connection.getMetaData().getURL()); - } catch (SQLException ex) { - throw new PgSqlException(ex); - } - } else { - host = PgHostImpl.ofUrl(databaseUrl); - } - return PgConnectionImpl.of(dataSource, host); - } - - private static boolean needToGetUrlFromMetaData(@Nullable final String databaseUrl) { - return Objects.isNull(databaseUrl) || - databaseUrl.isBlank() || - databaseUrl.startsWith(DatabaseStructureHealthCondition.TESTCONTAINERS_PG_URL_PREFIX); + 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 3e812ed9..09877d20 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 @@ -10,6 +10,7 @@ package io.github.mfvanek.pg.spring; +import io.github.mfvanek.pg.connection.host.PgUrlParser; import org.springframework.boot.autoconfigure.condition.ConditionMessage; import org.springframework.boot.autoconfigure.condition.ConditionOutcome; import org.springframework.boot.autoconfigure.condition.SpringBootCondition; @@ -24,19 +25,6 @@ */ public class DatabaseStructureHealthCondition extends SpringBootCondition { - /** - * The URL prefix used in Testcontainers to initialize PostgreSQL containers. - *

- * Testcontainers provides a special JDBC URL format that allows for on-the-fly creation and management - * of PostgreSQL database containers during tests. This prefix is part of the JDBC URL and signals - * Testcontainers to handle the lifecycle of the container automatically. - *

- * - * @see Testcontainers JDBC Support - */ - static final String TESTCONTAINERS_PG_URL_PREFIX = "jdbc:tc:postgresql:"; - - private static final String ORIGINAL_PG_URL_PREFIX = "jdbc:postgresql://"; private static final String PROPERTY_NAME = "spring.datasource.url"; /** @@ -47,7 +35,7 @@ public ConditionOutcome getMatchOutcome(final ConditionContext context, final An final ConditionMessage.Builder message = ConditionMessage.forCondition("pg.index.health.test PostgreSQL condition"); final String jdbcUrl = getJdbcUrl(context); if (jdbcUrl != null && !jdbcUrl.isBlank()) { - if (jdbcUrl.startsWith(ORIGINAL_PG_URL_PREFIX) || jdbcUrl.startsWith(TESTCONTAINERS_PG_URL_PREFIX)) { + 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"));