fixing value expansion filtered by url (#5969)

* test

* adding support for expansion based on filters and properties

* added changelog

* adding test cases

* spotless

* fixed some warnings

* spotless

* merge conflict resolution

---------

Co-authored-by: leif stawnyczy <leifstawnyczy@leifs-mbp.home>
This commit is contained in:
TipzCM 2024-06-06 14:41:38 -04:00 committed by GitHub
parent 48c387ecb0
commit 5b75639718
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 763 additions and 36 deletions

View File

@ -0,0 +1,8 @@
---
type: add
issue: 5968
title: "Added support for filtering on CodeSystem defined properties when
using database (hibernate) filtering.
The following ValueSet filters can be used to filter on
CodeSystem defined properties: EQUAL, EXISTS, IN, NOTIN
"

View File

@ -118,7 +118,7 @@ public class TermConceptProperty implements Serializable {
private byte[] myValueBin;
@Enumerated(EnumType.ORDINAL)
@Column(name = "PROP_TYPE", nullable = false, length = MAX_PROPTYPE_ENUM_LENGTH)
@Column(name = "PROP_TYPE", nullable = false)
@JdbcTypeCode(SqlTypes.INTEGER)
private TermConceptPropertyTypeEnum myType;

View File

@ -21,13 +21,11 @@ package ca.uhn.fhir.jpa.entity;
/**
* @see TermConceptProperty#getType()
* @see TermConceptProperty#MAX_PROPTYPE_ENUM_LENGTH
*/
public enum TermConceptPropertyTypeEnum {
/*
* VALUES SHOULD BE <= 6 CHARS LONG!
*
* We store this in a DB column of that length
/**
* Do not change order - the ordinal is used by hibernate in the column.
* TermConceptProperty#getType()
*/
/**
@ -37,5 +35,21 @@ public enum TermConceptPropertyTypeEnum {
/**
* Coding
*/
CODING
CODING,
/**
* Boolean values
*/
BOOLEAN,
/**
* Integer values
*/
INTEGER,
/**
* Decimal or float values.
*/
DECIMAL,
/**
* Date and time values.
*/
DATETIME
}

View File

@ -133,6 +133,8 @@ import org.hl7.fhir.r4.model.CanonicalType;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.CodeableConcept;
import org.hl7.fhir.r4.model.Coding;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.DomainResource;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Extension;
@ -1159,7 +1161,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
int accumulatedBatchesSoFar = 0;
for (var next : searchProps.getSearchScroll()) {
try (SearchScroll<EntityReference> scroll = next.get()) {
ourLog.debug(
"Beginning batch expansion for {} with max results per batch: {}",
(theAdd ? "inclusion" : "exclusion"),
@ -1394,7 +1395,10 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
return;
}
if (isBlank(theFilter.getValue()) || theFilter.getOp() == null || isBlank(theFilter.getProperty())) {
// if filter type is EXISTS, there's no reason to worry about the value (we won't set it anyways)
if ((isBlank(theFilter.getValue()) && theFilter.getOp() != ValueSet.FilterOperator.EXISTS)
|| theFilter.getOp() == null
|| isBlank(theFilter.getProperty())) {
throw new InvalidRequestException(
Msg.code(891) + "Invalid filter, must have fields populated: property op value");
}
@ -1441,8 +1445,37 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
ValueSet.ConceptSetFilterComponent theFilter) {
String value = theFilter.getValue();
Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty(), value);
theB.must(theF.match().field(term.field()).matching(term.text()));
if (theFilter.getOp() == ValueSet.FilterOperator.EXISTS) {
// EXISTS has no value and is thus handled differently
Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty());
theB.must(theF.exists().field(term.field()));
} else {
Term term = new Term(CONCEPT_PROPERTY_PREFIX_NAME + theFilter.getProperty(), value);
switch (theFilter.getOp()) {
case EQUAL:
theB.must(theF.match().field(term.field()).matching(term.text()));
break;
case IN:
case NOTIN:
boolean isNotFilter = theFilter.getOp() == ValueSet.FilterOperator.NOTIN;
// IN and NOTIN expect comma separated lists
String[] values = term.text().split(",");
Set<String> valueSet = new HashSet<>(Arrays.asList(values));
if (isNotFilter) {
theB.filter(theF.not(theF.terms().field(term.field()).matchingAny(valueSet)));
} else {
theB.filter(theF.terms().field(term.field()).matchingAny(valueSet));
}
break;
default:
/*
* We do not need to handle REGEX, because that's handled in parent
* We also don't handle EXISTS because that's a separate area (with different term)
*/
throw new InvalidRequestException(Msg.code(2526) + "Unsupported property filter "
+ theFilter.getOp().getDisplay());
}
}
}
private void handleFilterRegex(
@ -1598,7 +1631,7 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
SearchPredicateFactory f,
BooleanPredicateClausesStep<?> b,
ValueSet.ConceptSetFilterComponent theFilter) {
TermConcept code = findCodeForFilterCriteria(theSystem, theFilter);
TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, theFilter);
if (theFilter.getOp() == ValueSet.FilterOperator.ISA) {
ourLog.debug(
@ -1621,7 +1654,8 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
}
@Nonnull
private TermConcept findCodeForFilterCriteria(String theSystem, ValueSet.ConceptSetFilterComponent theFilter) {
private TermConcept findCodeForFilterCriteriaCodeOrConcept(
String theSystem, ValueSet.ConceptSetFilterComponent theFilter) {
return findCode(theSystem, theFilter.getValue())
.orElseThrow(() ->
new InvalidRequestException(Msg.code(2071) + "Invalid filter criteria - code does not exist: {"
@ -1866,18 +1900,23 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
for (ValueSet.ConceptSetFilterComponent nextFilter : theInclude.getFilter()) {
boolean handled = false;
switch (nextFilter.getProperty()) {
switch (nextFilter.getProperty().toLowerCase()) {
case "concept":
case "code":
if (nextFilter.getOp() == ValueSet.FilterOperator.ISA) {
theValueSetCodeAccumulator.addMessage(
"Processing IS-A filter in database - Note that Hibernate Search is not enabled on this server, so this operation can be inefficient.");
TermConcept code = findCodeForFilterCriteria(theSystem, nextFilter);
TermConcept code = findCodeForFilterCriteriaCodeOrConcept(theSystem, nextFilter);
addConceptAndChildren(
theValueSetCodeAccumulator, theAddedCodes, theInclude, theSystem, theAdd, code);
handled = true;
}
break;
default:
// TODO - we need to handle other properties (fields)
// and other operations (not just is-a)
// in some (preferably generic) way
break;
}
if (!handled) {
@ -3300,6 +3339,20 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
if (next.getValue() instanceof StringType) {
property.setType(TermConceptPropertyTypeEnum.STRING);
property.setValue(next.getValueStringType().getValue());
} else if (next.getValue() instanceof BooleanType) {
property.setType(TermConceptPropertyTypeEnum.BOOLEAN);
property.setValue(((BooleanType) next.getValue()).getValueAsString());
} else if (next.getValue() instanceof IntegerType) {
property.setType(TermConceptPropertyTypeEnum.INTEGER);
property.setValue(((IntegerType) next.getValue()).getValueAsString());
} else if (next.getValue() instanceof DecimalType) {
property.setType(TermConceptPropertyTypeEnum.DECIMAL);
property.setValue(((DecimalType) next.getValue()).getValueAsString());
} else if (next.getValue() instanceof DateTimeType) {
// DateType is not supported because it's not
// supported in CodeSystem.setValue
property.setType(TermConceptPropertyTypeEnum.DATETIME);
property.setValue(((DateTimeType) next.getValue()).getValueAsString());
} else if (next.getValue() instanceof Coding) {
Coding nextCoding = next.getValueCoding();
property.setType(TermConceptPropertyTypeEnum.CODING);
@ -3307,7 +3360,6 @@ public class TermReadSvcImpl implements ITermReadSvc, IHasScheduledJobs {
property.setValue(nextCoding.getCode());
property.setDisplay(nextCoding.getDisplay());
} else if (next.getValue() != null) {
// TODO: LOINC has properties of type BOOLEAN that we should handle
ourLog.warn("Don't know how to handle properties of type: "
+ next.getValue().getClass());
continue;

View File

@ -3,6 +3,7 @@ package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoCodeSystem;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.api.dao.IFhirSystemDao;
@ -66,7 +67,7 @@ import static org.mockito.Mockito.when;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = TestR4ConfigWithElasticHSearch.class)
@RequiresDocker
public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest {
public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest implements IValueSetExpansionIT {
protected static final String CS_URL = "http://example.com/my_code_system";
@Autowired
@ -118,6 +119,41 @@ public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest {
purgeDatabase(myStorageSettings, mySystemDao, myResourceReindexingSvc, mySearchCoordinatorSvc, mySearchParamRegistry, myBulkDataExportJobSchedulingHelper);
}
@Override
public FhirContext getFhirContext() {
return myFhirContext;
}
@Override
public ITermDeferredStorageSvc getTerminologyDefferedStorageService() {
return myTerminologyDeferredStorageSvc;
}
@Override
public ITermReadSvc getTerminologyReadSvc() {
return myTermSvc;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
@Override
public IFhirResourceDaoValueSet<ValueSet> getValueSetDao() {
return myValueSetDao;
}
@Override
public JpaStorageSettings getJpaStorageSettings() {
return myStorageSettings;
}
@Override
protected PlatformTransactionManager getTxManager() {
return myTxManager;
}
void createCodeSystem() {
CodeSystem codeSystem = new CodeSystem();
codeSystem.setUrl(CS_URL);
@ -332,15 +368,4 @@ public class ValueSetExpansionR4ElasticsearchIT extends BaseJpaTest {
}
}
}
@Override
protected FhirContext getFhirContext() {
return myFhirContext;
}
@Override
protected PlatformTransactionManager getTxManager() {
return myTxManager;
}
}

View File

@ -1,12 +1,13 @@
package ca.uhn.fhir.jpa.term;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertFalse;
import ca.uhn.fhir.context.FhirVersionEnum;
import ca.uhn.fhir.context.support.ConceptValidationOptions;
import ca.uhn.fhir.context.support.IValidationSupport;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.i18n.Msg;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.entity.TermCodeSystem;
import ca.uhn.fhir.jpa.entity.TermCodeSystemVersion;
import ca.uhn.fhir.jpa.entity.TermConcept;
@ -17,12 +18,15 @@ import ca.uhn.fhir.jpa.entity.TermValueSetPreExpansionStatusEnum;
import ca.uhn.fhir.jpa.model.entity.ResourceTable;
import ca.uhn.fhir.jpa.model.util.JpaConstants;
import ca.uhn.fhir.jpa.search.builder.SearchBuilder;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.jpa.term.custom.CustomTerminologySet;
import ca.uhn.fhir.jpa.util.SqlQuery;
import ca.uhn.fhir.jpa.util.ValueSetTestUtil;
import ca.uhn.fhir.rest.server.exceptions.InternalErrorException;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import com.google.common.collect.Lists;
import jakarta.annotation.Nonnull;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
@ -43,7 +47,6 @@ import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import jakarta.annotation.Nonnull;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
@ -51,17 +54,17 @@ import java.util.stream.Collectors;
import static ca.uhn.fhir.util.HapiExtensions.EXT_VALUESET_EXPANSION_MESSAGE;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.fail;
import static org.awaitility.Awaitility.await;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public class ValueSetExpansionR4Test extends BaseTermR4Test {
public class ValueSetExpansionR4Test extends BaseTermR4Test implements IValueSetExpansionIT {
private static final Logger ourLog = LoggerFactory.getLogger(ValueSetExpansionR4Test.class);
private final ValueSetTestUtil myValueSetTestUtil = new ValueSetTestUtil(FhirVersionEnum.R4);
@ -71,6 +74,31 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
SearchBuilder.setMaxPageSize50ForTest(false);
}
@Override
public ITermDeferredStorageSvc getTerminologyDefferedStorageService() {
return myTerminologyDeferredStorageSvc;
}
@Override
public ITermReadSvc getTerminologyReadSvc() {
return myTermSvc;
}
@Override
public DaoRegistry getDaoRegistry() {
return myDaoRegistry;
}
@Override
public IFhirResourceDaoValueSet<ValueSet> getValueSetDao() {
return myValueSetDao;
}
@Override
public JpaStorageSettings getJpaStorageSettings() {
return myStorageSettings;
}
@Test
public void testDeletePreExpandedValueSet() throws IOException {
myStorageSettings.setPreExpandValueSets(true);
@ -2046,5 +2074,4 @@ public class ValueSetExpansionR4Test extends BaseTermR4Test {
}
}

View File

@ -0,0 +1,601 @@
package ca.uhn.fhir.jpa.term;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.ValueSetExpansionOptions;
import ca.uhn.fhir.jpa.api.config.JpaStorageSettings;
import ca.uhn.fhir.jpa.api.dao.DaoRegistry;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDaoValueSet;
import ca.uhn.fhir.jpa.api.model.DaoMethodOutcome;
import ca.uhn.fhir.jpa.term.api.ITermDeferredStorageSvc;
import ca.uhn.fhir.jpa.term.api.ITermReadSvc;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.server.SystemRequestDetails;
import org.hl7.fhir.r4.model.CodeSystem;
import org.hl7.fhir.r4.model.DateTimeType;
import org.hl7.fhir.r4.model.DecimalType;
import org.hl7.fhir.r4.model.IntegerType;
import org.hl7.fhir.r4.model.ValueSet;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.EnumSource;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;
public interface IValueSetExpansionIT {
static final String CODE_SYSTEM_CODE = "PRODUCT-MULTI-SOURCE";
static final String PROPERTY_NAME = "ACTIVE";
static final String CODE_SYSTEM_STR_BASE =
"""
{
"resourceType": "CodeSystem",
"id": "4fb48e4e-57a4-4844-be74-d93707bdf9a1",
"meta": {
"versionId": "4",
"lastUpdated": "2024-01-16T19:10:18.370+00:00",
"source": "#c8957026d46dfab5"
},
"url": "https://health.gov.on.ca/idms/fhir/CodeSystem/Internal-Product-Types",
"version": "1.0.0",
"name": "IDMS-Internal-Product-Types",
"status": "active",
"date": "2024-01-10",
"publisher": "IDMS",
"description": "This contains a lists of Product Type codes.",
"content": "complete",
"property": [{
"code": "ACTIVE",
"type": "boolean"
}],
"concept": [
{
"code": "PRODUCT-MULTI-SOURCE",
"display": "Multi source drug product streamlined or Multi source drug product non- streamlined",
"property": [
{
"code": "ACTIVE",
"valueBoolean": true
}
]
}
]
}
""";
static final String VALUE_SET_STR_BASE =
"""
{
"resourceType": "ValueSet",
"id": "e0324e95-6d5c-4b08-8832-d5f5cd00a29a",
"meta": {
"versionId": "7",
"lastUpdated": "2024-01-16T19:03:43.313+00:00",
"source": "#1f91b035f91cd290"
},
"url": "https://health.gov.on.ca/idms/fhir/ValueSet/IDMS-Product-Types",
"version": "1.0.0",
"name": "IDMS-Product-Types",
"title": "IDMS Product Types",
"status": "active",
"experimental": false,
"date": "2024-01-16",
"publisher": "IDMS",
"description": "List of Product Types",
"compose": {
"include": [
{
"system": "https://health.gov.on.ca/idms/fhir/CodeSystem/Internal-Product-Types",
"filter": [
{
"property": "ACTIVE",
"op": "=",
"value": "true"
}
]
}
]
}
}
""";
FhirContext getFhirContext();
ITermDeferredStorageSvc getTerminologyDefferedStorageService();
ITermReadSvc getTerminologyReadSvc();
DaoRegistry getDaoRegistry();
IFhirResourceDaoValueSet<ValueSet> getValueSetDao();
JpaStorageSettings getJpaStorageSettings();
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withFiltersThatShouldNotMatchInInclude_addsNoNewCodes(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup codesystem
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(new IntegerType(1));
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EXISTS -> {
filterComponent.setProperty(PROPERTY_NAME + "-not");
filterComponent.setValue(null);
}
case IN -> filterComponent.setValue("2,3,4");
case NOTIN -> filterComponent.setValue("1,2,3");
case EQUAL -> filterComponent.setValue("2");
default ->
// just in case
fail(theOperator.getDisplay() + " is not added for testing");
}
conceptSetComponent.setFilter(List.of(filterComponent));
// test
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
doFailedValueSetExpansionTest(codeSystem, valueSet);
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withBooleanFilteredValuesInInclude_addsMatchingValues(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup codesystem (nothing to do - base is already boolean friendly)
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("true");
case NOTIN -> filterComponent.setValue("false");
}
conceptSetComponent.setFilter(List.of(filterComponent)); // overwrite the filter
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
ValueSet expanded = doSuccessfulValueSetExpansionTest(codeSystem, valueSet);
assertTrue(expanded.getExpansion().getContains().stream()
.anyMatch(c -> c.getCode().equals(CODE_SYSTEM_CODE)));
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withIntegerFilteredValuesInInclude_addsMatchingValues(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup codesystem
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(new IntegerType(1));
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("1");
case NOTIN -> filterComponent.setValue("2,3,4");
}
conceptSetComponent.setFilter(List.of(filterComponent));
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
ValueSet expanded = doSuccessfulValueSetExpansionTest(codeSystem, valueSet);
assertTrue(expanded.getExpansion().getContains().stream()
.anyMatch(c -> c.getCode().equals(CODE_SYSTEM_CODE)));
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withDecimalFilteredValuesInInclude_addsMatchingValues(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup code system
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(new DecimalType(1.1));
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("1.1");
case NOTIN -> filterComponent.setValue("2.1,3.2,4.3");
}
conceptSetComponent.setFilter(List.of(filterComponent));
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
ValueSet expanded = doSuccessfulValueSetExpansionTest(codeSystem, valueSet);
assertTrue(expanded.getExpansion().getContains().stream()
.anyMatch(c -> c.getCode().equals(CODE_SYSTEM_CODE)));
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withDateTimeFilteredValuesInInclude_addsMatchingValues(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
Date now = new Date();
DateTimeType dt = new DateTimeType(now);
// setup codesystem
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(dt);
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue(dt.getValueAsString());
case NOTIN -> {
StringBuilder sb = new StringBuilder();
for (int i = 1; i < 3; i++) {
DateTimeType arbitraryDateTime =
new DateTimeType(Date.from(Instant.now().minus(i, ChronoUnit.SECONDS)));
if (!sb.isEmpty()) {
sb.append(",");
}
sb.append(arbitraryDateTime.getValueAsString());
}
filterComponent.setValue(sb.toString());
}
}
conceptSetComponent.setFilter(List.of(filterComponent));
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
ValueSet expanded = doSuccessfulValueSetExpansionTest(codeSystem, valueSet);
assertTrue(expanded.getExpansion().getContains().stream()
.anyMatch(c -> c.getCode().equals(CODE_SYSTEM_CODE)));
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withBooleanFilterInExclude_doesNotAddMatchingCode(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup codesystem (nothing to do - base is already boolean friendly)
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ValueSetComposeComponent composeComponent = valueSet.getCompose();
ValueSet.ConceptSetComponent exclude = composeComponent.getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("true");
case NOTIN -> filterComponent.setValue("false");
}
exclude.setFilter(List.of(filterComponent));
composeComponent.setExclude(List.of(exclude));
composeComponent.setInclude(null);
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
doFailedValueSetExpansionTest(codeSystem, valueSet);
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withIntegerFilteredValuesInExclude_doesNotAddMatchingCode(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup codesystem
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(new IntegerType(1));
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("1");
case NOTIN -> filterComponent.setValue("2,3,4");
}
conceptSetComponent.setFilter(List.of(filterComponent));
valueSet.getCompose().setExclude(List.of(conceptSetComponent));
valueSet.getCompose().setInclude(null);
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
doFailedValueSetExpansionTest(codeSystem, valueSet);
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withDecimalFilteredValuesInExclude_doesNotAddMatchingCode(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
// setup code system
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(new DecimalType(1.1));
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue("1.1");
case NOTIN -> filterComponent.setValue("2.1,3.2,4.3");
}
conceptSetComponent.setFilter(List.of(filterComponent));
valueSet.getCompose().setExclude(List.of(conceptSetComponent));
valueSet.getCompose().setInclude(null);
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
doFailedValueSetExpansionTest(codeSystem, valueSet);
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
@ParameterizedTest
@EnumSource(
value = ValueSet.FilterOperator.class,
mode = EnumSource.Mode.INCLUDE,
names = {"EQUAL", "EXISTS", "IN", "NOTIN"})
default void expandByIdentifier_withDateTimeFilteredValuesInExclude_doesNotAddMatchingCode(
ValueSet.FilterOperator theOperator) {
// setup
IParser parser = getFhirContext().newJsonParser();
Date now = new Date();
DateTimeType dt = new DateTimeType(now);
// setup codesystem
CodeSystem codeSystem = parser.parseResource(CodeSystem.class, CODE_SYSTEM_STR_BASE);
CodeSystem.ConceptDefinitionComponent conceptDefinitionComponent =
codeSystem.getConcept().get(0);
CodeSystem.ConceptPropertyComponent propertyComponent = new CodeSystem.ConceptPropertyComponent();
propertyComponent.setCode(PROPERTY_NAME);
propertyComponent.setValue(dt);
conceptDefinitionComponent.setProperty(List.of(propertyComponent));
// setup valueset
ValueSet valueSet = parser.parseResource(ValueSet.class, VALUE_SET_STR_BASE);
ValueSet.ConceptSetComponent conceptSetComponent =
valueSet.getCompose().getInclude().get(0);
ValueSet.ConceptSetFilterComponent filterComponent = new ValueSet.ConceptSetFilterComponent();
filterComponent.setProperty(PROPERTY_NAME);
filterComponent.setOp(theOperator);
switch (theOperator) {
case EQUAL, EXISTS, IN -> filterComponent.setValue(dt.getValueAsString());
case NOTIN -> {
StringBuilder sb = new StringBuilder();
for (int i = 1; i < 3; i++) {
DateTimeType arbitraryDateTime =
new DateTimeType(Date.from(Instant.now().minus(i, ChronoUnit.SECONDS)));
if (!sb.isEmpty()) {
sb.append(",");
}
sb.append(arbitraryDateTime.getValueAsString());
}
filterComponent.setValue(sb.toString());
}
}
conceptSetComponent.setFilter(List.of(filterComponent));
valueSet.getCompose().setExclude(List.of(conceptSetComponent));
valueSet.getCompose().setInclude(null);
boolean preExpand = getJpaStorageSettings().isPreExpandValueSets();
getJpaStorageSettings().setPreExpandValueSets(true);
try {
doFailedValueSetExpansionTest(codeSystem, valueSet);
} finally {
getJpaStorageSettings().setPreExpandValueSets(preExpand);
}
}
/**
* Runs the test for value set expansion that will find no new codes to add
* @param theCodeSystem the code system to create
* @param theValueSet the value set to expand
* @return the expanded value set
*/
private ValueSet doFailedValueSetExpansionTest(CodeSystem theCodeSystem, ValueSet theValueSet) {
ValueSet expandedValueSet = createCodeSystemAndValueSetAndReturnExpandedValueSet(theCodeSystem, theValueSet);
// validate
assertNotNull(expandedValueSet);
assertNotNull(expandedValueSet.getExpansion());
assertTrue(expandedValueSet.getExpansion().getContains().isEmpty());
// pass back for additional validation
return expandedValueSet;
}
/**
* Runs the test for value set expansion that will find codes to add
* @param theCodeSystem the code system to create
* @param theValueSet the value set to expand
* @return the expanded value set
*/
private ValueSet doSuccessfulValueSetExpansionTest(CodeSystem theCodeSystem, ValueSet theValueSet) {
ValueSet expandedValueSet = createCodeSystemAndValueSetAndReturnExpandedValueSet(theCodeSystem, theValueSet);
// validate
assertNotNull(expandedValueSet);
assertNotNull(expandedValueSet.getExpansion());
assertFalse(expandedValueSet.getExpansion().getContains().isEmpty());
// pass back for additional validation
return expandedValueSet;
}
private ValueSet createCodeSystemAndValueSetAndReturnExpandedValueSet(
CodeSystem theCodeSystem, ValueSet theValueSet) {
SystemRequestDetails requestDetails = new SystemRequestDetails();
String url = "https://health.gov.on.ca/idms/fhir/ValueSet/IDMS-Product-Types";
// create the code system
{
@SuppressWarnings("unchecked")
IFhirResourceDao<CodeSystem> codeSystemDao = getDaoRegistry().getResourceDao("CodeSystem");
DaoMethodOutcome outcome = codeSystemDao.create(theCodeSystem, requestDetails);
theCodeSystem.setId(outcome.getId());
getTerminologyDefferedStorageService().saveAllDeferred();
}
// create the value set
{
@SuppressWarnings("unchecked")
IFhirResourceDao<ValueSet> valueSetDao = getDaoRegistry().getResourceDao("ValueSet");
DaoMethodOutcome outcome = valueSetDao.create(theValueSet, requestDetails);
theValueSet.setId(outcome.getId());
getTerminologyReadSvc().preExpandDeferredValueSetsToTerminologyTables();
}
// test
ValueSetExpansionOptions options = new ValueSetExpansionOptions();
return getValueSetDao().expandByIdentifier(url, options);
}
}

View File

@ -259,7 +259,7 @@ public abstract class BaseJpaTest extends BaseTest {
@Autowired
private IResourceHistoryTableDao myResourceHistoryTableDao;
@Autowired
private DaoRegistry myDaoRegistry;
protected DaoRegistry myDaoRegistry;
@Autowired
protected ITermDeferredStorageSvc myTermDeferredStorageSvc;
private final List<Object> myRegisteredInterceptors = new ArrayList<>(1);