Add support for _lanugage SP (#5300)

* Add support for _lanugage SP

* Add changelog

* Formatting fix

* Add to JPA

* Add validator

* Validate language SP

* Assign new code

* Fixes
This commit is contained in:
James Agnew 2023-09-12 20:35:17 -04:00 committed by GitHub
parent d4717a08d6
commit 2da8aafad0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 584 additions and 412 deletions

View File

@ -182,6 +182,11 @@ public class Constants {
public static final String PARAM_HAS = "_has"; public static final String PARAM_HAS = "_has";
public static final String PARAM_HISTORY = "_history"; public static final String PARAM_HISTORY = "_history";
public static final String PARAM_INCLUDE = "_include"; public static final String PARAM_INCLUDE = "_include";
/**
* @since 7.0.0
*/
public static final String PARAM_LANGUAGE = "_language";
public static final String PARAM_INCLUDE_QUALIFIER_RECURSE = ":recurse"; public static final String PARAM_INCLUDE_QUALIFIER_RECURSE = ":recurse";
public static final String PARAM_INCLUDE_RECURSE = "_include" + PARAM_INCLUDE_QUALIFIER_RECURSE; public static final String PARAM_INCLUDE_RECURSE = "_include" + PARAM_INCLUDE_QUALIFIER_RECURSE;
public static final String PARAM_INCLUDE_QUALIFIER_ITERATE = ":iterate"; public static final String PARAM_INCLUDE_QUALIFIER_ITERATE = ":iterate";

View File

@ -0,0 +1,6 @@
---
type: fix
issue: 5300
title: "A bug in DefaultProfileValidationSupport in R5 mode caused it to return duplicates
in the lists returned by `fetchAllStructureDefinitions()`, `fetchAllSearchParameters()`, etc.
This has been corrected."

View File

@ -0,0 +1,5 @@
---
type: add
issue: 5300
title: "A new configuration option has been added to `StorageSettings` which enables
support in the JPA server for the `_language` SearchParameter."

View File

@ -138,6 +138,13 @@ public class StorageSettings {
* Since 6.4.0 * Since 6.4.0
*/ */
private boolean myQualifySubscriptionMatchingChannelName = true; private boolean myQualifySubscriptionMatchingChannelName = true;
/**
* Should the {@literal _lamguage} SearchParameter be supported
* on this server?
*
* @since 7.0.0
*/
private boolean myLanguageSearchParameterEnabled = false;
/** /**
* If set to true, the server will prevent the creation of Subscriptions which cannot be evaluated IN-MEMORY. This can improve * If set to true, the server will prevent the creation of Subscriptions which cannot be evaluated IN-MEMORY. This can improve
@ -1295,6 +1302,23 @@ public class StorageSettings {
return myQualifySubscriptionMatchingChannelName; return myQualifySubscriptionMatchingChannelName;
} }
/**
* @return Should the {@literal _lamguage} SearchParameter be supported on this server? Defaults to {@literal false}.
* @since 7.0.0
*/
public boolean isLanguageSearchParameterEnabled() {
return myLanguageSearchParameterEnabled;
}
/**
* Should the {@literal _lamguage} SearchParameter be supported on this server? Defaults to {@literal false}.
*
* @since 7.0.0
*/
public void setLanguageSearchParameterEnabled(boolean theLanguageSearchParameterEnabled) {
myLanguageSearchParameterEnabled = theLanguageSearchParameterEnabled;
}
private static void validateTreatBaseUrlsAsLocal(String theUrl) { private static void validateTreatBaseUrlsAsLocal(String theUrl) {
Validate.notBlank(theUrl, "Base URL must not be null or empty"); Validate.notBlank(theUrl, "Base URL must not be null or empty");

View File

@ -165,7 +165,7 @@ public class MatchUrlService {
IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams( IQueryParameterAnd<?> param = JpaParamUtil.parseQueryParams(
myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList); myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList);
paramMap.add(nextParamName, param); paramMap.add(nextParamName, param);
} else if (nextParamName.startsWith("_")) { } else if (nextParamName.startsWith("_") && !Constants.PARAM_LANGUAGE.equals(nextParamName)) {
// ignore these since they aren't search params (e.g. _sort) // ignore these since they aren't search params (e.g. _sort)
} else { } else {
RuntimeSearchParam paramDef = RuntimeSearchParam paramDef =

View File

@ -32,6 +32,8 @@ import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry;
import ca.uhn.fhir.jpa.cache.ResourceChangeResult; import ca.uhn.fhir.jpa.cache.ResourceChangeResult;
import ca.uhn.fhir.jpa.model.entity.StorageSettings; import ca.uhn.fhir.jpa.model.entity.StorageSettings;
import ca.uhn.fhir.jpa.searchparam.SearchParameterMap; import ca.uhn.fhir.jpa.searchparam.SearchParameterMap;
import ca.uhn.fhir.rest.api.Constants;
import ca.uhn.fhir.rest.api.RestSearchParameterTypeEnum;
import ca.uhn.fhir.rest.api.server.IBundleProvider; import ca.uhn.fhir.rest.api.server.IBundleProvider;
import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.exceptions.ResourceNotFoundException;
import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry;
@ -193,6 +195,31 @@ public class SearchParamRegistryImpl
long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams);
ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); ourLog.trace("Have overridden {} built-in search parameters", overriddenCount);
removeInactiveSearchParams(searchParams); removeInactiveSearchParams(searchParams);
/*
* The _language SearchParameter is a weird exception - It is actually just a normal
* token SP, but we explcitly ban SPs from registering themselves with a prefix
* of "_" since that's system reserved so we put this one behind a settings toggle
*/
if (myStorageSettings.isLanguageSearchParameterEnabled()) {
IIdType id = myFhirContext.getVersion().newIdType();
id.setValue("SearchParameter/Resource-language");
RuntimeSearchParam sp = new RuntimeSearchParam(
id,
"http://hl7.org/fhir/SearchParameter/Resource-language",
Constants.PARAM_LANGUAGE,
"Language of the resource content",
"language",
RestSearchParameterTypeEnum.TOKEN,
Collections.emptySet(),
Collections.emptySet(),
RuntimeSearchParam.RuntimeSearchParamStatusEnum.ACTIVE,
myFhirContext.getResourceTypes());
for (String baseResourceType : sp.getBase()) {
searchParams.add(baseResourceType, sp.getName(), sp);
}
}
myActiveSearchParams = searchParams; myActiveSearchParams = searchParams;
myJpaSearchParamCache.populateActiveSearchParams( myJpaSearchParamCache.populateActiveSearchParams(
@ -282,7 +309,13 @@ public class SearchParamRegistryImpl
@Override @Override
public void forceRefresh() { public void forceRefresh() {
RuntimeSearchParamCache activeSearchParams = myActiveSearchParams;
myResourceChangeListenerCache.forceRefresh(); myResourceChangeListenerCache.forceRefresh();
// If the refresh didn't trigger a change, proceed with one anyway
if (myActiveSearchParams == activeSearchParams) {
rebuildActiveSearchParams();
}
} }
@Override @Override

View File

@ -40,279 +40,285 @@ import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder; import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.fail;
@ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class) @ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class)
@SuppressWarnings({"Duplicates"}) @SuppressWarnings({"Duplicates"})
public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test { public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test {
private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class); private static final org.slf4j.Logger ourLog = org.slf4j.LoggerFactory.getLogger(FhirResourceDaoR5SearchNoFtTest.class);
@AfterEach @AfterEach
public void after() { public void after() {
myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields()); JpaStorageSettings defaults = new JpaStorageSettings();
} myStorageSettings.setIndexMissingFields(defaults.getIndexMissingFields());
myStorageSettings.setLanguageSearchParameterEnabled(defaults.isLanguageSearchParameterEnabled());
mySearchParamRegistry.forceRefresh();
}
@Test @Test
public void testHasWithTargetReference() { public void testHasWithTargetReference() {
Organization org = new Organization(); Organization org = new Organization();
org.setId("ORG"); org.setId("ORG");
org.setName("ORG"); org.setName("ORG");
myOrganizationDao.update(org); myOrganizationDao.update(org);
Practitioner practitioner = new Practitioner(); Practitioner practitioner = new Practitioner();
practitioner.setId("PRACT"); practitioner.setId("PRACT");
practitioner.addName().setFamily("PRACT"); practitioner.addName().setFamily("PRACT");
myPractitionerDao.update(practitioner); myPractitionerDao.update(practitioner);
PractitionerRole role = new PractitionerRole(); PractitionerRole role = new PractitionerRole();
role.setId("ROLE"); role.setId("ROLE");
role.getPractitioner().setReference("Practitioner/PRACT"); role.getPractitioner().setReference("Practitioner/PRACT");
role.getOrganization().setReference("Organization/ORG"); role.getOrganization().setReference("Organization/ORG");
myPractitionerRoleDao.update(role); myPractitionerRoleDao.update(role);
SearchParameterMap params = new SearchParameterMap(); SearchParameterMap params = new SearchParameterMap();
HasAndListParam value = new HasAndListParam(); HasAndListParam value = new HasAndListParam();
value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "ORG"))); value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "ORG")));
params.add("_has", value); params.add("_has", value);
IBundleProvider outcome = myPractitionerDao.search(params); IBundleProvider outcome = myPractitionerDao.search(params);
assertEquals(1, outcome.getResources(0, 1).size()); assertEquals(1, outcome.getResources(0, 1).size());
} }
@Test
public void testHasWithTargetReferenceQualified() {
Organization org = new Organization();
org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org);
Practitioner practitioner = new Practitioner(); @Test
practitioner.setId("PRACT"); public void testHasWithTargetReferenceQualified() {
practitioner.addName().setFamily("PRACT"); Organization org = new Organization();
myPractitionerDao.update(practitioner); org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org);
PractitionerRole role = new PractitionerRole(); Practitioner practitioner = new Practitioner();
role.setId("ROLE"); practitioner.setId("PRACT");
role.getPractitioner().setReference("Practitioner/PRACT"); practitioner.addName().setFamily("PRACT");
role.getOrganization().setReference("Organization/ORG"); myPractitionerDao.update(practitioner);
myPractitionerRoleDao.update(role);
SearchParameterMap params = new SearchParameterMap(); PractitionerRole role = new PractitionerRole();
HasAndListParam value = new HasAndListParam(); role.setId("ROLE");
value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "Organization/ORG"))); role.getPractitioner().setReference("Practitioner/PRACT");
params.add("_has", value); role.getOrganization().setReference("Organization/ORG");
IBundleProvider outcome = myPractitionerDao.search(params); myPractitionerRoleDao.update(role);
assertEquals(1, outcome.getResources(0, 1).size());
}
@Test SearchParameterMap params = new SearchParameterMap();
public void testHasWithTargetId() { HasAndListParam value = new HasAndListParam();
Organization org = new Organization(); value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "Organization/ORG")));
org.setId("ORG"); params.add("_has", value);
org.setName("ORG"); IBundleProvider outcome = myPractitionerDao.search(params);
myOrganizationDao.update(org); assertEquals(1, outcome.getResources(0, 1).size());
}
Practitioner practitioner = new Practitioner(); @Test
practitioner.setId("PRACT"); public void testHasWithTargetId() {
practitioner.addName().setFamily("PRACT"); Organization org = new Organization();
myPractitionerDao.update(practitioner); org.setId("ORG");
org.setName("ORG");
myOrganizationDao.update(org);
PractitionerRole role = new PractitionerRole(); Practitioner practitioner = new Practitioner();
role.setId("ROLE"); practitioner.setId("PRACT");
role.getPractitioner().setReference("Practitioner/PRACT"); practitioner.addName().setFamily("PRACT");
role.getOrganization().setReference("Organization/ORG"); myPractitionerDao.update(practitioner);
myPractitionerRoleDao.update(role);
runInTransaction(() -> { PractitionerRole role = new PractitionerRole();
ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); role.setId("ROLE");
}); role.getPractitioner().setReference("Practitioner/PRACT");
role.getOrganization().setReference("Organization/ORG");
myPractitionerRoleDao.update(role);
SearchParameterMap params = SearchParameterMap.newSynchronous(); runInTransaction(() -> {
HasAndListParam value = new HasAndListParam(); ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * ")));
value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "_id", "ROLE"))); });
params.add("_has", value);
myCaptureQueriesListener.clear();
IBundleProvider outcome = myPractitionerDao.search(params);
myCaptureQueriesListener.logSelectQueriesForCurrentThread(1);
assertEquals(1, outcome.getResources(0, 1).size());
}
@Test SearchParameterMap params = SearchParameterMap.newSynchronous();
public void testSearchDoesntFailIfResourcesAreDeleted() { HasAndListParam value = new HasAndListParam();
value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "_id", "ROLE")));
params.add("_has", value);
myCaptureQueriesListener.clear();
IBundleProvider outcome = myPractitionerDao.search(params);
myCaptureQueriesListener.logSelectQueriesForCurrentThread(1);
assertEquals(1, outcome.getResources(0, 1).size());
}
Patient p = new Patient(); @Test
p.addIdentifier().setValue("1"); public void testSearchDoesntFailIfResourcesAreDeleted() {
myPatientDao.create(p);
p = new Patient(); Patient p = new Patient();
p.addIdentifier().setValue("2"); p.addIdentifier().setValue("1");
myPatientDao.create(p); myPatientDao.create(p);
p = new Patient(); p = new Patient();
p.addIdentifier().setValue("3"); p.addIdentifier().setValue("2");
Long id = myPatientDao.create(p).getId().getIdPartAsLong(); myPatientDao.create(p);
IBundleProvider outcome = myPatientDao.search(new SearchParameterMap()); p = new Patient();
assertEquals(3, outcome.size().intValue()); p.addIdentifier().setValue("3");
Long id = myPatientDao.create(p).getId().getIdPartAsLong();
runInTransaction(() -> { IBundleProvider outcome = myPatientDao.search(new SearchParameterMap());
ResourceTable table = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException()); assertEquals(3, outcome.size().intValue());
table.setDeleted(new Date());
myResourceTableDao.save(table);
});
assertEquals(2, outcome.getResources(0, 3).size()); runInTransaction(() -> {
ResourceTable table = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException());
table.setDeleted(new Date());
myResourceTableDao.save(table);
});
runInTransaction(() -> { assertEquals(2, outcome.getResources(0, 3).size());
myResourceHistoryTableDao.deleteAll();
});
assertEquals(0, outcome.getResources(0, 3).size()); runInTransaction(() -> {
} myResourceHistoryTableDao.deleteAll();
});
@Test assertEquals(0, outcome.getResources(0, 3).size());
public void testToken_CodeableReference_Reference() { }
// Setup
ObservationDefinition obs = new ObservationDefinition(); @Test
obs.setApprovalDate(new Date()); public void testToken_CodeableReference_Reference() {
String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); // Setup
ClinicalUseDefinition def = new ClinicalUseDefinition(); ObservationDefinition obs = new ObservationDefinition();
def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); obs.setApprovalDate(new Date());
String id = myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue(); String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue();
ClinicalUseDefinition def2 = new ClinicalUseDefinition(); ClinicalUseDefinition def = new ClinicalUseDefinition();
def2.getContraindication().getDiseaseSymptomProcedure().setConcept(new CodeableConcept().addCoding(new Coding("http://foo", "bar", "baz"))); def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId));
myClinicalUseDefinitionDao.create(def2, mySrd).getId().toUnqualifiedVersionless().getValue(); String id = myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue();
// Test ClinicalUseDefinition def2 = new ClinicalUseDefinition();
def2.getContraindication().getDiseaseSymptomProcedure().setConcept(new CodeableConcept().addCoding(new Coding("http://foo", "bar", "baz")));
myClinicalUseDefinitionDao.create(def2, mySrd).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION_REFERENCE, new ReferenceParam(obsId)); // Test
List<String> outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd));
assertThat(outcome, Matchers.contains(id));
} SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION_REFERENCE, new ReferenceParam(obsId));
List<String> outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd));
assertThat(outcome, Matchers.contains(id));
@Test }
public void testToken_CodeableReference_Coding() {
// Setup
ObservationDefinition obs = new ObservationDefinition(); @Test
obs.setApprovalDate(new Date()); public void testToken_CodeableReference_Coding() {
String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); // Setup
ClinicalUseDefinition def = new ClinicalUseDefinition(); ObservationDefinition obs = new ObservationDefinition();
def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); obs.setApprovalDate(new Date());
myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue(); String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue();
ClinicalUseDefinition def2 = new ClinicalUseDefinition(); ClinicalUseDefinition def = new ClinicalUseDefinition();
def2.getContraindication().getDiseaseSymptomProcedure().setConcept(new CodeableConcept().addCoding(new Coding("http://foo", "bar", "baz"))); def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId));
String id =myClinicalUseDefinitionDao.create(def2, mySrd).getId().toUnqualifiedVersionless().getValue(); myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue();
// Test ClinicalUseDefinition def2 = new ClinicalUseDefinition();
def2.getContraindication().getDiseaseSymptomProcedure().setConcept(new CodeableConcept().addCoding(new Coding("http://foo", "bar", "baz")));
String id = myClinicalUseDefinitionDao.create(def2, mySrd).getId().toUnqualifiedVersionless().getValue();
SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION, new TokenParam("http://foo", "bar")); // Test
List<String> outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd));
assertThat(outcome, Matchers.contains(id));
} SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION, new TokenParam("http://foo", "bar"));
List<String> outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd));
assertThat(outcome, Matchers.contains(id));
}
@Test @Test
public void testIndexAddressDistrict() { public void testIndexAddressDistrict() {
// Setup // Setup
Patient p = new Patient(); Patient p = new Patient();
p.addAddress() p.addAddress()
.setDistrict("DISTRICT123"); .setDistrict("DISTRICT123");
String id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless().getValue(); String id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless().getValue();
logAllStringIndexes(); logAllStringIndexes();
// Test // Test
SearchParameterMap params = SearchParameterMap SearchParameterMap params = SearchParameterMap
.newSynchronous(Patient.SP_ADDRESS, new StringParam("DISTRICT123")); .newSynchronous(Patient.SP_ADDRESS, new StringParam("DISTRICT123"));
IBundleProvider outcome = myPatientDao.search(params, mySrd); IBundleProvider outcome = myPatientDao.search(params, mySrd);
// Verify // Verify
assertThat(toUnqualifiedVersionlessIdValues(outcome), Matchers.contains(id)); assertThat(toUnqualifiedVersionlessIdValues(outcome), Matchers.contains(id));
} }
/** /**
* Index for * Index for
* [base]/Bundle?composition.patient.identifier=foo * [base]/Bundle?composition.patient.identifier=foo
*/ */
@ParameterizedTest @ParameterizedTest
@CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) @CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"})
public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) {
// Setup 1 // Setup 1
myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED);
SearchParameter sp = new SearchParameter(); SearchParameter sp = new SearchParameter();
sp.setId("SearchParameter/Bundle-composition-patient-identifier"); sp.setId("SearchParameter/Bundle-composition-patient-identifier");
sp.setCode("composition.patient.identifier"); sp.setCode("composition.patient.identifier");
sp.setName("composition.patient.identifier"); sp.setName("composition.patient.identifier");
sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier");
sp.setStatus(Enumerations.PublicationStatus.ACTIVE); sp.setStatus(Enumerations.PublicationStatus.ACTIVE);
sp.setType(Enumerations.SearchParamType.TOKEN); sp.setType(Enumerations.SearchParamType.TOKEN);
sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier");
sp.addBase(Enumerations.VersionIndependentResourceTypesAll.BUNDLE); sp.addBase(Enumerations.VersionIndependentResourceTypesAll.BUNDLE);
ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp));
mySearchParameterDao.update(sp, mySrd); mySearchParameterDao.update(sp, mySrd);
mySearchParamRegistry.forceRefresh(); mySearchParamRegistry.forceRefresh();
// Test 1 // Test 1
Composition composition = new Composition(); Composition composition = new Composition();
composition.addSubject().setReference(thePatientId); composition.addSubject().setReference(thePatientId);
Patient patient = new Patient(); Patient patient = new Patient();
patient.setId(new IdType(thePatientId)); patient.setId(new IdType(thePatientId));
patient.addIdentifier().setSystem("http://foo").setValue("bar"); patient.addIdentifier().setSystem("http://foo").setValue("bar");
Bundle bundle = new Bundle(); Bundle bundle = new Bundle();
bundle.setType(Bundle.BundleType.DOCUMENT); bundle.setType(Bundle.BundleType.DOCUMENT);
bundle.addEntry().setResource(composition); bundle.addEntry().setResource(composition);
bundle.addEntry().setResource(patient); bundle.addEntry().setResource(patient);
myBundleDao.create(bundle, mySrd); myBundleDao.create(bundle, mySrd);
Bundle bundle2 = new Bundle(); Bundle bundle2 = new Bundle();
bundle2.setType(Bundle.BundleType.DOCUMENT); bundle2.setType(Bundle.BundleType.DOCUMENT);
myBundleDao.create(bundle2, mySrd); myBundleDao.create(bundle2, mySrd);
// Verify 1 // Verify 1
runInTransaction(() -> { runInTransaction(() -> {
logAllTokenIndexes(); logAllTokenIndexes();
List<String> params = myResourceIndexedSearchParamTokenDao List<String> params = myResourceIndexedSearchParamTokenDao
.findAll() .findAll()
.stream() .stream()
.filter(t -> t.getParamName().contains(".")) .filter(t -> t.getParamName().contains("."))
.map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue())
.toList(); .toList();
assertThat(params.toString(), params, containsInAnyOrder( assertThat(params.toString(), params, containsInAnyOrder(
"composition.patient.identifier http://foo|bar" "composition.patient.identifier http://foo|bar"
)); ));
}); });
// Test 2 // Test 2
IBundleProvider outcome; IBundleProvider outcome;
SearchParameterMap map = SearchParameterMap SearchParameterMap map = SearchParameterMap
.newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar"));
outcome = myBundleDao.search(map, mySrd); outcome = myBundleDao.search(map, mySrd);
assertEquals(1, outcome.size()); assertEquals(1, outcome.size());
map = SearchParameterMap map = SearchParameterMap
.newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar"));
outcome = myBundleDao.search(map, mySrd); outcome = myBundleDao.search(map, mySrd);
assertEquals(1, outcome.size()); assertEquals(1, outcome.size());
} }
@Test @Test
public void testHasWithNonExistentReferenceField() { public void testHasWithNonExistentReferenceField() {
@ -334,5 +340,33 @@ public class FhirResourceDaoR5SearchNoFtTest extends BaseJpaR5Test {
} }
} }
@Test
public void testLanguageSearchParameter_DefaultDisabled() {
createObservation(withId("A"), withLanguage("en"));
createObservation(withId("B"), withLanguage("fr"));
logAllTokenIndexes();
runInTransaction(() -> assertEquals(0, myResourceIndexedSearchParamTokenDao.count()));
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(Constants.PARAM_LANGUAGE, new TokenParam("en"));
assertThrows(InvalidRequestException.class, () -> myObservationDao.search(params, mySrd));
}
@Test
public void testLanguageSearchParameter_Enabled() {
myStorageSettings.setLanguageSearchParameterEnabled(true);
mySearchParamRegistry.forceRefresh();
createObservation(withId("A"), withLanguage("en"));
createObservation(withId("B"), withLanguage("fr"));
logAllTokenIndexes();
runInTransaction(() -> assertEquals(2, myResourceIndexedSearchParamTokenDao.count()));
SearchParameterMap params = SearchParameterMap.newSynchronous();
params.add(Constants.PARAM_LANGUAGE, new TokenParam("en"));
assertThat(toUnqualifiedVersionlessIdValues(myObservationDao.search(params, mySrd)), contains("Observation/A"));
}
} }

