Skip to content

Commit 389c88d

Browse files
authored
Filter unversioned canonical refs from $package expansion parameters (#1033)
* Filter unversioned canonical refs from $package expansion parameters * Use Canonicals utility for canonical-URL detection in expansion-param filter * Spotless
1 parent a8dc5c1 commit 389c88d

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,13 @@ protected void handleValueSets(
303303
var params = (IParametersAdapter) createAdapterForResource(
304304
createAdapterForResource(expansionParams).copy());
305305

306+
// Drop any expansion-parameter whose value is an unversioned canonical URL.
307+
// Terminology servers reject expansions when system-version / canonicalVersion
308+
// entries lack a `|version` pin (the expansion would be non-deterministic),
309+
// so excluding them here keeps the Tx call viable. Non-URL-valued params
310+
// (booleans, codes, etc.) pass through unchanged.
311+
filterUnversionedCanonicalParams(params);
312+
306313
var valueSets = BundleHelper.getEntryResources(packagedBundle).stream()
307314
.filter(r -> r.fhirType().equals(VALUESET_FHIR_TYPE))
308315
.map(v -> (IValueSetAdapter) createAdapterForResource(v))
@@ -359,6 +366,41 @@ protected void handleValueSets(
359366
});
360367
}
361368

369+
/**
370+
* Removes expansion-parameter entries whose values are unversioned canonical URLs.
371+
* Logs a warning per dropped entry. Non-canonical-valued params (booleans, codes,
372+
* language tags, etc.) are kept. A value is treated as canonical when
373+
* {@link Canonicals#getUrl(String)} parses it (handles http/https URLs plus
374+
* urn:uuid / urn:oid forms) and versioned when {@link Canonicals#getVersion(String)}
375+
* returns non-null.
376+
*/
377+
private void filterUnversionedCanonicalParams(IParametersAdapter params) {
378+
if (params == null || params.getParameter() == null) {
379+
return;
380+
}
381+
var kept = params.getParameter().stream()
382+
.filter(p -> {
383+
if (!p.hasValue()) {
384+
return true;
385+
}
386+
var value = p.getPrimitiveValue();
387+
if (value == null || Canonicals.getUrl(value) == null) {
388+
return true;
389+
}
390+
if (Canonicals.getVersion(value) != null) {
391+
return true;
392+
}
393+
myLogger.warn(
394+
"Excluding unversioned canonical from $package expansion parameters: {}={}",
395+
p.getName(),
396+
value);
397+
return false;
398+
})
399+
.map(IParametersParameterComponentAdapter::get)
400+
.toList();
401+
params.setParameter(kept);
402+
}
403+
362404
// Helper to surface expansion parameter warnings as OperationOutcome issues
363405
private void addExpansionWarningsToOperationOutcome(IValueSetAdapter valueSetAdapter) {
364406
if (valueSetAdapter == null || valueSetAdapter.getExpansion() == null) {

cqf-fhir-cr/src/test/java/org/opencds/cqf/fhir/cr/visitor/r4/PackageVisitorTests.java

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1416,4 +1416,59 @@ private static String decodeCanonicalFromCvId(String id) {
14161416
byte[] bytes = Base64.getDecoder().decode(base64);
14171417
return new String(bytes, StandardCharsets.UTF_8);
14181418
}
1419+
1420+
@Test
1421+
void filterUnversionedCanonicalParams_dropsUrlsWithoutPin_keepsOthers() throws Exception {
1422+
var visitor = new PackageVisitor(repo);
1423+
1424+
var params = parameters();
1425+
params.addParameter("system-version", "http://snomed.info/sct"); // unversioned canonical — drop
1426+
params.addParameter("system-version", "http://loinc.org|2.81"); // versioned — keep
1427+
params.addParameter("canonicalVersion", "http://example.org/ValueSet/foo"); // unversioned canonical — drop
1428+
params.addParameter("canonicalVersion", "http://example.org/ValueSet/bar|1.0.0"); // versioned — keep
1429+
params.addParameter("system-version", "urn:oid:1.2.3.4"); // unversioned urn canonical — drop
1430+
params.addParameter("system-version", "urn:oid:1.2.3.4|2024"); // versioned urn canonical — keep
1431+
params.addParameter("displayLanguage", "en-US"); // non-canonical — keep
1432+
params.addParameter("activeOnly", true); // boolean — keep
1433+
1434+
var adapter = (IParametersAdapter)
1435+
IAdapterFactory.forFhirVersion(FhirVersionEnum.R4).createParameters(params);
1436+
1437+
var method =
1438+
PackageVisitor.class.getDeclaredMethod("filterUnversionedCanonicalParams", IParametersAdapter.class);
1439+
method.setAccessible(true);
1440+
method.invoke(visitor, adapter);
1441+
1442+
var remainingValues = adapter.getParameter().stream()
1443+
.map(p -> p.getName() + "=" + (p.hasValue() ? p.getPrimitiveValue() : "<no-value>"))
1444+
.toList();
1445+
1446+
assertEquals(5, remainingValues.size(), "should drop all three unversioned canonical entries");
1447+
assertTrue(remainingValues.contains("system-version=http://loinc.org|2.81"));
1448+
assertTrue(remainingValues.contains("canonicalVersion=http://example.org/ValueSet/bar|1.0.0"));
1449+
assertTrue(remainingValues.contains("system-version=urn:oid:1.2.3.4|2024"));
1450+
assertTrue(remainingValues.contains("displayLanguage=en-US"));
1451+
assertTrue(remainingValues.contains("activeOnly=true"));
1452+
assertFalse(remainingValues.stream().anyMatch(v -> v.equals("system-version=http://snomed.info/sct")));
1453+
assertFalse(
1454+
remainingValues.stream().anyMatch(v -> v.equals("canonicalVersion=http://example.org/ValueSet/foo")));
1455+
assertFalse(remainingValues.stream().anyMatch(v -> v.equals("system-version=urn:oid:1.2.3.4")));
1456+
}
1457+
1458+
@Test
1459+
void filterUnversionedCanonicalParams_handlesNullAndEmptyInputs() throws Exception {
1460+
var visitor = new PackageVisitor(repo);
1461+
var method =
1462+
PackageVisitor.class.getDeclaredMethod("filterUnversionedCanonicalParams", IParametersAdapter.class);
1463+
method.setAccessible(true);
1464+
1465+
// Null adapter — should not throw
1466+
method.invoke(visitor, new Object[] {null});
1467+
1468+
// Empty parameters — should not throw, no-op
1469+
var empty = (IParametersAdapter)
1470+
IAdapterFactory.forFhirVersion(FhirVersionEnum.R4).createParameters(parameters());
1471+
method.invoke(visitor, empty);
1472+
assertTrue(empty.getParameter().isEmpty());
1473+
}
14191474
}

0 commit comments

Comments
 (0)