Add Adapter api (#6450)

Lightweight implementation of the adapter pattern.
This commit is contained in:
Michael Buckley 2024-11-06 20:30:55 -05:00 committed by GitHub
parent 6c4c6bde2c
commit 7faf0f7731
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 347 additions and 0 deletions

View File

@ -0,0 +1,43 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Stream;
public class AdapterManager implements IAdapterManager {
public static final AdapterManager INSTANCE = new AdapterManager();
Set<IAdapterFactory> myAdapterFactories = new HashSet<>();
/**
* Hidden to force shared use of the public INSTANCE.
*/
AdapterManager() {}
public <T> @Nonnull Optional<T> getAdapter(Object theObject, Class<T> theTargetType) {
// todo this can be sped up with a cache of type->Factory.
return myAdapterFactories.stream()
.filter(nextFactory -> nextFactory.getAdapters().stream().anyMatch(theTargetType::isAssignableFrom))
.flatMap(nextFactory -> {
var adapter = nextFactory.getAdapter(theObject, theTargetType);
// can't use Optional.stream() because of our Android target is API level 26/JDK 8.
if (adapter.isPresent()) {
return Stream.of(adapter.get());
} else {
return Stream.empty();
}
})
.findFirst();
}
public void registerFactory(@Nonnull IAdapterFactory theFactory) {
myAdapterFactories.add(theFactory);
}
public void unregisterFactory(@Nonnull IAdapterFactory theFactory) {
myAdapterFactories.remove(theFactory);
}
}

View File

@ -0,0 +1,29 @@
package ca.uhn.fhir.util.adapters;
import java.util.Optional;
public class AdapterUtils {
/**
* Main entry point for adapter calls.
* Implements three conversions: cast to the target type, use IAdaptable if present, or lastly try the AdapterManager.INSTANCE.
* @param theObject the object to be adapted
* @param theTargetType the type of the adapter requested
*/
static <T> Optional<T> adapt(Object theObject, Class<T> theTargetType) {
if (theTargetType.isInstance(theObject)) {
//noinspection unchecked
return Optional.of((T) theObject);
}
if (theObject instanceof IAdaptable) {
IAdaptable adaptable = (IAdaptable) theObject;
var adapted = adaptable.getAdapter(theTargetType);
if (adapted.isPresent()) {
return adapted;
}
}
return AdapterManager.INSTANCE.getAdapter(theObject, theTargetType);
}
}

View File

@ -0,0 +1,19 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import java.util.Optional;
/**
* Generic version of Eclipse IAdaptable interface.
*/
public interface IAdaptable {
/**
* Get an adapter of requested type.
* @param theTargetType the desired type of the adapter
* @return an adapter of theTargetType if possible, or empty.
*/
default <T> @Nonnull Optional<T> getAdapter(@Nonnull Class<T> theTargetType) {
return AdapterUtils.adapt(this, theTargetType);
}
}

View File

@ -0,0 +1,25 @@
package ca.uhn.fhir.util.adapters;
import java.util.Collection;
import java.util.Optional;
/**
* Interface for external service that builds adaptors for targets.
*/
public interface IAdapterFactory {
/**
* Build an adaptor for the target.
* May return empty() even if the target type is listed in getAdapters() when
* the factory fails to convert a particular instance.
*
* @param theObject the object to be adapted.
* @param theAdapterType the target type
* @return the adapter, if possible.
*/
<T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType);
/**
* @return the collection of adapter target types handled by this factory.
*/
Collection<Class<?>> getAdapters();
}

View File

@ -0,0 +1,10 @@
package ca.uhn.fhir.util.adapters;
import java.util.Optional;
/**
* Get an adaptor
*/
public interface IAdapterManager {
<T> Optional<T> getAdapter(Object theTarget, Class<T> theAdapter);
}

View File

@ -0,0 +1,20 @@
/**
* Implements the Adapter pattern to allow external classes to extend/adapt existing classes.
* Useful for extending interfaces that are closed to modification, or restricted for classpath reasons.
* <p>
* For clients, the main entry point is {@link ca.uhn.fhir.util.adapters.AdapterUtils#adapt(java.lang.Object, java.lang.Class)}
* which will attempt to cast to the target type, or build an adapter of the target type.
* </p>
* <p>
* For implementors, you can support adaptation via two mechanisms:
* <ul>
* <li>by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters,
* <li>or when the class is closed to direct modification, you can implement
* an instance of {@link ca.uhn.fhir.util.adapters.IAdapterFactory} and register
* it with the public {@link ca.uhn.fhir.util.adapters.AdapterManager#INSTANCE}.</li>
* </ul>
* The AdapterUtils.adapt() supports both of these.
* </p>
* Inspired by the Eclipse runtime.
*/
package ca.uhn.fhir.util.adapters;