View File

@ -46,189 +46,186 @@ import static org.mockito.Mockito.lenient;
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
public class SearchParameterDaoValidatorTest { public class SearchParameterDaoValidatorTest {
@Spy private static final String SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN = "SearchParameter/observation-code";
private FhirContext myFhirContext = FhirContext.forR5Cached(); private static final String SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE = "SearchParameter/observation-patient";
@Mock private static final String SP_COMPONENT_DEFINITION_OF_TYPE_STRING = "SearchParameter/observation-markdown";
private ISearchParamRegistry mySearchParamRegistry; private static final String SP_COMPONENT_DEFINITION_OF_TYPE_DATE = "SearchParameter/observation-date";
@Spy private static final String SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY = "SearchParameter/observation-code";
private JpaStorageSettings myStorageSettings = new JpaStorageSettings(); private static final String SP_COMPONENT_DEFINITION_OF_TYPE_URI = "SearchParameter/component-value-canonical";
@InjectMocks private static final String SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER = "SearchParameter/component-value-number";
private SearchParameterDaoValidator mySvc; @Spy
private FhirContext myFhirContext = FhirContext.forR5Cached();
private final VersionCanonicalizer myVersionCanonicalizer = new VersionCanonicalizer(myFhirContext);
private final SearchParameterCanonicalizer mySearchParameterCanonicalizer = new SearchParameterCanonicalizer(myFhirContext);
@Mock
private ISearchParamRegistry mySearchParamRegistry;
@Spy
private JpaStorageSettings myStorageSettings = new JpaStorageSettings();
@InjectMocks
private SearchParameterDaoValidator mySvc;
private final VersionCanonicalizer myVersionCanonicalizer = new VersionCanonicalizer(myFhirContext); @BeforeEach
public void before() {
createAndMockSearchParameter(TOKEN, SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN, "observation-code", "Observation.code");
createAndMockSearchParameter(REFERENCE, SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE, "observation-patient", "Observation.subject.where(resolve() is Patient");
createAndMockSearchParameter(STRING, SP_COMPONENT_DEFINITION_OF_TYPE_DATE, "observation-category", "Observation.value.ofType(markdown)");
createAndMockSearchParameter(DATE, SP_COMPONENT_DEFINITION_OF_TYPE_STRING, "observation-date", "Observation.value.ofType(dateTime)");
createAndMockSearchParameter(QUANTITY, SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY, "observation-quantity", "Observation.value.ofType(Quantity)");
createAndMockSearchParameter(URI, SP_COMPONENT_DEFINITION_OF_TYPE_URI, "observation-component-value-canonical", "Observation.component.value.ofType(canonical)");
createAndMockSearchParameter(NUMBER, SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER, "observation-component-value-number", "Observation.component.valueInteger");
}
private final SearchParameterCanonicalizer mySearchParameterCanonicalizer = new SearchParameterCanonicalizer(myFhirContext); private void createAndMockSearchParameter(Enumerations.SearchParamType theType, String theDefinition, String theCodeValue, String theExpression) {
SearchParameter observationCodeSp = createSearchParameter(theType, theDefinition, theCodeValue, theExpression);
RuntimeSearchParam observationCodeRuntimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(observationCodeSp);
lenient().when(mySearchParamRegistry.getActiveSearchParamByUrl(eq(theDefinition))).thenReturn(observationCodeRuntimeSearchParam);
}
@Test
public void testValidateSubscription() {
SearchParameter sp = new SearchParameter();
sp.setId("SearchParameter/patient-eyecolour");
sp.setUrl("http://example.org/SearchParameter/patient-eyecolour");
sp.addBase(PATIENT);
sp.setCode("eyecolour");
sp.setType(TOKEN);
sp.setStatus(ACTIVE);
sp.setExpression("Patient.extension('http://foo')");
sp.addTarget(PATIENT);
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN = "SearchParameter/observation-code"; SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp);
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE = "SearchParameter/observation-patient"; mySvc.validate(canonicalSp);
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_STRING = "SearchParameter/observation-markdown"; }
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_DATE = "SearchParameter/observation-date";
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY = "SearchParameter/observation-code";
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_URI = "SearchParameter/component-value-canonical";
private static final String SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER = "SearchParameter/component-value-number";
@BeforeEach @Test
public void before() { public void testValidateSubscriptionWithCustomType() {
createAndMockSearchParameter(TOKEN, SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN, "observation-code", "Observation.code"); SearchParameter sp = new SearchParameter();
createAndMockSearchParameter(REFERENCE, SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE, "observation-patient", "Observation.subject.where(resolve() is Patient"); sp.setId("SearchParameter/meal-chef");
createAndMockSearchParameter(STRING, SP_COMPONENT_DEFINITION_OF_TYPE_DATE, "observation-category", "Observation.value.ofType(markdown)"); sp.setUrl("http://example.org/SearchParameter/meal-chef");
createAndMockSearchParameter(DATE, SP_COMPONENT_DEFINITION_OF_TYPE_STRING, "observation-date", "Observation.value.ofType(dateTime)"); sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE).setValue(new StringType("Meal")));
createAndMockSearchParameter(QUANTITY, SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY, "observation-quantity", "Observation.value.ofType(Quantity)"); sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE).setValue(new StringType("Chef")));
createAndMockSearchParameter(URI, SP_COMPONENT_DEFINITION_OF_TYPE_URI, "observation-component-value-canonical", "Observation.component.value.ofType(canonical)"); sp.setCode("chef");
createAndMockSearchParameter(NUMBER, SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER, "observation-component-value-number", "Observation.component.valueInteger"); sp.setType(REFERENCE);
} sp.setStatus(ACTIVE);
sp.setExpression("Meal.chef");
private void createAndMockSearchParameter(Enumerations.SearchParamType theType, String theDefinition, String theCodeValue, String theExpression) { SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp);
SearchParameter observationCodeSp = createSearchParameter(theType, theDefinition, theCodeValue, theExpression); mySvc.validate(canonicalSp);
RuntimeSearchParam observationCodeRuntimeSearchParam = mySearchParameterCanonicalizer.canonicalizeSearchParameter(observationCodeSp); }
lenient().when(mySearchParamRegistry.getActiveSearchParamByUrl(eq(theDefinition))).thenReturn(observationCodeRuntimeSearchParam);
}
@Test @ParameterizedTest
public void testValidateSubscription() { @MethodSource("extensionProvider")
SearchParameter sp = new SearchParameter(); public void testMethodValidate_nonUniqueComboAndCompositeSearchParamWithComponentOfTypeReference_isNotAllowed(Extension theExtension) {
sp.setId("SearchParameter/patient-eyecolour"); SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation");
sp.setUrl("http://example.org/SearchParameter/patient-eyecolour"); sp.addExtension(theExtension);
sp.addBase(PATIENT);
sp.setCode("eyecolour");
sp.setType(TOKEN);
sp.setStatus(ACTIVE);
sp.setExpression("Patient.extension('http://foo')");
sp.addTarget(PATIENT);
SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN));
mySvc.validate(canonicalSp); sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE));
}
@Test try {
public void testValidateSubscriptionWithCustomType() { mySvc.validate(sp);
SearchParameter sp = new SearchParameter(); fail();
sp.setId("SearchParameter/meal-chef"); } catch (UnprocessableEntityException ex) {
sp.setUrl("http://example.org/SearchParameter/meal-chef"); assertTrue(ex.getMessage().startsWith("HAPI-2347: "));
sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE).setValue(new StringType("Meal"))); assertTrue(ex.getMessage().contains("Invalid component search parameter type: REFERENCE in component.definition: http://example.org/SearchParameter/observation-patient"));
sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_TARGET_RESOURCE).setValue(new StringType("Chef"))); }
sp.setCode("chef"); }
sp.setType(REFERENCE);
sp.setStatus(ACTIVE);
sp.setExpression("Meal.chef");
SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); @Test
mySvc.validate(canonicalSp); public void testMethodValidate_uniqueComboSearchParamWithComponentOfTypeReference_isValid() {
} SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation");
sp.addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(true)));
@ParameterizedTest sp.addComponent(new SearchParameterComponentComponent()
@MethodSource("extensionProvider") .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN));
public void testMethodValidate_nonUniqueComboAndCompositeSearchParamWithComponentOfTypeReference_isNotAllowed(Extension theExtension) { sp.addComponent(new SearchParameterComponentComponent()
SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation"); .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE));
sp.addExtension(theExtension);
sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); mySvc.validate(sp);
sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE)); }
try { @ParameterizedTest
mySvc.validate(sp); @MethodSource("comboSpProvider")
fail(); public void testMethodValidate_comboSearchParamsWithNumberUriComponents_isValid(SearchParameter theSearchParameter) {
} catch (UnprocessableEntityException ex) { theSearchParameter.addComponent(new SearchParameterComponentComponent()
assertTrue(ex.getMessage().startsWith("HAPI-2347: ")); .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI));
assertTrue(ex.getMessage().contains("Invalid component search parameter type: REFERENCE in component.definition: http://example.org/SearchParameter/observation-patient")); theSearchParameter.addComponent(new SearchParameterComponentComponent()
} .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER));
}
@Test mySvc.validate(theSearchParameter);
public void testMethodValidate_uniqueComboSearchParamWithComponentOfTypeReference_isValid() { }
SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation");
sp.addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(true)));
sp.addComponent(new SearchParameterComponentComponent() @Test
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); public void testMethodValidate_compositeSearchParamsWithNumberUriComponents_isNotAllowed() {
sp.addComponent(new SearchParameterComponentComponent() SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/component-value-uri-number", "component-value-uri-number", "Observation");
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE));
mySvc.validate(sp); sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI));
} sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER));
@ParameterizedTest try {
@MethodSource("comboSpProvider") mySvc.validate(sp);
public void testMethodValidate_comboSearchParamsWithNumberUriComponents_isValid(SearchParameter theSearchParameter) { fail();
theSearchParameter.addComponent(new SearchParameterComponentComponent() } catch (UnprocessableEntityException ex) {
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI)); assertTrue(ex.getMessage().startsWith("HAPI-2347: "));
theSearchParameter.addComponent(new SearchParameterComponentComponent() assertTrue(ex.getMessage().contains("Invalid component search parameter type: URI in component.definition: http://example.org/SearchParameter/component-value-canonical"));
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER)); }
}
mySvc.validate(theSearchParameter); @ParameterizedTest
} @MethodSource("compositeSpProvider")
// we're testing for:
// SP of type composite,
// SP of type combo composite non-unique,
// SP of type combo composite unique,
public void testMethodValidate_allCompositeSpTypesWithComponentOfValidType_isValid(SearchParameter theSearchParameter) {
@Test theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
public void testMethodValidate_compositeSearchParamsWithNumberUriComponents_isNotAllowed() { .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN).setExpression("Observation"));
SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/component-value-uri-number", "component-value-uri-number", "Observation"); theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY).setExpression("Observation"));
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_STRING).setExpression("Observation"));
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_DATE).setExpression("Observation"));
sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI)); mySvc.validate(theSearchParameter);
sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER)); }
try { private static SearchParameter createSearchParameter(Enumerations.SearchParamType theType, String theId, String theCodeValue, String theExpression) {
mySvc.validate(sp);
fail();
} catch (UnprocessableEntityException ex) {
assertTrue(ex.getMessage().startsWith("HAPI-2347: "));
assertTrue(ex.getMessage().contains("Invalid component search parameter type: URI in component.definition: http://example.org/SearchParameter/component-value-canonical"));
}
}
@ParameterizedTest SearchParameter retVal = new SearchParameter();
@MethodSource("compositeSpProvider") retVal.setId(theId);
// we're testing for: retVal.setUrl("http://example.org/" + theId);
// SP of type composite, retVal.addBase(OBSERVATION);
// SP of type combo composite non-unique, retVal.setCode(theCodeValue);
// SP of type combo composite unique, retVal.setType(theType);
public void testMethodValidate_allCompositeSpTypesWithComponentOfValidType_isValid(SearchParameter theSearchParameter) { retVal.setStatus(ACTIVE);
retVal.setExpression(theExpression);
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent() return retVal;
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN).setExpression("Observation")); }
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_QUANTITY).setExpression("Observation"));
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_STRING).setExpression("Observation"));
theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent()
.setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_DATE).setExpression("Observation"));
mySvc.validate(theSearchParameter); static Stream<Arguments> extensionProvider() {
} return Stream.of(
Arguments.of(
new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(false))), // composite SP of type combo with non-unique index
Arguments.of((Object) null) // composite SP
);
}
private static SearchParameter createSearchParameter(Enumerations.SearchParamType theType, String theId, String theCodeValue, String theExpression) { static Stream<Arguments> comboSpProvider() {
return Stream.of(
Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")
.addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(false)))), // composite SP of type combo with non-unique index
SearchParameter retVal = new SearchParameter(); Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")
retVal.setId(theId); .addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(true)))) // composite SP of type combo with unique index
retVal.setUrl("http://example.org/" + theId); );
retVal.addBase(OBSERVATION); }
retVal.setCode(theCodeValue);
retVal.setType(theType);
retVal.setStatus(ACTIVE);
retVal.setExpression(theExpression);
return retVal; static Stream<Arguments> compositeSpProvider() {
} return Stream.concat(comboSpProvider(), Stream.of(
Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")) // composite SP
static Stream<Arguments> extensionProvider() { ));
return Stream.of( }
Arguments.of(
new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(false))), // composite SP of type combo with non-unique index
Arguments.of((Object) null) // composite SP
);
}
static Stream<Arguments> comboSpProvider() {
return Stream.of(
Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")
.addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(false)))), // composite SP of type combo with non-unique index
Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")
.addExtension(new Extension(HapiExtensions.EXT_SP_UNIQUE, new BooleanType(true)))) // composite SP of type combo with unique index
);
}
static Stream<Arguments> compositeSpProvider() {
return Stream.concat(comboSpProvider(), Stream.of(
Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")) // composite SP
));
}
} }

