Skip to content

Commit b2f1f3d

Browse files
adds support for federated terminology provider (#905)
* adds support for federated terminology provider * fixes PackageVisitor tests applies spotless * Fixed bug in FederatedTerminologyProviderRouter and broken ReleaseVisitor tests * Closing CRMI spec gaps and additional testing * Wiring up the library package provider with updated parameter * Add VSAC versioning syntax fix and enhanced logging to ExpandRunner - Add buildResourceIdForExpand() to handle VSAC's non-standard URL format where version is appended with a dash (ValueSet/id-version) instead of the standard pipe separator - Add formatParametersForLogging() and formatParameterValue() helpers for structured debug output of expansion parameters - Enhance expansion attempt logging with terminology server base URL, full expand URL, and parameter details - Change failure logging from info to warn level with additional context * Fix LibraryPackageProvider compilation errors for artifactEndpointConfiguration support * Add tests for federated terminology provider infrastructure * add coverage tests for federated terminology provider infrastructure FederatedTerminologyProviderRouterTest (16 tests): - List-based expand overloads exercising prioritizeEndpoints and stream filtering - List-based getValueSetResource, getCodeSystemResource, getLatestValueSetResource - Empty endpoint list edge cases - Exception catch-and-fallback paths in expandWithConfigurations, getValueSetResourceWithConfigurations, getCodeSystemResourceWithConfigurations ExpandRunnerTest (2 tests): - Transient server error (500) retry path exercising isTransient() true branch - Part-parameters formatting exercising formatParameterValue hasPart() branch GenericTerminologyServerClientClientTest (3 tests): - Null URL with no url parameter throws UnprocessableEntityException - initializeClientWithAuth with endpoint headers exercises hasHeaders() branch - Single-arg constructor exercises default settings fallback ArtifactEndpointConfigurationTest (8 tests): - Optional getters for artifactRoute, endpointUri, endpoint (present and empty) - toString with non-null endpoint resource --------- Co-authored-by: c-schuler <hoofschu@gmail.com>
1 parent 7270f66 commit b2f1f3d

34 files changed

+2621
-270
lines changed

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/common/HapiArtifactDiffProcessor.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
import org.opencds.cqf.fhir.utility.adapter.IAdapterFactory;
4949
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
5050
import org.opencds.cqf.fhir.utility.adapter.IValueSetAdapter;
51-
import org.opencds.cqf.fhir.utility.client.TerminologyServerClient;
51+
import org.opencds.cqf.fhir.utility.client.terminology.FederatedTerminologyProviderRouter;
5252
import org.slf4j.Logger;
5353
import org.slf4j.LoggerFactory;
5454
import org.springframework.beans.BeanWrapperImpl;
@@ -363,7 +363,7 @@ private static void tryExpandValueSet(
363363
// Only update if ValueSet has changed since last expansion
364364
if (wasValueSetChangedSinceLastExpansion(vset)) {
365365
var factory = IAdapterFactory.forFhirVersion(FhirVersionEnum.R4);
366-
var ts = new TerminologyServerClient(context);
366+
var ts = new FederatedTerminologyProviderRouter(context);
367367
var expandHelper = new ExpandHelper(repository, ts);
368368
var endpointAdapter = Optional.ofNullable(terminologyEndpoint).map(factory::createEndpoint);
369369
var valueSetAdapter = (IValueSetAdapter) factory.createKnowledgeArtifactAdapter(vset);

cqf-fhir-cr-hapi/src/main/java/org/opencds/cqf/fhir/cr/hapi/r4/library/LibraryPackageProvider.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,15 @@
1616
import java.util.List;
1717
import java.util.stream.Collectors;
1818
import org.hl7.fhir.exceptions.FHIRException;
19+
import org.hl7.fhir.instance.model.api.IBase;
1920
import org.hl7.fhir.instance.model.api.IBaseBundle;
2021
import org.hl7.fhir.instance.model.api.IPrimitiveType;
2122
import org.hl7.fhir.r4.model.BooleanType;
2223
import org.hl7.fhir.r4.model.CodeType;
2324
import org.hl7.fhir.r4.model.Endpoint;
2425
import org.hl7.fhir.r4.model.IdType;
2526
import org.hl7.fhir.r4.model.Library;
26-
import org.hl7.fhir.r4.model.Parameters.ParametersParameterComponent;
27+
import org.hl7.fhir.r4.model.Parameters;
2728
import org.hl7.fhir.r4.model.PrimitiveType;
2829
import org.hl7.fhir.r4.model.StringType;
2930
import org.opencds.cqf.fhir.cr.hapi.common.ILibraryProcessorFactory;
@@ -60,6 +61,8 @@ public LibraryPackageProvider(ILibraryProcessorFactory libraryProcessorFactory)
6061
* It is invalid to request a 'transaction' bundle and use
6162
* paging. Doing so will result in an error.
6263
* @param include Specifies what contents should only be included in the resulting package.
64+
* @param artifactEndpointConfiguration Configuration information to resolve canonical artifacts.
65+
* Contains parts: artifactRoute, endpointUri, endpoint.
6366
* @param terminologyEndpoint the FHIR {@link Endpoint} Endpoint resource or url to use to access terminology (i.e. valuesets, codesystems, naming systems, concept maps, and membership testing) referenced by the Resource. If no terminology endpoint is supplied, the evaluation will attempt to use the server on which the operation is being performed as the terminology server.
6467
* @param usePut the boolean value to determine if the Bundle returned uses PUT or POST request methods. Defaults to false.
6568
* @param requestDetails the details (such as tenant) of this request. Usually autopopulated by HAPI.
@@ -72,10 +75,16 @@ public IBaseBundle packageLibrary(
7275
@OperationParam(name = "count", typeName = "integer") IPrimitiveType<Integer> count,
7376
@OperationParam(name = "bundleType") StringType bundleType,
7477
@OperationParam(name = "include") List<CodeType> include,
75-
@OperationParam(name = "terminologyEndpoint") ParametersParameterComponent terminologyEndpoint,
78+
@OperationParam(name = "artifactEndpointConfiguration")
79+
List<Parameters.ParametersParameterComponent> artifactEndpointConfiguration,
80+
@OperationParam(name = "terminologyEndpoint") Parameters.ParametersParameterComponent terminologyEndpoint,
7681
@OperationParam(name = "usePut") BooleanType usePut,
7782
RequestDetails requestDetails)
7883
throws InternalErrorException, FHIRException {
84+
var terminologyEndpointParam = getEndpoint(fhirVersion, terminologyEndpoint);
85+
List<IBase> artifactEndpointConfigurationParam = artifactEndpointConfiguration == null
86+
? null
87+
: artifactEndpointConfiguration.stream().map(p -> (IBase) p).collect(Collectors.<IBase>toList());
7988
return libraryProcessorFactory
8089
.create(requestDetails)
8190
.packageLibrary(
@@ -91,7 +100,8 @@ public IBaseBundle packageLibrary(
91100
.distinct()
92101
.map(PrimitiveType::getValueAsString)
93102
.collect(Collectors.toList()),
94-
getEndpoint(fhirVersion, terminologyEndpoint),
103+
artifactEndpointConfigurationParam,
104+
terminologyEndpointParam,
95105
usePut == null ? Boolean.FALSE : usePut.booleanValue()));
96106
}
97107

@@ -135,10 +145,16 @@ public IBaseBundle packageLibrary(
135145
@OperationParam(name = "count", typeName = "integer") IPrimitiveType<Integer> count,
136146
@OperationParam(name = "bundleType") StringType bundleType,
137147
@OperationParam(name = "include") List<CodeType> include,
138-
@OperationParam(name = "terminologyEndpoint") ParametersParameterComponent terminologyEndpoint,
148+
@OperationParam(name = "artifactEndpointConfiguration")
149+
List<Parameters.ParametersParameterComponent> artifactEndpointConfiguration,
150+
@OperationParam(name = "terminologyEndpoint") Parameters.ParametersParameterComponent terminologyEndpoint,
139151
@OperationParam(name = "usePut") BooleanType usePut,
140152
RequestDetails requestDetails)
141153
throws InternalErrorException, FHIRException {
154+
var terminologyEndpointParam = getEndpoint(fhirVersion, terminologyEndpoint);
155+
List<IBase> artifactEndpointConfigurationParam = artifactEndpointConfiguration == null
156+
? null
157+
: artifactEndpointConfiguration.stream().map(p -> (IBase) p).collect(Collectors.<IBase>toList());
142158
return libraryProcessorFactory
143159
.create(requestDetails)
144160
.packageLibrary(
@@ -157,7 +173,8 @@ public IBaseBundle packageLibrary(
157173
.distinct()
158174
.map(PrimitiveType::getValueAsString)
159175
.collect(Collectors.toList()),
160-
getEndpoint(fhirVersion, terminologyEndpoint),
176+
artifactEndpointConfigurationParam,
177+
terminologyEndpointParam,
161178
usePut == null ? Boolean.FALSE : usePut.booleanValue()));
162179
}
163180
}

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/BaseKnowledgeArtifactVisitor.java

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@
3535
import org.opencds.cqf.fhir.utility.adapter.IEndpointAdapter;
3636
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactAdapter;
3737
import org.opencds.cqf.fhir.utility.adapter.IKnowledgeArtifactVisitor;
38-
import org.opencds.cqf.fhir.utility.client.TerminologyServerClient;
38+
import org.opencds.cqf.fhir.utility.client.terminology.ITerminologyProviderRouter;
3939
import org.slf4j.Logger;
4040
import org.slf4j.LoggerFactory;
4141

@@ -131,7 +131,7 @@ protected void recursiveGather(
131131
List<String> include,
132132
ImmutableTriple<List<String>, List<String>, List<String>> versionTuple,
133133
IEndpointAdapter terminologyEndpoint,
134-
TerminologyServerClient client,
134+
ITerminologyProviderRouter router,
135135
IBaseOperationOutcome[] messagesWrapper)
136136
throws PreconditionFailedException {
137137
Map<String, String> igDependencyVersions = extractIgDependencyVersions(adapter);
@@ -142,7 +142,7 @@ protected void recursiveGather(
142142
include,
143143
versionTuple,
144144
terminologyEndpoint,
145-
client,
145+
router,
146146
messagesWrapper,
147147
igDependencyVersions);
148148
}
@@ -154,7 +154,7 @@ protected void recursiveGather(
154154
List<String> include,
155155
ImmutableTriple<List<String>, List<String>, List<String>> versionTuple,
156156
IEndpointAdapter terminologyEndpoint,
157-
TerminologyServerClient client,
157+
ITerminologyProviderRouter client,
158158
IBaseOperationOutcome[] messagesWrapper,
159159
Map<String, String> igDependencyVersions)
160160
throws PreconditionFailedException {
@@ -246,11 +246,11 @@ protected <T extends ICompositeType & IBaseHasExtensions> void addRelatedArtifac
246246
}
247247

248248
private IDomainResource tryGetValueSetsFromTxServer(
249-
IDependencyInfo ra, TerminologyServerClient client, IEndpointAdapter endpoint) {
250-
if (client != null
249+
IDependencyInfo ra, ITerminologyProviderRouter router, IEndpointAdapter endpoint) {
250+
if (router != null
251251
&& endpoint != null
252252
&& Canonicals.getResourceType(ra.getReference()).equals("ValueSet")) {
253-
return client.getValueSetResource(endpoint, ra.getReference()).orElse(null);
253+
return router.getValueSetResource(endpoint, ra.getReference()).orElse(null);
254254
}
255255
return null;
256256
}

cqf-fhir-cr/src/main/java/org/opencds/cqf/fhir/cr/visitor/ExpandHelper.java

Lines changed: 72 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@
3030
import org.opencds.cqf.fhir.utility.adapter.IParametersParameterComponentAdapter;
3131
import org.opencds.cqf.fhir.utility.adapter.IValueSetAdapter;
3232
import org.opencds.cqf.fhir.utility.client.ExpandRunner.TerminologyServerExpansionException;
33-
import org.opencds.cqf.fhir.utility.client.TerminologyServerClient;
33+
import org.opencds.cqf.fhir.utility.client.terminology.ArtifactEndpointConfiguration;
34+
import org.opencds.cqf.fhir.utility.client.terminology.ITerminologyProviderRouter;
35+
import org.opencds.cqf.fhir.utility.client.terminology.ITerminologyServerClient;
3436
import org.slf4j.Logger;
3537
import org.slf4j.LoggerFactory;
3638

@@ -39,7 +41,7 @@ public class ExpandHelper {
3941
private static final Logger log = LoggerFactory.getLogger(ExpandHelper.class);
4042
private final IRepository repository;
4143
private final IAdapterFactory adapterFactory;
42-
private final TerminologyServerClient terminologyServerClient;
44+
private final ITerminologyProviderRouter terminologyServerRouter;
4345
public static final List<String> unsupportedParametersToRemove = List.of(Constants.CANONICAL_VERSION);
4446

4547
// Parameters we care to validate round-trip in the expansion
@@ -50,10 +52,10 @@ public class ExpandHelper {
5052
"system-version", "used-system-version",
5153
"valueset-version", "used-valueset-version");
5254

53-
public ExpandHelper(IRepository repository, TerminologyServerClient server) {
55+
public ExpandHelper(IRepository repository, ITerminologyProviderRouter server) {
5456
this.repository = repository;
5557
adapterFactory = IAdapterFactory.forFhirContext(this.repository.fhirContext());
56-
terminologyServerClient = server;
58+
terminologyServerRouter = server;
5759
}
5860

5961
private FhirContext fhirContext() {
@@ -91,9 +93,10 @@ public void expandValueSet(
9193
.filter(e -> e.getUrl().equals(Constants.AUTHORITATIVE_SOURCE_URL))
9294
.findFirst()
9395
.map(url -> ((IPrimitiveType<String>) url.getValue()).getValueAsString())
94-
.map(url -> TerminologyServerClient.getAddressBase(url, fhirContext()))
96+
.map(url -> ITerminologyServerClient.getAddressBase(url, fhirContext()))
9597
.orElse(null);
96-
// If terminologyEndpoint exists and we have no authoritativeSourceUrl or the authoritativeSourceUrl matches the
98+
// If terminologyEndpoint exists, and we have no authoritativeSourceUrl or the authoritativeSourceUrl matches
99+
// the
97100
// terminologyEndpoint address then we will use the terminologyEndpoint for expansion
98101
if (terminologyEndpoint.isPresent()
99102
&& (authoritativeSourceUrl == null
@@ -144,10 +147,64 @@ else if (valueSet.hasGroupingCompose()) {
144147
expandedList.add(valueSet.getUrl());
145148
}
146149

150+
/**
151+
* Expands a ValueSet using CRMI artifact endpoint configurations for routing.
152+
* Falls back to legacy terminologyEndpoint if no configurations match, then to local expansion.
153+
*
154+
* @param valueSet the ValueSet to expand
155+
* @param expansionParameters expansion parameters
156+
* @param artifactEndpointConfigurations CRMI endpoint configurations for routing
157+
* @param terminologyEndpoint legacy single endpoint (used as fallback)
158+
* @param valueSets list of all ValueSets being processed
159+
* @param expandedList list of already expanded ValueSet URLs
160+
* @param expansionTimestamp timestamp for expansion
161+
*/
162+
public void expandValueSet(
163+
IValueSetAdapter valueSet,
164+
IParametersAdapter expansionParameters,
165+
List<ArtifactEndpointConfiguration> artifactEndpointConfigurations,
166+
Optional<IEndpointAdapter> terminologyEndpoint,
167+
List<IValueSetAdapter> valueSets,
168+
List<String> expandedList,
169+
Date expansionTimestamp) {
170+
// Have we already expanded this ValueSet?
171+
if (expandedList.contains(valueSet.getUrl())) {
172+
return;
173+
}
174+
filterOutUnsupportedParameters(expansionParameters);
175+
176+
// Try CRMI configuration-based routing first if configurations are provided
177+
if (artifactEndpointConfigurations != null && !artifactEndpointConfigurations.isEmpty()) {
178+
try {
179+
var expandedResult = terminologyServerRouter.expandWithConfigurations(
180+
valueSet, artifactEndpointConfigurations, expansionParameters);
181+
if (expandedResult != null) {
182+
var expandedValueSet = (IValueSetAdapter) adapterFactory.createResource(expandedResult);
183+
if (!valueSet.hasVersion()) {
184+
valueSet.setVersion(expandedValueSet.getVersion());
185+
}
186+
valueSet.setExpansion(expandedValueSet.getExpansion());
187+
validateExpansionParameters(valueSet, expansionParameters);
188+
expandedList.add(valueSet.getUrl());
189+
return;
190+
}
191+
} catch (TerminologyServerExpansionException e) {
192+
log.warn(
193+
"Failed to expand value set {} using artifact endpoint configurations. Reason: {}. "
194+
+ "Will attempt fallback expansion.",
195+
valueSet.getUrl(),
196+
e.getMessage());
197+
}
198+
}
199+
200+
// Fall back to legacy single endpoint or local expansion
201+
expandValueSet(valueSet, expansionParameters, terminologyEndpoint, valueSets, expandedList, expansionTimestamp);
202+
}
203+
147204
private void terminologyServerExpand(
148205
IValueSetAdapter valueSet, IParametersAdapter expansionParameters, IEndpointAdapter terminologyEndpoint) {
149206
var expandedValueSet = (IValueSetAdapter) adapterFactory.createResource(
150-
terminologyServerClient.expand(valueSet, terminologyEndpoint, expansionParameters));
207+
terminologyServerRouter.expand(valueSet, terminologyEndpoint, expansionParameters));
151208
// expansions are only valid for a particular version
152209
if (!valueSet.hasVersion()) {
153210
valueSet.setVersion(expandedValueSet.getVersion());
@@ -261,7 +318,7 @@ private IValueSetAdapter getIncludedValueSet(
261318
.orElseGet(() -> {
262319
if (terminologyEndpoint.isPresent()) {
263320
try {
264-
return terminologyServerClient
321+
return terminologyServerRouter
265322
.getValueSetResource(terminologyEndpoint.get(), reference)
266323
.map(r -> (IValueSetAdapter) adapterFactory.createResource(r))
267324
.orElse(null);
@@ -286,29 +343,29 @@ private void expandIncluded(
286343
IValueSetAdapter includedVS) {
287344
// update url and version exp params for child expansions
288345
var childExpParams = (IParametersAdapter) adapterFactory.createResource(expansionParameters.copy());
289-
if (childExpParams.hasParameter(TerminologyServerClient.urlParamName)) {
346+
if (childExpParams.hasParameter(ITerminologyServerClient.urlParamName)) {
290347
var newParams = childExpParams.getParameter().stream()
291-
.filter(p -> !p.getName().equals(TerminologyServerClient.urlParamName))
348+
.filter(p -> !p.getName().equals(ITerminologyServerClient.urlParamName))
292349
.collect(Collectors.toList());
293350
if (includedVS.hasUrl()) {
294351
newParams.add(adapterFactory.createParametersParameter((IBaseBackboneElement)
295352
(fhirContext().getVersion().getVersion() == FhirVersionEnum.DSTU3
296353
? Parameters.newUriPart(
297-
fhirContext(), TerminologyServerClient.urlParamName, includedVS.getUrl())
354+
fhirContext(), ITerminologyServerClient.urlParamName, includedVS.getUrl())
298355
: Parameters.newUrlPart(
299-
fhirContext(), TerminologyServerClient.urlParamName, includedVS.getUrl()))));
356+
fhirContext(), ITerminologyServerClient.urlParamName, includedVS.getUrl()))));
300357
}
301358
childExpParams.setParameter(newParams.stream()
302359
.map(IParametersParameterComponentAdapter::get)
303360
.toList());
304361
}
305-
if (childExpParams.hasParameter(TerminologyServerClient.versionParamName)) {
362+
if (childExpParams.hasParameter(ITerminologyServerClient.versionParamName)) {
306363
var newParams = childExpParams.getParameter().stream()
307-
.filter(p -> !p.getName().equals(TerminologyServerClient.versionParamName))
364+
.filter(p -> !p.getName().equals(ITerminologyServerClient.versionParamName))
308365
.collect(Collectors.toList());
309366
if (includedVS.hasVersion()) {
310367
newParams.add(adapterFactory.createParametersParameter((IBaseBackboneElement) Parameters.newStringPart(
311-
fhirContext(), TerminologyServerClient.versionParamName, includedVS.getVersion())));
368+
fhirContext(), ITerminologyServerClient.versionParamName, includedVS.getVersion())));
312369
}
313370
childExpParams.setParameter(newParams.stream()
314371
.map(IParametersParameterComponentAdapter::get)

0 commit comments

Comments
 (0)