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:
Ken Stevens 2024-04-04 22:24:05 -03:00 committed by GitHub
parent bdea4b6900
commit 3a5ff47c58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 252 additions and 57 deletions

View File

@ -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."

View File

@ -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);
}
}
}

View File

@ -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);
}

View File

@ -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 {}

View File

@ -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;
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}
}