View File

@ -6,5 +6,6 @@ public class CommonJpaStorageSettingsConfigurer {
public CommonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) { public CommonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) {
theStorageSettings.setIndexOnUpliftedRefchains(true); theStorageSettings.setIndexOnUpliftedRefchains(true);
theStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false); theStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false);
theStorageSettings.setLanguageSearchParameterEnabled(true);
} }
} }

View File

@ -98,10 +98,12 @@ public class SearchParameterDaoValidator {
return; return;
} }
// Search parameters must have a base
if (isCompositeWithoutBase(searchParameter)) { if (isCompositeWithoutBase(searchParameter)) {
throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing"); throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing");
} }
// Do we have a valid expression
if (isCompositeWithoutExpression(searchParameter)) { if (isCompositeWithoutExpression(searchParameter)) {
// this is ok // this is ok

View File

@ -80,6 +80,13 @@ public interface ITestDataBuilder {
return t -> __setPrimitiveChild(getFhirContext(), t, "active", "boolean", "false"); return t -> __setPrimitiveChild(getFhirContext(), t, "active", "boolean", "false");
} }
/**
* Set Resource.language
*/
default ICreationArgument withLanguage(String theLanguage) {
return t -> __setPrimitiveChild(getFhirContext(), t, "language", "string", theLanguage);
}
/** /**
* Set Patient.gender * Set Patient.gender
*/ */

View File

@ -16,12 +16,14 @@ import org.hl7.fhir.r4.model.ValueSet;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.Collections;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors;
import javax.annotation.Nonnull; import javax.annotation.Nonnull;
import javax.annotation.Nullable; import javax.annotation.Nullable;
@ -34,10 +36,14 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank;
public class PrePopulatedValidationSupport extends BaseStaticResourceValidationSupport public class PrePopulatedValidationSupport extends BaseStaticResourceValidationSupport
implements IValidationSupport, ILockable { implements IValidationSupport, ILockable {
private final Map<String, IBaseResource> myCodeSystems; private final Map<String, IBaseResource> myUrlToCodeSystems;
private final Map<String, IBaseResource> myStructureDefinitions; private final Map<String, IBaseResource> myUrlToStructureDefinitions;
private final Map<String, IBaseResource> mySearchParameters; private final Map<String, IBaseResource> myUrlToSearchParameters;
private final Map<String, IBaseResource> myValueSets; private final Map<String, IBaseResource> myUrlToValueSets;
private final List<IBaseResource> myCodeSystems;
private final List<IBaseResource> myStructureDefinitions;
private final List<IBaseResource> mySearchParameters;
private final List<IBaseResource> myValueSets;
private final Map<String, byte[]> myBinaries; private final Map<String, byte[]> myBinaries;
private boolean myLocked; private boolean myLocked;
@ -51,51 +57,67 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
/** /**
* Constructor * Constructor
* *
* @param theStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
* values are the resource itself. * values are the resource itself.
* @param theValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are * @param theUrlToValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself. * the resource itself.
* @param theCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are * @param theUrlToCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself. * the resource itself.
**/ **/
public PrePopulatedValidationSupport( public PrePopulatedValidationSupport(
FhirContext theFhirContext, FhirContext theFhirContext,
Map<String, IBaseResource> theStructureDefinitions, Map<String, IBaseResource> theUrlToStructureDefinitions,
Map<String, IBaseResource> theValueSets, Map<String, IBaseResource> theUrlToValueSets,
Map<String, IBaseResource> theCodeSystems) { Map<String, IBaseResource> theUrlToCodeSystems) {
this(theFhirContext, theStructureDefinitions, theValueSets, theCodeSystems, new HashMap<>(), new HashMap<>()); this(
theFhirContext,
theUrlToStructureDefinitions,
theUrlToValueSets,
theUrlToCodeSystems,
new HashMap<>(),
new HashMap<>());
} }
/** /**
* Constructor * Constructor
* *
* @param theStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and * @param theUrlToStructureDefinitions The StructureDefinitions to be returned by this module. Keys are the logical URL for the resource, and
* values are the resource itself. * values are the resource itself.
* @param theValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are * @param theUrlToValueSets The ValueSets to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself. * the resource itself.
* @param theCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are * @param theUrlToCodeSystems The CodeSystems to be returned by this module. Keys are the logical URL for the resource, and values are
* the resource itself. * the resource itself.
* @param theBinaries The binary files to be returned by this module. Keys are the unique filename for the binary, and values * @param theBinaries The binary files to be returned by this module. Keys are the unique filename for the binary, and values
* are the contents of the file as a byte array. * are the contents of the file as a byte array.
*/ */
public PrePopulatedValidationSupport( public PrePopulatedValidationSupport(
FhirContext theFhirContext, FhirContext theFhirContext,
Map<String, IBaseResource> theStructureDefinitions, Map<String, IBaseResource> theUrlToStructureDefinitions,
Map<String, IBaseResource> theValueSets, Map<String, IBaseResource> theUrlToValueSets,
Map<String, IBaseResource> theCodeSystems, Map<String, IBaseResource> theUrlToCodeSystems,
Map<String, IBaseResource> theSearchParameters, Map<String, IBaseResource> theUrlToSearchParameters,
Map<String, byte[]> theBinaries) { Map<String, byte[]> theBinaries) {
super(theFhirContext); super(theFhirContext);
Validate.notNull(theFhirContext, "theFhirContext must not be null"); Validate.notNull(theFhirContext, "theFhirContext must not be null");
Validate.notNull(theStructureDefinitions, "theStructureDefinitions must not be null"); Validate.notNull(theUrlToStructureDefinitions, "theStructureDefinitions must not be null");
Validate.notNull(theValueSets, "theValueSets must not be null"); Validate.notNull(theUrlToValueSets, "theValueSets must not be null");
Validate.notNull(theCodeSystems, "theCodeSystems must not be null"); Validate.notNull(theUrlToCodeSystems, "theCodeSystems must not be null");
Validate.notNull(theSearchParameters, "theSearchParameters must not be null"); Validate.notNull(theUrlToSearchParameters, "theSearchParameters must not be null");
Validate.notNull(theBinaries, "theBinaries must not be null"); Validate.notNull(theBinaries, "theBinaries must not be null");
myStructureDefinitions = theStructureDefinitions; myUrlToStructureDefinitions = theUrlToStructureDefinitions;
myValueSets = theValueSets; myStructureDefinitions =
myCodeSystems = theCodeSystems; theUrlToStructureDefinitions.values().stream().distinct().collect(Collectors.toList());
mySearchParameters = theSearchParameters;
myUrlToValueSets = theUrlToValueSets;
myValueSets = theUrlToValueSets.values().stream().distinct().collect(Collectors.toList());
myUrlToCodeSystems = theUrlToCodeSystems;
myCodeSystems = theUrlToCodeSystems.values().stream().distinct().collect(Collectors.toList());
myUrlToSearchParameters = theUrlToSearchParameters;
mySearchParameters =
theUrlToSearchParameters.values().stream().distinct().collect(Collectors.toList());
myBinaries = theBinaries; myBinaries = theBinaries;
} }
@ -127,7 +149,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
public void addCodeSystem(IBaseResource theCodeSystem) { public void addCodeSystem(IBaseResource theCodeSystem) {
validateNotLocked(); validateNotLocked();
Set<String> urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem"); Set<String> urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem");
addToMap(theCodeSystem, myCodeSystems, urls); addToMap(theCodeSystem, myCodeSystems, myUrlToCodeSystems, urls);
} }
private Set<String> processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) { private Set<String> processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) {
@ -185,16 +207,18 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
public void addStructureDefinition(IBaseResource theStructureDefinition) { public void addStructureDefinition(IBaseResource theStructureDefinition) {
validateNotLocked(); validateNotLocked();
Set<String> url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition"); Set<String> url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition");
addToMap(theStructureDefinition, myStructureDefinitions, url); addToMap(theStructureDefinition, myStructureDefinitions, myUrlToStructureDefinitions, url);
} }
public void addSearchParameter(IBaseResource theSearchParameter) { public void addSearchParameter(IBaseResource theSearchParameter) {
validateNotLocked(); validateNotLocked();
Set<String> url = processResourceAndReturnUrls(theSearchParameter, "SearchParameter"); Set<String> url = processResourceAndReturnUrls(theSearchParameter, "SearchParameter");
addToMap(theSearchParameter, mySearchParameters, url); addToMap(theSearchParameter, mySearchParameters, myUrlToSearchParameters, url);
} }
private <T extends IBaseResource> void addToMap(T theResource, Map<String, T> theMap, Collection<String> theUrls) { private <T extends IBaseResource> void addToMap(
T theResource, List<T> theList, Map<String, T> theMap, Collection<String> theUrls) {
theList.add(theResource);
for (String urls : theUrls) { for (String urls : theUrls) {
if (isNotBlank(urls)) { if (isNotBlank(urls)) {
theMap.put(urls, theResource); theMap.put(urls, theResource);
@ -228,7 +252,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
public void addValueSet(IBaseResource theValueSet) { public void addValueSet(IBaseResource theValueSet) {
validateNotLocked(); validateNotLocked();
Set<String> urls = processResourceAndReturnUrls(theValueSet, "ValueSet"); Set<String> urls = processResourceAndReturnUrls(theValueSet, "ValueSet");
addToMap(theValueSet, myValueSets, urls); addToMap(theValueSet, myValueSets, myUrlToValueSets, urls);
} }
/** /**
@ -259,36 +283,38 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
@Override @Override
public List<IBaseResource> fetchAllConformanceResources() { public List<IBaseResource> fetchAllConformanceResources() {
ArrayList<IBaseResource> retVal = new ArrayList<>(); ArrayList<IBaseResource> retVal = new ArrayList<>();
retVal.addAll(myCodeSystems.values()); retVal.addAll(myCodeSystems);
retVal.addAll(myStructureDefinitions.values()); retVal.addAll(myStructureDefinitions);
retVal.addAll(myValueSets.values()); retVal.addAll(myValueSets);
return retVal; return retVal;
} }
@SuppressWarnings("unchecked")
@Nullable @Nullable
@Override @Override
public <T extends IBaseResource> List<T> fetchAllSearchParameters() { public <T extends IBaseResource> List<T> fetchAllSearchParameters() {
return toList(mySearchParameters); return (List<T>) Collections.unmodifiableList(mySearchParameters);
} }
@SuppressWarnings("unchecked")
@Override @Override
public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() { public <T extends IBaseResource> List<T> fetchAllStructureDefinitions() {
return toList(myStructureDefinitions); return (List<T>) Collections.unmodifiableList(myStructureDefinitions);
} }
@Override @Override
public IBaseResource fetchCodeSystem(String theSystem) { public IBaseResource fetchCodeSystem(String theSystem) {
return myCodeSystems.get(theSystem); return myUrlToCodeSystems.get(theSystem);
} }
@Override @Override
public IBaseResource fetchValueSet(String theUri) { public IBaseResource fetchValueSet(String theUri) {
return myValueSets.get(theUri); return myUrlToValueSets.get(theUri);
} }
@Override @Override
public IBaseResource fetchStructureDefinition(String theUrl) { public IBaseResource fetchStructureDefinition(String theUrl) {
return myStructureDefinitions.get(theUrl); return myUrlToStructureDefinitions.get(theUrl);
} }
@Override @Override
@ -298,19 +324,23 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS
@Override @Override
public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) {
return myCodeSystems.containsKey(theSystem); return myUrlToCodeSystems.containsKey(theSystem);
} }
@Override @Override
public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) {
return myValueSets.containsKey(theValueSetUrl); return myUrlToValueSets.containsKey(theValueSetUrl);
} }
/** /**
* Returns a count of all known resources * Returns a count of all known resources
*/ */
public int countAll() { public int countAll() {
return myBinaries.size() + myCodeSystems.size() + myStructureDefinitions.size() + myValueSets.size(); return myBinaries.size()
+ myCodeSystems.size()
+ myStructureDefinitions.size()
+ myValueSets.size()
+ myStructureDefinitions.size();
} }
@Override @Override

View File

@ -2,7 +2,6 @@ package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.context.FhirContext; import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport; import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.CodeSystem; import org.hl7.fhir.r4.model.CodeSystem;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@ -10,7 +9,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertNull;
public class DefaultProfileValidationSupportTest { public class DefaultProfileValidationSupportR4Test {
private static FhirContext ourCtx = FhirContext.forR4Cached(); private static FhirContext ourCtx = FhirContext.forR4Cached();
private DefaultProfileValidationSupport mySvc = new DefaultProfileValidationSupport(ourCtx); private DefaultProfileValidationSupport mySvc = new DefaultProfileValidationSupport(ourCtx);

View File

@ -0,0 +1,29 @@
package org.hl7.fhir.dstu3.hapi.validation;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.context.support.DefaultProfileValidationSupport;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r5.model.StructureDefinition;
import org.junit.jupiter.api.Test;
import java.util.List;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class DefaultProfileValidationSupportR5Test {
private static FhirContext ourCtx = FhirContext.forR5Cached();
private DefaultProfileValidationSupport mySvc = new DefaultProfileValidationSupport(ourCtx);
@Test
public void testNoDuplicates() {
List<IBaseResource> allSds = mySvc.fetchAllStructureDefinitions()
.stream()
.map(t->(StructureDefinition)t)
.filter(t->t.getUrl().equals("http://hl7.org/fhir/StructureDefinition/language"))
.collect(Collectors.toList());
assertEquals(1, allSds.size());
}
}