Skip to content

Commit

Permalink
Updated JpaTerminologyProvider to support expansion with version and …
Browse files Browse the repository at this point in the history
…code system bindings (#645)

* Removed exception thrown in JpaTerminologyProvider when expanding with version and codesystem bindings ... added tests to validate support

* Added canonical versioning to urls and test for multiple valueset versions
  • Loading branch information
c-schuler authored Oct 26, 2022
1 parent 1a6d940 commit 2a2069f
Show file tree
Hide file tree
Showing 10 changed files with 12,115 additions and 30 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
import java.util.Map;

import org.cqframework.cql.elm.execution.VersionedIdentifier;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.opencds.cqf.cql.engine.runtime.Code;
import org.opencds.cqf.cql.engine.terminology.CodeSystemInfo;
import org.opencds.cqf.cql.engine.terminology.TerminologyProvider;
Expand All @@ -20,6 +19,7 @@
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.rest.api.server.RequestDetails;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import org.opencds.cqf.ruler.utility.Canonicals;

/**
* This class provides an implementation of the cql-engine's TerminologyProvider
Expand All @@ -30,7 +30,6 @@ public class JpaTerminologyProvider implements TerminologyProvider {

private final ITermReadSvc myTerminologySvc;
private final IValidationSupport myValidationSupport;
private final RequestDetails myRequestDetails;
private final Map<VersionedIdentifier, List<Code>> myGlobalCodeCache;

public JpaTerminologyProvider(ITermReadSvc theTerminologySvc, IValidationSupport theValidationSupport,
Expand All @@ -39,12 +38,10 @@ public JpaTerminologyProvider(ITermReadSvc theTerminologySvc, IValidationSupport
}

public JpaTerminologyProvider(ITermReadSvc theTerminologySvc, IValidationSupport theValidationSupport,
Map<VersionedIdentifier, List<Code>> theGlobalCodeCache,
RequestDetails theRequestDetails) {
Map<VersionedIdentifier, List<Code>> theGlobalCodeCache, RequestDetails theRequestDetails) {
myTerminologySvc = theTerminologySvc;
myValidationSupport = theValidationSupport;
myGlobalCodeCache = theGlobalCodeCache;
myRequestDetails = theRequestDetails;
}

@Override
Expand All @@ -59,30 +56,10 @@ public boolean in(Code code, ValueSetInfo valueSet) throws ResourceNotFoundExcep
return false;
}

protected boolean hasUrlId(ValueSetInfo valueSet) {
return valueSet.getId().startsWith("http://") || valueSet.getId().startsWith("https://");
}

protected boolean hasVersion(ValueSetInfo valueSet) {
return valueSet.getVersion() != null;
}

protected boolean hasVersionedCodeSystem(ValueSetInfo valueSet) {
return valueSet.getCodeSystems() != null && valueSet.getCodeSystems().size() > 1
|| valueSet.getCodeSystems() != null
&& valueSet.getCodeSystems().stream().anyMatch(x -> x.getVersion() != null);
}

@Override
public Iterable<Code> expand(ValueSetInfo valueSet) throws ResourceNotFoundException {
// This could possibly be refactored into a single call to the underlying HAPI
// Terminology service. Need to think through that..,
IBaseResource vs;
if (hasUrlId(valueSet) && (hasVersion(valueSet) || hasVersionedCodeSystem(valueSet))) {
throw new UnsupportedOperationException(String.format(
"Could not expand value set %s; version and code system bindings are not supported at this time.",
valueSet.getId()));
}

VersionedIdentifier vsId = new VersionedIdentifier().withId(valueSet.getId()).withVersion(valueSet.getVersion());

Expand All @@ -94,19 +71,27 @@ public Iterable<Code> expand(ValueSetInfo valueSet) throws ResourceNotFoundExcep
valueSetExpansionOptions.setFailOnMissingCodeSystem(false);
valueSetExpansionOptions.setCount(Integer.MAX_VALUE);

vs = myTerminologySvc.expandValueSet(valueSetExpansionOptions, valueSet.getId());
if (valueSet.getVersion() != null && Canonicals.getUrl(valueSet.getId()) != null
&& Canonicals.getVersion(valueSet.getId()) == null) {
valueSet.setId(valueSet.getId() + "|" + valueSet.getVersion());
}

List<Code> codes = getCodes((org.hl7.fhir.r4.model.ValueSet) vs);
org.hl7.fhir.r4.model.ValueSet vs =
myTerminologySvc.expandValueSet(valueSetExpansionOptions, valueSet.getId());

List<Code> codes = getCodes(vs);
this.myGlobalCodeCache.put(vsId, codes);
return codes;
}

@Override
public Code lookup(Code code, CodeSystemInfo codeSystem) throws ResourceNotFoundException {
LookupCodeResult cs = myTerminologySvc.lookupCode(new ValidationSupportContext(myValidationSupport),
codeSystem.getId(), code.getCode());
LookupCodeResult cs = myTerminologySvc.lookupCode(
new ValidationSupportContext(myValidationSupport), codeSystem.getId(), code.getCode());

code.setDisplay(cs.getCodeDisplay());
if (cs != null) {
code.setDisplay(cs.getCodeDisplay());
}
code.setSystem(codeSystem.getId());

return code;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package org.opencds.cqf.ruler.cql;

import ca.uhn.fhir.jpa.api.config.DaoConfig;
import ca.uhn.fhir.jpa.partition.SystemRequestDetails;
import org.apache.commons.collections4.IterableUtils;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.opencds.cqf.cql.engine.terminology.ValueSetInfo;
import org.opencds.cqf.ruler.test.RestIntegrationTest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = { JpaTerminologyProviderIT.class },
properties = { "hapi.fhir.fhir_version=r4" })
class JpaTerminologyProviderIT extends RestIntegrationTest {

@Autowired
DaoConfig daoConfig;
@Autowired
private JpaTerminologyProviderFactory jpaTerminologyProviderFactory;
private JpaTerminologyProvider terminologyProvider;

@BeforeAll
void setup() {
SystemRequestDetails requestDetails = new SystemRequestDetails();
requestDetails.setFhirContext(getFhirContext());
requestDetails.setFhirServerBase(getServerBase());
terminologyProvider = jpaTerminologyProviderFactory.create(requestDetails);
}

@Test
void testExpandFhirCodeSystem() {
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-filter-comparator.json",
"http://hl7.org/fhir/us/cqfmeasures/ValueSet/value-filter-comparator",
"3.0.0");
assertNotNull(expandResult);
assertEquals(7, IterableUtils.size(expandResult));
}

/*
Possible issue with the following codes having the same code, but different display values:
/[HPF]
/[LPF]
[beth'U]
[pptr]
[todd'U]
[iU]
{Ehrlich'U}/100.g
The ValueSet composition includes 1364 codes, but the expansion returns 1357 codes
- display values are not present in the expansion
*/
@Test
void testExpandUnitsOfMeasureCodeSystemMoreThan1000() {
daoConfig.setMaximumExpansionSize(Integer.MAX_VALUE);
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-ucum-common.json", "http://hl7.org/fhir/ValueSet/ucum-common", "1.0.0");
assertEquals(1357, IterableUtils.size(expandResult));
}

@Test
void testPreExpandedRxNormCodeSystemMoreThan1000() {
daoConfig.setMaximumExpansionSize(Integer.MAX_VALUE);
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-opioid-analgesics-with-ambulatory-misuse-potential.json",
"http://fhir.org/guides/cdc/opioid-cds/ValueSet/opioid-analgesics-with-ambulatory-misuse-potential",
"0.1.1");
assertNotNull(expandResult);
assertEquals(1180, IterableUtils.size(expandResult));
}

