Add CapabilityStatementFilterFactory to register filters that can modify the capability statement (#5814)
* added new customizer filter factory. have not yet deleted old code it replaces. * added new customizer filter factory. have not yet deleted old code it replaces. * replaced websocket filter. works. still some cleanup to do * replaced websocket filter. works. still some cleanup to do * cosmetic change * add coverage and fix bugs it found * spotless * move capability statement classes * add changelog and rename new classes * review feedback
This commit is contained in:
parent
bdea4b6900
commit
3a5ff47c58
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
type: change
|
||||
issue: 5814
|
||||
title: "Extracted methods out of ResourceProviderFactory into ObservableSupplierSet so that functionality can be used by
|
||||
other services. Unit tests revealed a cleanup bug in MdmProviderLoader that is fixed in this MR."
|
|
@ -32,6 +32,8 @@ import jakarta.annotation.PreDestroy;
|
|||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
@Service
|
||||
public class MdmProviderLoader {
|
||||
@Autowired
|
||||
|
@ -58,26 +60,28 @@ public class MdmProviderLoader {
|
|||
@Autowired
|
||||
private IInterceptorBroadcaster myInterceptorBroadcaster;
|
||||
|
||||
private BaseMdmProvider myMdmProvider;
|
||||
private MdmLinkHistoryProviderDstu3Plus myMdmHistoryProvider;
|
||||
private Supplier<Object> myMdmProviderSupplier;
|
||||
private Supplier<Object> myMdmHistoryProviderSupplier;
|
||||
|
||||
public void loadProvider() {
|
||||
switch (myFhirContext.getVersion().getVersion()) {
|
||||
case DSTU3:
|
||||
case R4:
|
||||
case R5:
|
||||
myResourceProviderFactory.addSupplier(() -> new MdmProviderDstu3Plus(
|
||||
// We store the supplier so that removeSupplier works properly
|
||||
myMdmProviderSupplier = () -> new MdmProviderDstu3Plus(
|
||||
myFhirContext,
|
||||
myMdmControllerSvc,
|
||||
myMdmControllerHelper,
|
||||
myMdmSubmitSvc,
|
||||
myInterceptorBroadcaster,
|
||||
myMdmSettings));
|
||||
myMdmSettings);
|
||||
// We store the supplier so that removeSupplier works properly
|
||||
myResourceProviderFactory.addSupplier(myMdmProviderSupplier);
|
||||
if (myStorageSettings.isNonResourceDbHistoryEnabled()) {
|
||||
myResourceProviderFactory.addSupplier(() -> {
|
||||
return new MdmLinkHistoryProviderDstu3Plus(
|
||||
myFhirContext, myMdmControllerSvc, myInterceptorBroadcaster);
|
||||
});
|
||||
myMdmHistoryProviderSupplier = () -> new MdmLinkHistoryProviderDstu3Plus(
|
||||
myFhirContext, myMdmControllerSvc, myInterceptorBroadcaster);
|
||||
myResourceProviderFactory.addSupplier(myMdmHistoryProviderSupplier);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
|
@ -88,11 +92,11 @@ public class MdmProviderLoader {
|
|||
|
||||
@PreDestroy
|
||||
public void unloadProvider() {
|
||||
if (myMdmProvider != null) {
|
||||
myResourceProviderFactory.removeSupplier(() -> myMdmProvider);
|
||||
if (myMdmProviderSupplier != null) {
|
||||
myResourceProviderFactory.removeSupplier(myMdmProviderSupplier);
|
||||
}
|
||||
if (myMdmHistoryProvider != null) {
|
||||
myResourceProviderFactory.removeSupplier(() -> myMdmHistoryProvider);
|
||||
if (myMdmHistoryProviderSupplier != null) {
|
||||
myResourceProviderFactory.removeSupplier(myMdmHistoryProviderSupplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
package ca.uhn.fhir.rest.server.provider;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* See {@link ObservableSupplierSet}
|
||||
*/
|
||||
public interface IObservableSupplierSetObserver {
|
||||
void update(@Nonnull Supplier<Object> theSupplier);
|
||||
|
||||
void remove(@Nonnull Supplier<Object> theSupplier);
|
||||
}
|
|
@ -19,12 +19,7 @@
|
|||
*/
|
||||
package ca.uhn.fhir.rest.server.provider;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public interface IResourceProviderFactoryObserver {
|
||||
void update(@Nonnull Supplier<Object> theSupplier);
|
||||
|
||||
void remove(@Nonnull Supplier<Object> theSupplier);
|
||||
}
|
||||
/**
|
||||
* See {@link ObservableSupplierSet}
|
||||
*/
|
||||
public interface IResourceProviderFactoryObserver extends IObservableSupplierSetObserver {}
|
||||
|
|
|
@ -0,0 +1,104 @@
|
|||
package ca.uhn.fhir.rest.server.provider;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* This is a generic implementation of the <a href="https://refactoring.guru/design-patterns/observer">Observer Design Pattern</a>.
|
||||
* We use this to pass sets of beans from exporting Spring application contexts to importing Spring application contexts. We defer
|
||||
* resolving the observed beans via a Supplier to give the exporting context a chance to initialize the beans before they are used.
|
||||
* @param <T> the class of the Observer
|
||||
* <p>
|
||||
* A typical usage pattern would be:
|
||||
* <ol>
|
||||
* <li>Create {@link ObservableSupplierSet} in exporter context.</li>
|
||||
* <li>Add all the suppliers in the exporter context.</li>
|
||||
* <li>Attach the importer to the {@link ObservableSupplierSet}</li>
|
||||
* <li>Importer calls {@link ObservableSupplierSet#getSupplierResults} and processes all the beans</li>
|
||||
* <li>Some other service beans may add more suppliers later as a part of their initialization and the observer handlers will process them accordingly</li>
|
||||
* <li>Those other service beans should call {@link ObservableSupplierSet#removeSupplier(Supplier)} in a @PreDestroy method so they are properly cleaned up if those services are shut down or restarted</li>
|
||||
* </ol>
|
||||
*
|
||||
*/
|
||||
public class ObservableSupplierSet<T extends IObservableSupplierSetObserver> {
|
||||
private static final Logger ourLog = LoggerFactory.getLogger(ObservableSupplierSet.class);
|
||||
|
||||
private final Set<T> myObservers = Collections.synchronizedSet(new HashSet<>());
|
||||
|
||||
private final Set<Supplier<Object>> mySuppliers = new LinkedHashSet<>();
|
||||
|
||||
/** Add a supplier and notify all observers
|
||||
*
|
||||
* @param theSupplier supplies the object to be observed
|
||||
*/
|
||||
public void addSupplier(@Nonnull Supplier<Object> theSupplier) {
|
||||
if (mySuppliers.add(theSupplier)) {
|
||||
myObservers.forEach(observer -> observer.update(theSupplier));
|
||||
}
|
||||
}
|
||||
|
||||
/** Remove a supplier and notify all observers. CAUTION, you might think that this code would work, but it does not:
|
||||
* <code>
|
||||
* observableSupplierSet.addSupplier(() -> myBean);
|
||||
* ...
|
||||
* observableSupplierSet.removeSupplier(() -> myBean);
|
||||
* </code>
|
||||
* the removeSupplier in this example would fail because it is a different lambda instance from the first. Instead,
|
||||
* you need to store the supplier between the add and remove:
|
||||
* <code>
|
||||
* mySupplier = () -> myBean;
|
||||
* observableSupplierSet.addSupplier(mySupplier);
|
||||
* ...
|
||||
* observableSupplierSet.removeSupplier(mySupplier);
|
||||
* </code>
|
||||
*
|
||||
* @param theSupplier the supplier to be removed
|
||||
*/
|
||||
public void removeSupplier(@Nonnull Supplier<Object> theSupplier) {
|
||||
if (mySuppliers.remove(theSupplier)) {
|
||||
myObservers.forEach(observer -> observer.remove(theSupplier));
|
||||
} else {
|
||||
ourLog.warn("Failed to remove supplier", new RuntimeException());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach an observer to this observableSupplierSet. This observer will be notified every time a supplier is added or removed.
|
||||
* @param theObserver the observer to be notified
|
||||
*/
|
||||
public void attach(T theObserver) {
|
||||
myObservers.add(theObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach an observer from this observableSupplierSet, so it is no longer notified when suppliers are added and removed.
|
||||
* @param theObserver the observer to be removed
|
||||
*/
|
||||
public void detach(T theObserver) {
|
||||
myObservers.remove(theObserver);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return a list of get() being called on all suppliers.
|
||||
*/
|
||||
protected List<Object> getSupplierResults() {
|
||||
List<Object> retVal = new ArrayList<>();
|
||||
for (Supplier<Object> next : mySuppliers) {
|
||||
Object nextRp = next.get();
|
||||
if (nextRp != null) {
|
||||
retVal.add(nextRp);
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
}
|
|
@ -19,45 +19,16 @@
|
|||
*/
|
||||
package ca.uhn.fhir.rest.server.provider;
|
||||
|
||||
import jakarta.annotation.Nonnull;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public class ResourceProviderFactory {
|
||||
private Set<IResourceProviderFactoryObserver> myObservers = Collections.synchronizedSet(new HashSet<>());
|
||||
private List<Supplier<Object>> mySuppliers = new ArrayList<>();
|
||||
|
||||
public void addSupplier(@Nonnull Supplier<Object> theSupplier) {
|
||||
mySuppliers.add(theSupplier);
|
||||
myObservers.forEach(observer -> observer.update(theSupplier));
|
||||
}
|
||||
|
||||
public void removeSupplier(@Nonnull Supplier<Object> theSupplier) {
|
||||
mySuppliers.remove(theSupplier);
|
||||
myObservers.forEach(observer -> observer.remove(theSupplier));
|
||||
}
|
||||
/**
|
||||
* This Factory stores FHIR Resource Provider instance suppliers that will be registered on a FHIR Endpoint later.
|
||||
* See {@link ObservableSupplierSet}
|
||||
*/
|
||||
public class ResourceProviderFactory extends ObservableSupplierSet<IResourceProviderFactoryObserver> {
|
||||
public ResourceProviderFactory() {}
|
||||
|
||||
public List<Object> createProviders() {
|
||||
List<Object> retVal = new ArrayList<>();
|
||||
for (Supplier<Object> next : mySuppliers) {
|
||||
Object nextRp = next.get();
|
||||
if (nextRp != null) {
|
||||
retVal.add(nextRp);
|
||||
}
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
public void attach(IResourceProviderFactoryObserver theObserver) {
|
||||
myObservers.add(theObserver);
|
||||
}
|
||||
|
||||
public void detach(IResourceProviderFactoryObserver theObserver) {
|
||||
myObservers.remove(theObserver);
|
||||
return super.getSupplierResults();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,102 @@
|
|||
package ca.uhn.fhir.rest.server.provider;
|
||||
|
||||
import ca.uhn.test.util.LogbackCaptureTestExtension;
|
||||
import ch.qos.logback.classic.Level;
|
||||
import ch.qos.logback.classic.spi.ILoggingEvent;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
class ObservableSupplierSetTest {
|
||||
|
||||
private final ObservableSupplierSet<TestObserver> myObservableSupplierSet = new ObservableSupplierSet<>();
|
||||
private final TestObserver myObserver = new TestObserver();
|
||||
private final AtomicInteger myCounter = new AtomicInteger();
|
||||
|
||||
@RegisterExtension
|
||||
final LogbackCaptureTestExtension myLogger = new LogbackCaptureTestExtension((ch.qos.logback.classic.Logger) LoggerFactory.getLogger(ObservableSupplierSet.class));
|
||||
|
||||
@BeforeEach
|
||||
public void before() {
|
||||
myObservableSupplierSet.attach(myObserver);
|
||||
myObserver.assertCalls(0, 0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void observersNotifiedByAddRemove() {
|
||||
Supplier<Object> supplier = myCounter::incrementAndGet;
|
||||
myObservableSupplierSet.addSupplier(supplier);
|
||||
myObserver.assertCalls(1, 0);
|
||||
assertEquals(0, myCounter.get());
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(1));
|
||||
assertEquals(1, myCounter.get());
|
||||
myObservableSupplierSet.removeSupplier(supplier);
|
||||
myObserver.assertCalls(1, 1);
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRemoveWrongSupplier() {
|
||||
myObservableSupplierSet.addSupplier(myCounter::incrementAndGet);
|
||||
myObserver.assertCalls(1, 0);
|
||||
assertEquals(0, myCounter.get());
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(1));
|
||||
assertEquals(1, myCounter.get());
|
||||
|
||||
// You might expect this to remove our supplier, but in fact it is a different lambda, so it fails and logs a stack trace
|
||||
myObservableSupplierSet.removeSupplier(myCounter::incrementAndGet);
|
||||
myObserver.assertCalls(1, 0);
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(1));
|
||||
List<ILoggingEvent> events = myLogger.filterLoggingEventsWithMessageContaining("Failed to remove supplier");
|
||||
assertThat(events, hasSize(1));
|
||||
assertEquals(Level.WARN, events.get(0).getLevel());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDetach() {
|
||||
myObservableSupplierSet.addSupplier(myCounter::incrementAndGet);
|
||||
myObserver.assertCalls(1, 0);
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(1));
|
||||
|
||||
myObservableSupplierSet.addSupplier(myCounter::incrementAndGet);
|
||||
myObserver.assertCalls(2, 0);
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(2));
|
||||
|
||||
myObservableSupplierSet.detach(myObserver);
|
||||
|
||||
// We now have a third supplier but the observer has been detached, so it was not notified of the third supplier
|
||||
myObservableSupplierSet.addSupplier(myCounter::incrementAndGet);
|
||||
myObserver.assertCalls(2, 0);
|
||||
assertThat(myObservableSupplierSet.getSupplierResults(), hasSize(3));
|
||||
}
|
||||
|
||||
private static class TestObserver implements IObservableSupplierSetObserver {
|
||||
int updated = 0;
|
||||
int removed = 0;
|
||||
|
||||
@Override
|
||||
public void update(@NotNull Supplier<Object> theSupplier) {
|
||||
++updated;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remove(@NotNull Supplier<Object> theSupplier) {
|
||||
++removed;
|
||||
}
|
||||
|
||||
public void assertCalls(int theExpectedUpdated, int theExpectedRemoved) {
|
||||
assertEquals(theExpectedUpdated, updated);
|
||||
assertEquals(theExpectedRemoved, removed);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue