Add 'required' parameter to ConnectionDetailsFactories

Update `ConnectionDetailsFactories` so that callers can now declare if
a result is required or not and improve exception hierarchy.

See gh-35168
pull/35165/head
Phillip Webb 2 years ago
parent 2b261e6ebd
commit 403481ff96

@ -65,10 +65,16 @@ public class ConnectionDetailsFactories {
* given source. * given source.
* @param <S> the source type * @param <S> the source type
* @param source the source * @param source the source
* @param required if a connection details result is required
* @return a map of {@link ConnectionDetails} instances * @return a map of {@link ConnectionDetails} instances
* @throws ConnectionDetailsFactoryNotFoundException if a result is required but no
* connection details factory is registered for the source
* @throws ConnectionDetailsNotFoundException if a result is required but no
* connection details instance was created from a registered factory
*/ */
public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source) { public <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source, boolean required)
List<Registration<S, ?>> registrations = getRegistrations(source); throws ConnectionDetailsFactoryNotFoundException, ConnectionDetailsNotFoundException {
List<Registration<S, ?>> registrations = getRegistrations(source, required);
Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>(); Map<Class<?>, ConnectionDetails> result = new LinkedHashMap<>();
for (Registration<S, ?> registration : registrations) { for (Registration<S, ?> registration : registrations) {
ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source); ConnectionDetails connectionDetails = registration.factory().getConnectionDetails(source);
@ -79,11 +85,14 @@ public class ConnectionDetailsFactories {
.formatted(connectionDetailsType.getName())); .formatted(connectionDetailsType.getName()));
} }
} }
if (required && result.isEmpty()) {
throw new ConnectionDetailsNotFoundException(source);
}
return Map.copyOf(result); return Map.copyOf(result);
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
<S> List<Registration<S, ?>> getRegistrations(S source) { <S> List<Registration<S, ?>> getRegistrations(S source, boolean required) {
Class<S> sourceType = (Class<S>) source.getClass(); Class<S> sourceType = (Class<S>) source.getClass();
List<Registration<S, ?>> result = new ArrayList<>(); List<Registration<S, ?>> result = new ArrayList<>();
for (Registration<?, ?> candidate : this.registrations) { for (Registration<?, ?> candidate : this.registrations) {
@ -91,7 +100,7 @@ public class ConnectionDetailsFactories {
result.add((Registration<S, ?>) candidate); result.add((Registration<S, ?>) candidate);
} }
} }
if (result.isEmpty()) { if (required && result.isEmpty()) {
throw new ConnectionDetailsFactoryNotFoundException(source); throw new ConnectionDetailsFactoryNotFoundException(source);
} }
result.sort(Comparator.comparing(Registration::factory, AnnotationAwareOrderComparator.INSTANCE)); result.sort(Comparator.comparing(Registration::factory, AnnotationAwareOrderComparator.INSTANCE));

@ -28,7 +28,7 @@ package org.springframework.boot.autoconfigure.service.connection;
public class ConnectionDetailsFactoryNotFoundException extends RuntimeException { public class ConnectionDetailsFactoryNotFoundException extends RuntimeException {
public <S> ConnectionDetailsFactoryNotFoundException(S source) { public <S> ConnectionDetailsFactoryNotFoundException(S source) {
super("No ConnectionDetailsFactory found for source '" + source + "'"); super("No ConnectionDetailsFactory found for source '%s'".formatted(source));
} }
} }

@ -0,0 +1,34 @@
/*
* Copyright 2012-2023 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.service.connection;
/**
* {@link RuntimeException} thrown when required {@link ConnectionDetails} could not be
* found.
*
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @since 3.1.0
*/
public class ConnectionDetailsNotFoundException extends RuntimeException {
public <S> ConnectionDetailsNotFoundException(S source) {
super("No ConnectionDetails found for source '%s'".formatted(source));
}
}

@ -42,27 +42,49 @@ class ConnectionDetailsFactoriesTests {
private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader();
@Test @Test
void getConnectionDetailsWhenNoFactoryForSourceThrowsException() { void getRequiredConnectionDetailsWhenNoFactoryForSourceThrowsException() {
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class) assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class)
.isThrownBy(() -> factories.getConnectionDetails("source")); .isThrownBy(() -> factories.getConnectionDetails("source", true));
}
@Test
void getOptionalConnectionDetailsWhenNoFactoryForSourceThrowsException() {
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThat(factories.getConnectionDetails("source", false)).isEmpty();
} }
@Test @Test
void getConnectionDetailsWhenSourceHasOneMatchReturnsSingleResult() { void getConnectionDetailsWhenSourceHasOneMatchReturnsSingleResult() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory()); this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source"); Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source", false);
assertThat(connectionDetails).hasSize(1); assertThat(connectionDetails).hasSize(1);
assertThat(connectionDetails.get(TestConnectionDetails.class)).isInstanceOf(TestConnectionDetailsImpl.class); assertThat(connectionDetails.get(TestConnectionDetails.class)).isInstanceOf(TestConnectionDetailsImpl.class);
} }
@Test
void getRequiredConnectionDetailsWhenSourceHasNoMatchTheowsException() {
this.loader.addInstance(ConnectionDetailsFactory.class, new NullResultTestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatExceptionOfType(ConnectionDetailsNotFoundException.class)
.isThrownBy(() -> factories.getConnectionDetails("source", true));
}
@Test
void getOptionalConnectionDetailsWhenSourceHasNoMatchReturnsEmptyMap() {
this.loader.addInstance(ConnectionDetailsFactory.class, new NullResultTestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source", false);
assertThat(connectionDetails).isEmpty();
}
@Test @Test
void getConnectionDetailsWhenSourceHasMultipleMatchesReturnsMultipleResults() { void getConnectionDetailsWhenSourceHasMultipleMatchesReturnsMultipleResults() {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(), this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new OtherConnectionDetailsFactory()); new OtherConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source"); Map<Class<?>, ConnectionDetails> connectionDetails = factories.getConnectionDetails("source", false);
assertThat(connectionDetails).hasSize(2); assertThat(connectionDetails).hasSize(2);
} }
@ -71,7 +93,7 @@ class ConnectionDetailsFactoriesTests {
this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(), this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(),
new TestConnectionDetailsFactory()); new TestConnectionDetailsFactory());
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
assertThatIllegalStateException().isThrownBy(() -> factories.getConnectionDetails("source")) assertThatIllegalStateException().isThrownBy(() -> factories.getConnectionDetails("source", false))
.withMessage("Duplicate connection details supplied for " + TestConnectionDetails.class.getName()); .withMessage("Duplicate connection details supplied for " + TestConnectionDetails.class.getName());
} }
@ -82,7 +104,7 @@ class ConnectionDetailsFactoriesTests {
TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3); TestConnectionDetailsFactory orderThree = new TestConnectionDetailsFactory(3);
this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo); this.loader.addInstance(ConnectionDetailsFactory.class, orderOne, orderThree, orderTwo);
ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader);
List<Registration<String, ?>> registrations = factories.getRegistrations("source"); List<Registration<String, ?>> registrations = factories.getRegistrations("source", false);
assertThat(registrations.get(0).factory()).isEqualTo(orderOne); assertThat(registrations.get(0).factory()).isEqualTo(orderOne);
assertThat(registrations.get(1).factory()).isEqualTo(orderTwo); assertThat(registrations.get(1).factory()).isEqualTo(orderTwo);
assertThat(registrations.get(2).factory()).isEqualTo(orderThree); assertThat(registrations.get(2).factory()).isEqualTo(orderThree);
@ -119,6 +141,16 @@ class ConnectionDetailsFactoriesTests {
} }
private static final class NullResultTestConnectionDetailsFactory
implements ConnectionDetailsFactory<String, TestConnectionDetails> {
@Override
public TestConnectionDetails getConnectionDetails(String source) {
return null;
}
}
private static final class OtherConnectionDetailsFactory private static final class OtherConnectionDetailsFactory
implements ConnectionDetailsFactory<String, OtherConnectionDetails> { implements ConnectionDetailsFactory<String, OtherConnectionDetails> {

@ -65,7 +65,7 @@ class DockerComposeServiceConnectionsApplicationListener
private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) { private void registerConnectionDetails(BeanDefinitionRegistry registry, List<RunningService> runningServices) {
for (RunningService runningService : runningServices) { for (RunningService runningService : runningServices) {
DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService); DockerComposeConnectionSource source = new DockerComposeConnectionSource(runningService);
this.factories.getConnectionDetails(source) this.factories.getConnectionDetails(source, false)
.forEach((connectionDetailsType, connectionDetails) -> register(registry, runningService, .forEach((connectionDetailsType, connectionDetails) -> register(registry, runningService,
connectionDetailsType, connectionDetails)); connectionDetailsType, connectionDetails));
} }

@ -19,7 +19,6 @@ package org.springframework.boot.testcontainers.service.connection;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.function.Supplier; import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@ -32,7 +31,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories;
import org.springframework.core.log.LogMessage; import org.springframework.core.log.LogMessage;
import org.springframework.util.Assert;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils; import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@ -67,18 +65,11 @@ class ContainerConnectionSourcesRegistrar {
} }
private void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source) { private void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source) {
getConnectionDetails(source) this.connectionDetailsFactories.getConnectionDetails(source, true)
.forEach((connectionDetailsType, connectionDetails) -> registerBeanDefinition(registry, source, .forEach((connectionDetailsType, connectionDetails) -> registerBeanDefinition(registry, source,
connectionDetailsType, connectionDetails)); connectionDetailsType, connectionDetails));
} }
private <S> Map<Class<?>, ConnectionDetails> getConnectionDetails(S source) {
Map<Class<?>, ConnectionDetails> connectionDetails = this.connectionDetailsFactories
.getConnectionDetails(source);
Assert.state(!connectionDetails.isEmpty(), () -> "No connection details created for %s".formatted(source));
return connectionDetails;
}
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private <T> void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source, private <T> void registerBeanDefinition(BeanDefinitionRegistry registry, ContainerConnectionSource<?> source,
Class<?> connectionDetailsType, ConnectionDetails connectionDetails) { Class<?> connectionDetailsType, ConnectionDetails connectionDetails) {

@ -36,7 +36,6 @@ import org.springframework.core.annotation.MergedAnnotation;
import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.MergedContextConfiguration;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given; import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then; import static org.mockito.BDDMockito.then;
@ -85,7 +84,7 @@ class ServiceConnectionContextCustomizerTests {
given(context.getBeanFactory()).willReturn(beanFactory); given(context.getBeanFactory()).willReturn(beanFactory);
MergedContextConfiguration mergedConfig = mock(MergedContextConfiguration.class); MergedContextConfiguration mergedConfig = mock(MergedContextConfiguration.class);
JdbcConnectionDetails connectionDetails = new TestJdbcConnectionDetails(); JdbcConnectionDetails connectionDetails = new TestJdbcConnectionDetails();
given(this.factories.getConnectionDetails(this.source)) given(this.factories.getConnectionDetails(this.source, true))
.willReturn(Map.of(JdbcConnectionDetails.class, connectionDetails)); .willReturn(Map.of(JdbcConnectionDetails.class, connectionDetails));
customizer.customizeContext(context, mergedConfig); customizer.customizeContext(context, mergedConfig);
ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class); ArgumentCaptor<BeanDefinition> beanDefinitionCaptor = ArgumentCaptor.forClass(BeanDefinition.class);
@ -96,18 +95,6 @@ class ServiceConnectionContextCustomizerTests {
assertThat(beanDefinition.getBeanClass()).isEqualTo(TestJdbcConnectionDetails.class); assertThat(beanDefinition.getBeanClass()).isEqualTo(TestJdbcConnectionDetails.class);
} }
@Test
void customizeContextWhenFactoriesHasNoConnectionDetailsThrowsException() {
ServiceConnectionContextCustomizer customizer = new ServiceConnectionContextCustomizer(List.of(this.source),
this.factories);
ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class);
DefaultListableBeanFactory beanFactory = spy(new DefaultListableBeanFactory());
given(context.getBeanFactory()).willReturn(beanFactory);
MergedContextConfiguration mergedConfig = mock(MergedContextConfiguration.class);
assertThatIllegalStateException().isThrownBy(() -> customizer.customizeContext(context, mergedConfig))
.withMessageStartingWith("No connection details created for @ServiceConnection source");
}
/** /**
* Test {@link JdbcConnectionDetails}. * Test {@link JdbcConnectionDetails}.
*/ */

Loading…
Cancel
Save