Add infrastructure for pluggable connection details factories

See gh-34658

Co-Authored-By: Phillip Webb <pwebb@vmware.com>
Co-Authored-By: Mortitz Halbritter <mkammerer@vmware.com>
pull/34759/head
Andy Wilkinson 2 years ago
parent 8e4b8a869e
commit 8ec266bea4

@ -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<FactoryDetails> registeredFactories = new ArrayList<>();
public ConnectionDetailsFactories() {
this(SpringFactoriesLoader.forDefaultResourceLocation(ConnectionDetailsFactory.class.getClassLoader()));
}
@SuppressWarnings("rawtypes")
ConnectionDetailsFactories(SpringFactoriesLoader loader) {
List<ConnectionDetailsFactory> 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<? extends ConnectionDetails>) 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 <S> ConnectionDetailsFactory<S, ConnectionDetails> getConnectionDetailsFactory(S source) {
Class<S> input = (Class<S>) source.getClass();
List<ConnectionDetailsFactory<S, ConnectionDetails>> matchingFactories = new ArrayList<>();
for (FactoryDetails factoryDetails : this.registeredFactories) {
if (factoryDetails.input.isAssignableFrom(input)) {
matchingFactories.add((ConnectionDetailsFactory<S, ConnectionDetails>) 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<? extends ConnectionDetails> output,
ConnectionDetailsFactory<?, ?> factory) {
}
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
@SuppressWarnings("unchecked")
public ConnectionDetails getConnectionDetails(Object source) {
for (ConnectionDetailsFactory<S, ConnectionDetails> 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<ConnectionDetailsFactory<S, ConnectionDetails>> getDelegates() {
return this.delegates;
}
}
}

@ -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 <S> the source type accepted by the factory. Implementations are expected to
* provide a valid {@code toString}.
* @param <D> 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<S, D extends ConnectionDetails> {
/**
* 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);
}

@ -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 <S> ConnectionDetailsFactoryNotFoundException(S source) {
super("No ConnectionDetailsFactory found for source '" + source + "'");
}
}

@ -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<String, ConnectionDetails> 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<String, ConnectionDetails> 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<String, ConnectionDetails> 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<String, TestConnectionDetails>, 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() {
}
}
}
Loading…
Cancel
Save