Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Updated JpaTerminologyProvider to support expansion with version and code system bindings #645

Merged
merged 2 commits into from
Oct 26, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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());
JPercival marked this conversation as resolved.
Show resolved Hide resolved
JPercival marked this conversation as resolved.
Show resolved Hide resolved

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());
JPercival marked this conversation as resolved.
Show resolved Hide resolved

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() {
JPercival marked this conversation as resolved.
Show resolved Hide resolved
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