diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java new file mode 100644 index 00000000000..09b24657b66 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterManager.java @@ -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 myAdapterFactories = new HashSet<>(); + + /** + * Hidden to force shared use of the public INSTANCE. + */ + AdapterManager() {} + + public @Nonnull Optional getAdapter(Object theObject, Class 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); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java new file mode 100644 index 00000000000..36237becd9a --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/AdapterUtils.java @@ -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 Optional adapt(Object theObject, Class 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); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java new file mode 100644 index 00000000000..5eb1b266b49 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdaptable.java @@ -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 @Nonnull Optional getAdapter(@Nonnull Class theTargetType) { + return AdapterUtils.adapt(this, theTargetType); + } +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java new file mode 100644 index 00000000000..a183705cc53 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterFactory.java @@ -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. + */ + Optional getAdapter(Object theObject, Class theAdapterType); + + /** + * @return the collection of adapter target types handled by this factory. + */ + Collection> getAdapters(); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java new file mode 100644 index 00000000000..9550ccbdc27 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/IAdapterManager.java @@ -0,0 +1,10 @@ +package ca.uhn.fhir.util.adapters; + +import java.util.Optional; + +/** + * Get an adaptor + */ +public interface IAdapterManager { + Optional getAdapter(Object theTarget, Class theAdapter); +} diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java new file mode 100644 index 00000000000..de9e92df5c4 --- /dev/null +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/util/adapters/package-info.java @@ -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. + *

+ * 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. + *

+ *

+ * For implementors, you can support adaptation via two mechanisms: + *

    + *
  • by implementing {@link ca.uhn.fhir.util.adapters.IAdaptable} directly on a class to provide supported adapters, + *
  • 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}.
  • + *
+ * The AdapterUtils.adapt() supports both of these. + *

+ * Inspired by the Eclipse runtime. + */ +package ca.uhn.fhir.util.adapters; diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java new file mode 100644 index 00000000000..ee621533587 --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterManagerTest.java @@ -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 Optional getAdapter(Object theObject, Class 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> getAdapters() { + return List.of(Integer.class); + } + } +} diff --git a/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java new file mode 100644 index 00000000000..c7dfef8661b --- /dev/null +++ b/hapi-fhir-base/src/test/java/ca/uhn/fhir/util/adapters/AdapterUtilsTest.java @@ -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 Optional getAdapter(@Nonnull Class 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 Optional getAdapter(Object theObject, Class theAdapterType) { + if (theObject instanceof ManagerAdaptableClass && theAdapterType == InterfaceA.class) { + T adapter = theAdapterType.cast(buildInterfaceAWrapper(theObject)); + return Optional.of(adapter); + } + return Optional.empty(); + } + + @Override + public Collection> getAdapters() { + return Set.of(InterfaceA.class); + } + } +}