Add Adapter api (#6450)
Lightweight implementation of the adapter pattern.
This commit is contained in:
parent
6c4c6bde2c
commit
7faf0f7731
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue