Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/master' into ld-20250306-cqf-fhi…
Browse files Browse the repository at this point in the history
…r-cr-test-back-to-cqf-fhir-cr
  • Loading branch information
lukedegruchy committed Mar 7, 2025
2 parents 23e6ab7 + 286fd4d commit 216e8e6
Show file tree
Hide file tree
Showing 6 changed files with 281 additions and 75 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,16 @@
import org.opencds.cqf.fhir.utility.PackageHelper;
import org.opencds.cqf.fhir.utility.SearchHelper;
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
import org.opencds.cqf.fhir.utility.adapter.IDependencyInfo;
import org.opencds.cqf.fhir.utility.adapter.IEndpointAdapter;
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactVisitor;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class BaseKnowledgeArtifactVisitor implements IKnowledgeArtifactVisitor {
private static final Logger myLogger = LoggerFactory.getLogger(BaseKnowledgeArtifactVisitor.class);
String isOwnedUrl = "http://hl7.org/fhir/StructureDefinition/artifact-isOwned";
protected final Repository repository;
protected final Optional<IValueSetExpansionCache> valueSetExpansionCache;
Expand Down Expand Up @@ -110,6 +116,18 @@ protected void recursiveGather(
List<String> include,
ImmutableTriple<List<String>, List<String>, List<String>> versionTuple)
throws PreconditionFailedException {
recursiveGather(adapter, gatheredResources, capability, include, versionTuple, null, null);
}

protected void recursiveGather(
IKnowledgeArtifactAdapter adapter,
Map<String, IKnowledgeArtifactAdapter> gatheredResources,
List<String> capability,
List<String> include,
ImmutableTriple<List<String>, List<String>, List<String>> versionTuple,
IEndpointAdapter terminologyEndpoint,
TerminologyServerClient client)
throws PreconditionFailedException {
if (adapter == null) {
return;
}
Expand All @@ -135,12 +153,20 @@ protected void recursiveGather(
}
}
})
.map(ra -> SearchHelper.searchRepositoryByCanonicalWithPaging(repository, ra.getReference()))
.map(searchBundle -> (IDomainResource) BundleHelper.getEntryResourceFirstRep(searchBundle))
.map(ra -> Optional.ofNullable(
SearchHelper.searchRepositoryByCanonicalWithPaging(repository, ra.getReference()))
.map(bundle -> (IDomainResource) BundleHelper.getEntryResourceFirstRep(bundle))
.orElseGet(() -> tryGetValueSetsFromTxServer(ra, client, terminologyEndpoint)))
.filter(r -> r != null)
.map(r -> IAdapterFactory.forFhirVersion(fhirVersion()).createKnowledgeArtifactAdapter(r))
.forEach(component ->
recursiveGather(component, gatheredResources, capability, include, versionTuple));
.forEach(component -> recursiveGather(
component,
gatheredResources,
capability,
include,
versionTuple,
terminologyEndpoint,
client));
}
}

Expand Down Expand Up @@ -173,4 +199,18 @@ protected <T extends ICompositeType & IBaseHasExtensions> void addRelatedArtifac
fhirVersion(), "depends-on", reference, adapter.getDescriptor()));
}
}

