Skip to content

Commit

Permalink
Add ability to construct PgConnection from URL (#522)
Browse files Browse the repository at this point in the history
  • Loading branch information
mfvanek authored Dec 3, 2024
1 parent f1958cf commit 395626b
Show file tree
Hide file tree
Showing 6 changed files with 130 additions and 54 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/**
Expand All @@ -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");
}

Expand All @@ -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}
*/
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
* <p>
* 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.
* </p>
*
* @see <a href="https://java.testcontainers.org/modules/databases/jdbc/">Testcontainers JDBC Support</a>
* @since 0.14.2
*/
public static final String TESTCONTAINERS_PG_URL_PREFIX = "jdbc:tc:postgresql:";

private static final Map<String, String> DEFAULT_URL_PARAMETERS = Map.ofEntries(
Map.entry("targetServerType", "primary"),
Map.entry("hostRecheckSeconds", "2"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +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.support.DatabaseAwareTestBase;
import nl.jqno.equalsverifier.EqualsVerifier;
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;

Expand Down Expand Up @@ -46,18 +49,26 @@ 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")
@Test
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();
Expand Down Expand Up @@ -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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -24,19 +25,6 @@
*/
public class DatabaseStructureHealthCondition extends SpringBootCondition {

/**
* The URL prefix used in Testcontainers to initialize PostgreSQL containers.
* <p>
* 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.
* </p>
*
* @see <a href="https://java.testcontainers.org/modules/databases/jdbc/">Testcontainers JDBC Support</a>
*/
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";

/**
Expand All @@ -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"));
Expand Down

0 comments on commit 395626b

Please sign in to comment.