Refactor testcontainers service connections

Update restcontainers service connections support so that
technology specific `@ServiceConnector` annotations are not longer
required.

A single `@ServiceConnector` annotation can now be used to create
all `ConnectionDetail` beans.

Closes gh-35017
This commit is contained in:
Phillip Webb
2023-04-12 22:36:59 -07:00
parent 11dac5b5b7
commit 81a972af8d
78 changed files with 1297 additions and 903 deletions

View File

@@ -17,14 +17,17 @@
package org.springframework.boot.autoconfigure.service.connection;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Stream;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.core.style.ToStringCreator;
import org.springframework.util.Assert;
/**
* A registry of {@link ConnectionDetailsFactory} instances.
@@ -49,30 +52,55 @@ public class ConnectionDetailsFactories {
registrations.filter(Objects::nonNull).forEach(this.registrations::add);
}
public <S> ConnectionDetails getConnectionDetails(S source) {
return getConnectionDetailsFactory(source).getConnectionDetails(source);
/**
* Return a {@link Map} of {@link ConnectionDetails} interface type to
* {@link ConnectionDetails} instance created from the factories associated with the
* given source.
* @param <S> the source type
* @param source the source
* @return a list of {@link ConnectionDetails} instances.
*/
public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source) {
List<Registration<S, ?>> registrations = getRegistrations(source);
Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>();
for (Registration<S, ?> registration : registrations) {
ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source);
if (connectionDetails != null) {
Class<?> connectionDetailsType = registration.connectionDetailsType();
ConnectionDetails previous = result.put(connectionDetailsType, connectionDetails);
Assert.state(previous == null, () -> "Duplicate connection details supplied for %s"
.formatted(connectionDetailsType.getName()));
}
}
return Map.copyOf(result);
}
@SuppressWarnings("unchecked")
public <S> ConnectionDetailsFactory<S, ConnectionDetails> getConnectionDetailsFactory(S source) {
<S> List<Registration<S, ?>> getRegistrations(S source) {
Class<S> sourceType = (Class<S>) source.getClass();
List<ConnectionDetailsFactory<S, ConnectionDetails>> result = new ArrayList<>();
List<Registration<S, ?>> result = new ArrayList<>();
for (Registration<?, ?> candidate : this.registrations) {
if (candidate.sourceType().isAssignableFrom(sourceType)) {
result.add((ConnectionDetailsFactory<S, ConnectionDetails>) candidate.factory());
result.add((Registration<S, ?>) candidate);
}
}
if (result.isEmpty()) {
throw new ConnectionDetailsFactoryNotFoundException(source);
}
AnnotationAwareOrderComparator.sort(result);
return (result.size() != 1) ? new CompositeConnectionDetailsFactory<>(result) : result.get(0);
result.sort(Comparator.comparing(Registration::factory, AnnotationAwareOrderComparator.INSTANCE));
return List.copyOf(result);
}
/**
* A {@link ConnectionDetailsFactory} registration.
*
* @param <S> the source type
* @param <D> the connection details type
* @param sourceType the source type
* @param connectionDetailsType the connection details type
* @param factory the factory
*/
private record Registration<S, D extends ConnectionDetails>(Class<S> sourceType, Class<D> connectionDetailsType,
record Registration<S, D extends ConnectionDetails>(Class<S> sourceType, Class<D> connectionDetailsType,
ConnectionDetailsFactory<S, D> factory) {
@SuppressWarnings("unchecked")
@@ -87,37 +115,4 @@ public class ConnectionDetailsFactories {
}
/**
* Composite {@link ConnectionDetailsFactory} implementation.
*
* @param <S> the source type
*/
static class CompositeConnectionDetailsFactory<S> implements ConnectionDetailsFactory<S, ConnectionDetails> {
private final List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates;
CompositeConnectionDetailsFactory(List<ConnectionDetailsFactory<S, ConnectionDetails>> delegates) {
this.delegates = delegates;
}
@Override
public ConnectionDetails getConnectionDetails(S source) {
return this.delegates.stream()
.map((delegate) -> delegate.getConnectionDetails(source))
.filter(Objects::nonNull)
.findFirst()
.orElse(null);
}
List<ConnectionDetailsFactory<S, ConnectionDetails>> getDelegates() {
return this.delegates;
}
@Override
public String toString() {
return new ToStringCreator(this).append("delegates", this.delegates).toString();
}
}
}

View File

@@ -16,15 +16,18 @@
package org.springframework.boot.autoconfigure.service.connection;
import org.assertj.core.api.InstanceOfAssertFactories;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.CompositeConnectionDetailsFactory;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.Registration;
import org.springframework.core.Ordered;
import org.springframework.core.test.io.support.MockSpringFactoriesLoader;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
/**
* Tests for {@link ConnectionDetailsFactories}.
@@ -38,43 +41,50 @@ class ConnectionDetailsFactoriesTests {
private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader();
@Test
void getConnectionDetailsFactoryShouldThrowWhenNoFactoryForSource() {
void getConnectionDetailsWhenNoFactoryForSourceThrowsException() {
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
.isThrownBy(() -> factories.getConnectionDetailsFactory("source"));
.isThrownBy(() -> factories.getConnectionDetails("source"));
}
@Test
void getConnectionDetailsFactoryShouldReturnSingleFactoryWhenSourceHasOneMatch() {
void getConnectionDetailsWhenSourceHasOneMatchReturnsSingleResult() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).isInstanceOf(TestConnectionDetailsFactory.class);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source");
assertThat(connectionDetails).hasSize(1);
assertThat(connectionDetails.get(TestConnectionDetails.class)).isInstanceOf(TestConnectionDetailsImpl.class);
}
@Test
@SuppressWarnings("unchecked")
void getConnectionDetailsFactoryShouldReturnCompositeFactoryWhenSourceHasMultipleMatches() {
void getConnectionDetailsWhenSourceHasMultipleMatchesReturnsMultipleResults() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new OtherConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source");
assertThat(connectionDetails).hasSize(2);
}
@Test
void getConnectionDetailsWhenDuplicatesThrowsException() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
.satisfies((composite) -> assertThat(composite.getDelegates()).hasSize(2));
assertThatIllegalStateException().isThrownBy(() -> factories.getConnectionDetails("source"))
.withMessage("Duplicate connection details supplied for " + TestConnectionDetails.class.getName());
}
@Test
@SuppressWarnings("unchecked")
void compositeFactoryShouldHaveOrderedDelegates() {
void getRegistrationsReturnsOrderedDelegates() {
TestConnectionDetailsFactory orderOne = new TestConnectionDetailsFactory(1);
TestConnectionDetailsFactory orderTwo = new TestConnectionDetailsFactory(2);
TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3);
this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo);
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
ConnectionDetailsFactory<String, ConnectionDetails> factory = factories.getConnectionDetailsFactory("source");
assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class))
.satisfies((composite) -> assertThat(composite.getDelegates()).containsExactly(orderOne, orderTwo,
orderThree));
List<Registration<String, ?>> registrations = factories.getRegistrations("source");
assertThat(registrations.get(0).factory()).isEqualTo(orderOne);
assertThat(registrations.get(1).factory()).isEqualTo(orderTwo);
assertThat(registrations.get(2).factory()).isEqualTo(orderThree);
}
private static final class TestConnectionDetailsFactory
@@ -92,7 +102,7 @@ class ConnectionDetailsFactoriesTests {
@Override
public TestConnectionDetails getConnectionDetails(String source) {
return new TestConnectionDetails();
return new TestConnectionDetailsImpl();
}
@Override
@@ -102,11 +112,30 @@ class ConnectionDetailsFactoriesTests {
}
private static final class TestConnectionDetails implements ConnectionDetails {
private static final class OtherConnectionDetailsFactory
implements ConnectionDetailsFactory<String, OtherConnectionDetails> {
private TestConnectionDetails() {
@Override
public OtherConnectionDetails getConnectionDetails(String source) {
return new OtherConnectionDetailsImpl();
}
}
private interface TestConnectionDetails extends ConnectionDetails {
}
private static final class TestConnectionDetailsImpl implements TestConnectionDetails {
}
private interface OtherConnectionDetails extends ConnectionDetails {
}
private static final class OtherConnectionDetailsImpl implements OtherConnectionDetails {
}
}