private IDomainResource tryGetValueSetsFromTxServer(
IDependencyInfo ra, TerminologyServerClient client, IEndpointAdapter endpoint) {
if (client != null
&& endpoint != null
&& Canonicals.getResourceType(ra.getReference()).equals("ValueSet")) {
return client.getResource(
endpoint,
ra.getReference(),
fhirContext().getVersion().getVersion())
.orElse(null);
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,20 @@ public class PackageVisitor extends BaseKnowledgeArtifactVisitor {
protected Map<String, List<?>> resourceTypes = new HashMap<>();

public PackageVisitor(Repository repository) {
this(repository, null);
this(repository, null, null);
}

public PackageVisitor(Repository repository, IValueSetExpansionCache cache) {
public PackageVisitor(Repository repository, TerminologyServerClient client) {
this(repository, client, null);
}

public PackageVisitor(Repository repository, TerminologyServerClient client, IValueSetExpansionCache cache) {
super(repository, cache);
terminologyServerClient = new TerminologyServerClient(fhirContext());
if (client == null) {
terminologyServerClient = new TerminologyServerClient(fhirContext());
} else {
terminologyServerClient = client;
}
expandHelper = new ExpandHelper(this.repository, terminologyServerClient);
setupResourceTypes();
}
Expand Down Expand Up @@ -108,9 +116,11 @@ public IBase visit(IKnowledgeArtifactAdapter adapter, IBaseParameters packagePar

Optional<String> artifactRoute = VisitorHelper.getStringParameter("artifactRoute", packageParameters);
Optional<String> endpointUri = VisitorHelper.getStringParameter("endpointUri", packageParameters);
Optional<IBaseResource> endpoint = VisitorHelper.getResourceParameter("endpoint", packageParameters);
Optional<IBaseResource> terminologyEndpoint =
VisitorHelper.getResourceParameter("terminologyEndpoint", packageParameters);
Optional<IEndpointAdapter> endpoint = VisitorHelper.getResourceParameter("endpoint", packageParameters)
.map(ep -> (IEndpointAdapter) createAdapterForResource(ep));
Optional<IEndpointAdapter> terminologyEndpoint = VisitorHelper.getResourceParameter(
"terminologyEndpoint", packageParameters)
.map(ep -> (IEndpointAdapter) createAdapterForResource(ep));
Optional<Boolean> packageOnly = VisitorHelper.getBooleanParameter("packageOnly", packageParameters);
Optional<Integer> count = VisitorHelper.getIntegerParameter("count", packageParameters);
Optional<Integer> offset = VisitorHelper.getIntegerParameter("offset", packageParameters);
Expand Down Expand Up @@ -157,7 +167,14 @@ public IBase visit(IKnowledgeArtifactAdapter adapter, IBaseParameters packagePar
BundleHelper.addEntry(packagedBundle, entry);
} else {
var packagedResources = new HashMap<String, IKnowledgeArtifactAdapter>();
recursiveGather(adapter, packagedResources, capability, include, versionTuple);
recursiveGather(
adapter,
packagedResources,
capability,
include,
versionTuple,
terminologyEndpoint.orElse(null),
terminologyServerClient);
packagedResources.values().stream()
.filter(r -> !r.getCanonical().equals(adapter.getCanonical()))
.forEach(r -> addBundleEntry(packagedBundle, isPut, r));
Expand All @@ -174,7 +191,7 @@ public IBase visit(IKnowledgeArtifactAdapter adapter, IBaseParameters packagePar
// what is dependency, where did it originate? potentially the package?
}

protected void handleValueSets(IBaseBundle packagedBundle, Optional<IBaseResource> terminologyEndpoint) {
protected void handleValueSets(IBaseBundle packagedBundle, Optional<IEndpointAdapter> terminologyEndpoint) {
var expansionParams = newParameters(fhirContext());
var rootSpecificationLibrary = getRootSpecificationLibrary(packagedBundle);
if (rootSpecificationLibrary != null) {
Expand All @@ -189,7 +206,6 @@ protected void handleValueSets(IBaseBundle packagedBundle, Optional<IBaseResourc
}
}
var params = (IParametersAdapter) createAdapterForResource(expansionParams);
var expandedList = new ArrayList<String>();

var valueSets = BundleHelper.getEntryResources(packagedBundle).stream()
.filter(r -> r.fhirType().equals("ValueSet"))
Expand All @@ -199,6 +215,7 @@ protected void handleValueSets(IBaseBundle packagedBundle, Optional<IBaseResourc
var expansionParamsHash = expansionCache.map(
e -> e.getExpansionParametersHash(rootSpecificationLibrary).orElse(null));
var missingInCache = new ArrayList<>(valueSets);
var expandedList = new ArrayList<String>();
if (expansionCache.isPresent()) {
var startCache = (new Date()).getTime();
valueSets.forEach(v -> {
Expand All @@ -212,18 +229,12 @@ protected void handleValueSets(IBaseBundle packagedBundle, Optional<IBaseResourc
}
});
var elapsed = String.valueOf(((new Date()).getTime() - startCache) / 1000);
myLogger.info("retrieved cached ValueSet Expansions in: {}s", elapsed);
myLogger.info("retrieved {} cached ValueSet Expansions in: {}s", expandedList.size(), elapsed);
}
missingInCache.forEach(valueSet -> {
var url = valueSet.getUrl();
var expansionStartTime = new Date().getTime();
expandHelper.expandValueSet(
valueSet,
params,
terminologyEndpoint.map(e -> (IEndpointAdapter) createAdapterForResource(e)),
valueSets,
expandedList,
new Date());
expandHelper.expandValueSet(valueSet, params, terminologyEndpoint, valueSets, expandedList, new Date());
var elapsed = String.valueOf(((new Date()).getTime() - expansionStartTime) / 1000);
myLogger.info("Expanded {} in {}s", url, elapsed);
if (expansionCache.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.isNull;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.opencds.cqf.fhir.utility.dstu3.Parameters.parameters;
import static org.opencds.cqf.fhir.utility.dstu3.Parameters.part;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.server.exceptions.PreconditionFailedException;
import ca.uhn.fhir.rest.server.exceptions.UnprocessableEntityException;
Expand All @@ -25,6 +28,7 @@
import java.util.Map.Entry;
import java.util.Optional;
import java.util.concurrent.ThreadLocalRandom;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.hl7.fhir.dstu3.model.Bundle;
import org.hl7.fhir.dstu3.model.Bundle.BundleEntryComponent;
Expand All @@ -44,14 +48,22 @@
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import org.mockito.internal.stubbing.defaultanswers.ReturnsDeepStubs;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.opencds.cqf.fhir.api.Repository;
import org.opencds.cqf.fhir.cr.visitor.IValueSetExpansionCache;
import org.opencds.cqf.fhir.cr.visitor.PackageVisitor;
import org.opencds.cqf.fhir.utility.Constants;
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
import org.opencds.cqf.fhir.utility.adapter.IEndpointAdapter;
import org.opencds.cqf.fhir.utility.adapter.ILibraryAdapter;
import org.opencds.cqf.fhir.utility.adapter.IParametersAdapter;
import org.opencds.cqf.fhir.utility.adapter.IValueSetAdapter;
import org.opencds.cqf.fhir.utility.adapter.dstu3.AdapterFactory;
import org.opencds.cqf.fhir.utility.adapter.dstu3.LibraryAdapter;
import org.opencds.cqf.fhir.utility.adapter.dstu3.ValueSetAdapter;
import org.opencds.cqf.fhir.utility.client.TerminologyServerClient;
import org.opencds.cqf.fhir.utility.repository.InMemoryFhirRepository;

class PackageVisitorTests {
Expand Down Expand Up @@ -434,7 +446,7 @@ public void packageVisitorShouldUseExpansionCacheIfProvided() {
.copy();
var libraryAdapter = new AdapterFactory().createLibrary(library);
var mockCache = Mockito.mock(IValueSetExpansionCache.class);
var packageVisitor = new PackageVisitor(repo, mockCache);
var packageVisitor = new PackageVisitor(repo, null, mockCache);

var canonical1 = "http://cts.nlm.nih.gov/fhir/ValueSet/123-this-will-be-routine|20210526";
var mockValueSetAdapter1 = Mockito.mock(ValueSetAdapter.class);
Expand All @@ -461,4 +473,54 @@ public void packageVisitorShouldUseExpansionCacheIfProvided() {
verify(mockCache, times(2)).addToCache(any(), isNull());
verify(mockCache, times(1)).getExpansionParametersHash(any(LibraryAdapter.class));
}

@Test
void fallback_to_tx_server_if_valueset_missing_locally() {
final var leafOid = "2.16.840.1.113762.1.4.1146.6";
final var authoritativeSource = "http://cts.nlm.nih.gov/fhir/";
var bundle = (Bundle) jsonParser.parseResource(
ReleaseVisitorTests.class.getResourceAsStream("Bundle-ersd-small-active.json"));
Predicate<BundleEntryComponent> leafFinder = e -> e.getResource().getResourceType() == ResourceType.ValueSet
&& ((ValueSet) e.getResource()).getUrl().contains(leafOid);
// remove leaf from bundle
var leafEntry = bundle.getEntry().stream().filter(leafFinder).findFirst();
var missingVset = leafEntry.map(e -> (ValueSet) e.getResource()).get();
bundle.getEntry().remove(leafEntry.get());

repo.transaction(bundle);
var library = repo.read(Library.class, new IdType("Library/SpecificationLibrary"))
.copy();
var endpoint = createEndpoint(authoritativeSource);

var clientMock = mock(TerminologyServerClient.class, new ReturnsDeepStubs());
// expect the Tx Server to provide the missing ValueSet
when(clientMock.getResource(any(IEndpointAdapter.class), any(), any())).thenReturn(Optional.of(missingVset));
doAnswer(new Answer<ValueSet>() {
@Override
public ValueSet answer(InvocationOnMock invocation) throws Throwable {
return new ValueSet(); // Return a new instance of ValueSet
}
})
.when(clientMock)
.expand(any(IValueSetAdapter.class), any(IEndpointAdapter.class), any(IParametersAdapter.class));
var packageVisitor = new PackageVisitor(repo, clientMock);
var libraryAdapter = new AdapterFactory().createLibrary(library);
var params = parameters(part("terminologyEndpoint", (org.hl7.fhir.dstu3.model.Endpoint) endpoint.get()));
// create package
var packagedBundle = (Bundle) libraryAdapter.accept(packageVisitor, params);
var containsVset = packagedBundle.getEntry().stream().anyMatch(leafFinder);
// check for ValueSet
assertTrue(containsVset);
}

private IEndpointAdapter createEndpoint(String authoritativeSource) {
var factory = IAdapterFactory.forFhirVersion(FhirVersionEnum.DSTU3);
var endpoint = factory.createEndpoint(new org.hl7.fhir.dstu3.model.Endpoint());
endpoint.setAddress(authoritativeSource);
endpoint.addExtension(new org.hl7.fhir.dstu3.model.Extension(
Constants.VSAC_USERNAME, new org.hl7.fhir.dstu3.model.StringType("username")));
endpoint.addExtension(new org.hl7.fhir.dstu3.model.Extension(
Constants.APIKEY, new org.hl7.fhir.dstu3.model.StringType("password")));
return endpoint;
}
}
Loading

0 comments on commit 216e8e6

Please sign in to comment.