@Test
void testPreExpandedSnomedCodeSystem() {
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-hospice-procedure.json",
"http://fhir.org/guides/cdc/opioid-cds/ValueSet/hospice-procedure",
"1.0.0");
assertNotNull(expandResult);
assertEquals(6, IterableUtils.size(expandResult));
}

@Test
void testExpandIgDefinedCodeSystem() {
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-opioidcds-indicator.json",
"http://fhir.org/guides/cdc/opioid-cds/ValueSet/opioidcds-indicator",
"0.1.1");
assertNotNull(expandResult);
assertEquals(3, IterableUtils.size(expandResult));
}

@Test
void testExpandFilterWithoutExpansion() {
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-hospice-finding.json",
"http://fhir.org/guides/cdc/opioid-cds/ValueSet/hospice-finding",
"0.1.1");
assertNotNull(expandResult);
assertEquals(0, IterableUtils.size(expandResult));
}

@Test
void testMultipleVersions() {
Iterable<org.opencds.cqf.cql.engine.runtime.Code> expandResult = getExpansion(
"valueset-event-status-4.3.0.json",
"http://example.org/fhir/ValueSet/event-status",
"4.3.0");
assertNotNull(expandResult);
assertEquals(8, IterableUtils.size(expandResult));
expandResult = getExpansion(
"valueset-event-status-3.0.2.json",
"http://example.org/fhir/ValueSet/event-status",
"3.0.2");
assertNotNull(expandResult);
assertEquals(7, IterableUtils.size(expandResult));
/*
The last ValueSet added to the cache will be used when no version is supplied.
In theory, this is the desired behavior as the "latest" ValueSet is picked for expansion by default.
However, as in this case, the 4.3.0 version of the ValueSet was added to the cache before the 3.0.2 version.
Therefore, the latest version of the ValueSet was not picked for expansion.
This is not exactly ideal behavior and the FHIR spec states:
"Note that if a References to a canonical URL does not have a version, and the server finds
multiple versions for the value set, the system using the reference should pick the latest
version of the target resource and use that." (http://www.hl7.org/fhir/references.html#canonical)
Based on that, this is not a bug in the HAPI code. It is not the preferred approach though.
*/
ValueSetInfo vsInfo = new ValueSetInfo().withId("http://example.org/fhir/ValueSet/event-status");
expandResult = terminologyProvider.expand(vsInfo);
assertEquals(7, IterableUtils.size(expandResult));
}