View File

@ -0,0 +1,78 @@
package ca.uhn.fhir.util.adapters;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import static org.assertj.core.api.Assertions.assertThat;
public class AdapterManagerTest {
AdapterManager myAdapterManager = new AdapterManager();
@AfterAll
static void tearDown() {
assertThat(AdapterManager.INSTANCE.myAdapterFactories)
.withFailMessage("Don't dirty the public instance").isEmpty();
}
@Test
void testRegisterFactory_providesAdapter() {
// given
myAdapterManager.registerFactory(new StringToIntFactory());
// when
var result = myAdapterManager.getAdapter("22", Integer.class);
// then
assertThat(result).contains(22);
}
@Test
void testRegisterFactory_wrongTypeStillEmpty() {
// given
myAdapterManager.registerFactory(new StringToIntFactory());
// when
var result = myAdapterManager.getAdapter("22", Float.class);
// then
assertThat(result).isEmpty();
}
@Test
void testUnregisterFactory_providesEmpty() {
// given active factory, now gone.
StringToIntFactory factory = new StringToIntFactory();
myAdapterManager.registerFactory(factory);
myAdapterManager.getAdapter("22", Integer.class);
myAdapterManager.unregisterFactory(factory);
// when
var result = myAdapterManager.getAdapter("22", Integer.class);
// then
assertThat(result).isEmpty();
}
static class StringToIntFactory implements IAdapterFactory {
@Override
public <T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType) {
if (theObject instanceof String s) {
if (theAdapterType.isAssignableFrom(Integer.class)) {
@SuppressWarnings("unchecked")
T i = (T) Integer.valueOf(s);
return Optional.of(i);
}
}
return Optional.empty();
}
public Collection<Class<?>> getAdapters() {
return List.of(Integer.class);
}
}
}

View File

@ -0,0 +1,123 @@
package ca.uhn.fhir.util.adapters;
import jakarta.annotation.Nonnull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.Collection;
import java.util.Optional;
import java.util.Set;
import static org.assertj.core.api.Assertions.assertThat;
class AdapterUtilsTest {
final private IAdapterFactory myTestFactory = new TestAdaptorFactory();
@AfterEach
void tearDown() {
AdapterManager.INSTANCE.unregisterFactory(myTestFactory);
}
@Test
void testNullDoesNotAdapt() {
// when
var adapted = AdapterUtils.adapt(null, InterfaceA.class);
// then
assertThat(adapted).isEmpty();
}
@Test
void testAdaptObjectImplementingInterface() {
// given
var object = new ClassB();
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
assertThat(adapted.get()).withFailMessage("Use object since it implements interface").isSameAs(object);
}
@Test
void testAdaptObjectImplementingAdaptorSupportingInterface() {
// given
var object = new SelfAdaptableClass();
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
}
@Test
void testAdaptObjectViaAdapterManager() {
// given
var object = new ManagerAdaptableClass();
AdapterManager.INSTANCE.registerFactory(myTestFactory);
// when
var adapted = AdapterUtils.adapt(object, InterfaceA.class);
// then
assertThat(adapted)
.isPresent()
.get().isInstanceOf(InterfaceA.class);
}
interface InterfaceA {
}
static class ClassB implements InterfaceA {
}
/** class that can adapt itself to IAdaptable */
static class SelfAdaptableClass implements IAdaptable {
@Nonnull
@Override
public <T> Optional<T> getAdapter(@Nonnull Class<T> theTargetType) {
if (theTargetType.isAssignableFrom(InterfaceA.class)) {
T value = theTargetType.cast(buildInterfaceAWrapper(this));
return Optional.of(value);
}
return Optional.empty();
}
}
private static @Nonnull InterfaceA buildInterfaceAWrapper(Object theObject) {
return new InterfaceA() {};
}
/** Class that relies on an external IAdapterFactory */
static class ManagerAdaptableClass {
}
static class TestAdaptorFactory implements IAdapterFactory {
@Override
public <T> Optional<T> getAdapter(Object theObject, Class<T> theAdapterType) {
if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) {
T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject));
return Optional.of(adapter);
}
return Optional.empty();
}
@Override
public Collection<Class<?>> getAdapters() {
return Set.of(InterfaceA.class);
}
}
}