From 8ec266bea469144d0c56d52e4f92d1beac9d2491 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 22 Mar 2023 12:37:43 +0000 Subject: [PATCH] Add infrastructure for pluggable connection details factories See gh-34658 Co-Authored-By: Phillip Webb Co-Authored-By: Mortitz Halbritter --- .../ConnectionDetailsFactories.java | 139 ++++++++++++++++++ .../connection/ConnectionDetailsFactory.java | 41 ++++++ ...ectionDetailsFactoryNotFoundException.java | 34 +++++ .../ConnectionDetailsFactoriesTests.java | 112 ++++++++++++++ 4 files changed, 326 insertions(+) create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactory.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoryNotFoundException.java create mode 100644 spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoriesTests.java diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java new file mode 100644 index 0000000000..01d53c65d9 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactories.java @@ -0,0 +1,139 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.io.support.SpringFactoriesLoader; +import org.springframework.core.style.ToStringCreator; + +/** + * A registry of {@link ConnectionDetailsFactory} instances. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class ConnectionDetailsFactories { + + private List registeredFactories = new ArrayList<>(); + + public ConnectionDetailsFactories() { + this(SpringFactoriesLoader.forDefaultResourceLocation(ConnectionDetailsFactory.class.getClassLoader())); + } + + @SuppressWarnings("rawtypes") + ConnectionDetailsFactories(SpringFactoriesLoader loader) { + List factories = loader.load(ConnectionDetailsFactory.class); + factories.stream().map(this::factoryDetails).filter(Objects::nonNull).forEach(this::register); + } + + @SuppressWarnings("unchecked") + private FactoryDetails factoryDetails(ConnectionDetailsFactory factory) { + ResolvableType connectionDetailsFactory = findConnectionDetailsFactory( + ResolvableType.forClass(factory.getClass())); + if (connectionDetailsFactory != null) { + ResolvableType input = connectionDetailsFactory.getGeneric(0); + ResolvableType output = connectionDetailsFactory.getGeneric(1); + return new FactoryDetails(input.getRawClass(), (Class) output.getRawClass(), + factory); + } + return null; + } + + private ResolvableType findConnectionDetailsFactory(ResolvableType type) { + try { + ResolvableType[] interfaces = type.getInterfaces(); + for (ResolvableType iface : interfaces) { + if (iface.getRawClass().equals(ConnectionDetailsFactory.class)) { + return iface; + } + } + } + catch (TypeNotPresentException ex) { + // A type referenced by the factory is not present. Skip it. + } + ResolvableType superType = type.getSuperType(); + return ResolvableType.NONE.equals(superType) ? null : findConnectionDetailsFactory(superType); + } + + private void register(FactoryDetails details) { + this.registeredFactories.add(details); + } + + @SuppressWarnings("unchecked") + public ConnectionDetailsFactory getConnectionDetailsFactory(S source) { + Class input = (Class) source.getClass(); + List> matchingFactories = new ArrayList<>(); + for (FactoryDetails factoryDetails : this.registeredFactories) { + if (factoryDetails.input.isAssignableFrom(input)) { + matchingFactories.add((ConnectionDetailsFactory) factoryDetails.factory); + } + } + if (matchingFactories.isEmpty()) { + throw new ConnectionDetailsFactoryNotFoundException(source); + } + else { + if (matchingFactories.size() == 1) { + return matchingFactories.get(0); + } + AnnotationAwareOrderComparator.sort(matchingFactories); + return new CompositeConnectionDetailsFactory<>(matchingFactories); + } + } + + private record FactoryDetails(Class input, Class output, + ConnectionDetailsFactory factory) { + } + + static class CompositeConnectionDetailsFactory implements ConnectionDetailsFactory { + + private final List> delegates; + + CompositeConnectionDetailsFactory(List> delegates) { + this.delegates = delegates; + } + + @Override + @SuppressWarnings("unchecked") + public ConnectionDetails getConnectionDetails(Object source) { + for (ConnectionDetailsFactory delegate : this.delegates) { + ConnectionDetails connectionDetails = delegate.getConnectionDetails((S) source); + if (connectionDetails != null) { + return connectionDetails; + } + } + return null; + } + + @Override + public String toString() { + return new ToStringCreator(this).append("delegates", this.delegates).toString(); + } + + List> getDelegates() { + return this.delegates; + } + + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactory.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactory.java new file mode 100644 index 0000000000..e44739ab63 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactory.java @@ -0,0 +1,41 @@ +/* + * 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; + +/** + * A factory to create {@link ConnectionDetails} from a given {@code source}. + * Implementations should be registered in {@code META-INF/spring.factories}. + * + * @param the source type accepted by the factory. Implementations are expected to + * provide a valid {@code toString}. + * @param the type of {@link ConnectionDetails} produced by the factory + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public interface ConnectionDetailsFactory { + + /** + * Get the {@link ConnectionDetails} from the given {@code source}. May return + * {@code null} if no details can be created. + * @param source the source + * @return the connection details or {@code null} + */ + D getConnectionDetails(S source); + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoryNotFoundException.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoryNotFoundException.java new file mode 100644 index 0000000000..f618da5f01 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoryNotFoundException.java @@ -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 a {@link ConnectionDetailsFactory} could not be + * found. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + * @since 3.1.0 + */ +public class ConnectionDetailsFactoryNotFoundException extends RuntimeException { + + public ConnectionDetailsFactoryNotFoundException(S source) { + super("No ConnectionDetailsFactory found for source '" + source + "'"); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoriesTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoriesTests.java new file mode 100644 index 0000000000..42fe9a672d --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/service/connection/ConnectionDetailsFactoriesTests.java @@ -0,0 +1,112 @@ +/* + * 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; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.service.connection.ConnectionDetailsFactories.CompositeConnectionDetailsFactory; +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; + +/** + * Tests for {@link ConnectionDetailsFactories}. + * + * @author Moritz Halbritter + * @author Andy Wilkinson + * @author Phillip Webb + */ +class ConnectionDetailsFactoriesTests { + + private final MockSpringFactoriesLoader loader = new MockSpringFactoriesLoader(); + + @Test + void getConnectionDetailsFactoryShouldThrowWhenNoFactoryForSource() { + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); + assertThatExceptionOfType(ConnectionDetailsFactoryNotFoundException.class) + .isThrownBy(() -> factories.getConnectionDetailsFactory("source")); + } + + @Test + void getConnectionDetailsFactoryShouldReturnSingleFactoryWhenSourceHasOneMatch() { + this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory()); + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); + ConnectionDetailsFactory factory = factories.getConnectionDetailsFactory("source"); + assertThat(factory).isInstanceOf(TestConnectionDetailsFactory.class); + } + + @Test + @SuppressWarnings("unchecked") + void getConnectionDetailsFactoryShouldReturnCompositeFactoryWhenSourceHasMultipleMatches() { + this.loader.addInstance(ConnectionDetailsFactory.class, new TestConnectionDetailsFactory(), + new TestConnectionDetailsFactory()); + ConnectionDetailsFactories factories = new ConnectionDetailsFactories(this.loader); + ConnectionDetailsFactory factory = factories.getConnectionDetailsFactory("source"); + assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class)) + .satisfies((composite) -> assertThat(composite.getDelegates()).hasSize(2)); + } + + @Test + @SuppressWarnings("unchecked") + void compositeFactoryShouldHaveOrderedDelegates() { + 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 factory = factories.getConnectionDetailsFactory("source"); + assertThat(factory).asInstanceOf(InstanceOfAssertFactories.type(CompositeConnectionDetailsFactory.class)) + .satisfies((composite) -> assertThat(composite.getDelegates()).containsExactly(orderOne, orderTwo, + orderThree)); + } + + private static final class TestConnectionDetailsFactory + implements ConnectionDetailsFactory, Ordered { + + private final int order; + + private TestConnectionDetailsFactory() { + this(0); + } + + private TestConnectionDetailsFactory(int order) { + this.order = order; + } + + @Override + public TestConnectionDetails getConnectionDetails(String source) { + return new TestConnectionDetails(); + } + + @Override + public int getOrder() { + return this.order; + } + + } + + private static final class TestConnectionDetails implements ConnectionDetails { + + private TestConnectionDetails() { + } + + } + +}