private Iterable<org.opencds.cqf.cql.engine.runtime.Code> getExpansion(
String vsFileName, String url, String version) {
loadResource(vsFileName);
ValueSetInfo vsInfo = new ValueSetInfo().withId(url).withVersion(version);
return terminologyProvider.expand(vsInfo);
}
}
68 changes: 68 additions & 0 deletions plugin/cql/src/test/resources/valueset-event-status-3.0.2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
{
"resourceType" : "ValueSet",
"id" : "event-status-3.0.2",
"meta" : {
"profile" : ["http://hl7.org/fhir/StructureDefinition/shareablevalueset"]
},
"url" : "http://example.org/fhir/ValueSet/event-status",
"version" : "3.0.2",
"name" : "EventStatus",
"title" : "EventStatus",
"status" : "draft",
"experimental" : false,
"date" : "2021-03-11T17:06:20+11:00",
"publisher" : "HL7 (FHIR Project)",
"contact" : [
{
"telecom" : [
{
"system" : "url",
"value" : "http://hl7.org/fhir"
},
{
"system" : "email",
"value" : "fhir@lists.hl7.org"
}
]
}
],
"description" : "Codes identifying the lifecycle stage of an event.",
"immutable" : true,
"compose" : {
"include" : [
{
"system": "http://example.org/fhir/event-status",
"concept": [
{
"code" : "preparation",
"display" : "Preparation"
},
{
"code" : "in-progress",
"display" : "In Progress"
},
{
"code" : "on-hold",
"display" : "On Hold"
},
{
"code" : "stopped",
"display" : "Stopped"
},
{
"code" : "completed",
"display" : "Completed"
},
{
"code" : "entered-in-error",
"display" : "Entered in Error"
},
{
"code" : "unknown",
"display" : "Unknown"
}
]
}
]
}
}
78 changes: 78 additions & 0 deletions plugin/cql/src/test/resources/valueset-event-status-4.3.0.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"resourceType" : "ValueSet",
"id" : "event-status-4.3.0",
"meta" : {
"profile" : ["http://hl7.org/fhir/StructureDefinition/shareablevalueset"]
},
"url" : "http://example.org/fhir/ValueSet/event-status",
"identifier" : [
{
"system" : "urn:ietf:rfc:3986",
"value" : "urn:oid:2.16.840.1.113883.4.642.3.109"
}
],
"version" : "4.3.0",
"name" : "EventStatus",
"title" : "EventStatus",
"status" : "draft",
"experimental" : true,
"date" : "2022-05-28T12:47:40+10:00",
"publisher" : "HL7 (FHIR Project)",
"contact" : [
{
"telecom" : [
{
"system" : "url",
"value" : "http://hl7.org/fhir"
},
{
"system" : "email",
"value" : "fhir@lists.hl7.org"
}
]
}
],
"description" : "Codes identifying the lifecycle stage of an event.",
"immutable" : true,
"compose" : {
"include" : [
{
"system": "http://example.org/fhir/event-status",
"concept": [
{
"code" : "preparation",
"display" : "Preparation"
},
{
"code" : "in-progress",
"display" : "In Progress"
},
{
"code" : "not-done",
"display" : "Not Done"
},
{
"code" : "on-hold",
"display" : "On Hold"
},
{
"code" : "stopped",
"display" : "Stopped"
},
{
"code" : "completed",
"display" : "Completed"
},
{
"code" : "entered-in-error",
"display" : "Entered in Error"
},
{
"code" : "unknown",
"display" : "Unknown"
}
]
}
]
}
}
Loading

0 comments on commit 2a2069f

Please sign in to comment.