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
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…
Reference in New Issue