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:
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user