diff --git a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java index 1eacae7780e..ab3faa6862d 100644 --- a/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java +++ b/hapi-fhir-base/src/main/java/ca/uhn/fhir/rest/api/Constants.java @@ -182,6 +182,11 @@ public class Constants { public static final String PARAM_HAS = "_has"; public static final String PARAM_HISTORY = "_history"; 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_RECURSE = "_include" + PARAM_INCLUDE_QUALIFIER_RECURSE; public static final String PARAM_INCLUDE_QUALIFIER_ITERATE = ":iterate"; diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-remove-duplicates-from-r5-defaultprofilevalidationsupport.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-remove-duplicates-from-r5-defaultprofilevalidationsupport.yaml new file mode 100644 index 00000000000..5ffee261d22 --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-remove-duplicates-from-r5-defaultprofilevalidationsupport.yaml @@ -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." diff --git a/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-support-language-sp.yaml b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-support-language-sp.yaml new file mode 100644 index 00000000000..ed819e5fe7e --- /dev/null +++ b/hapi-fhir-docs/src/main/resources/ca/uhn/hapi/fhir/changelog/7_0_0/5300-support-language-sp.yaml @@ -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." diff --git a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java index 42a2b393f53..b14872263ce 100644 --- a/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java +++ b/hapi-fhir-jpaserver-model/src/main/java/ca/uhn/fhir/jpa/model/entity/StorageSettings.java @@ -138,6 +138,13 @@ public class StorageSettings { * Since 6.4.0 */ 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 @@ -1295,6 +1302,23 @@ public class StorageSettings { 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) { Validate.notBlank(theUrl, "Base URL must not be null or empty"); diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java index 82a812d2864..f1d5a4a66b3 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/MatchUrlService.java @@ -165,7 +165,7 @@ public class MatchUrlService { IQueryParameterAnd param = JpaParamUtil.parseQueryParams( myFhirContext, RestSearchParameterTypeEnum.TOKEN, nextParamName, paramList); 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) } else { RuntimeSearchParam paramDef = diff --git a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java index 4313ac95d25..d8bd1b6b55d 100644 --- a/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java +++ b/hapi-fhir-jpaserver-searchparam/src/main/java/ca/uhn/fhir/jpa/searchparam/registry/SearchParamRegistryImpl.java @@ -32,6 +32,8 @@ import ca.uhn.fhir.jpa.cache.IResourceChangeListenerRegistry; import ca.uhn.fhir.jpa.cache.ResourceChangeResult; import ca.uhn.fhir.jpa.model.entity.StorageSettings; 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.server.exceptions.ResourceNotFoundException; import ca.uhn.fhir.rest.server.util.ISearchParamRegistry; @@ -193,6 +195,31 @@ public class SearchParamRegistryImpl long overriddenCount = overrideBuiltinSearchParamsWithActiveJpaSearchParams(searchParams, theJpaSearchParams); ourLog.trace("Have overridden {} built-in search parameters", overriddenCount); 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; myJpaSearchParamCache.populateActiveSearchParams( @@ -282,7 +309,13 @@ public class SearchParamRegistryImpl @Override public void forceRefresh() { + RuntimeSearchParamCache activeSearchParams = myActiveSearchParams; myResourceChangeListenerCache.forceRefresh(); + + // If the refresh didn't trigger a change, proceed with one anyway + if (myActiveSearchParams == activeSearchParams) { + rebuildActiveSearchParams(); + } } @Override diff --git a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java index a70eb85bc1e..3fdb26f9961 100644 --- a/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java +++ b/hapi-fhir-jpaserver-test-r5/src/test/java/ca/uhn/fhir/jpa/dao/r5/FhirResourceDaoR5SearchNoFtTest.java @@ -40,279 +40,285 @@ import java.util.List; import java.util.stream.Collectors; import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.contains; import static org.hamcrest.Matchers.containsInAnyOrder; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.fail; @ContextConfiguration(classes = TestHSearchAddInConfig.NoFT.class) @SuppressWarnings({"Duplicates"}) 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 - public void after() { - myStorageSettings.setIndexMissingFields(new JpaStorageSettings().getIndexMissingFields()); - } + @AfterEach + public void after() { + JpaStorageSettings defaults = new JpaStorageSettings(); + myStorageSettings.setIndexMissingFields(defaults.getIndexMissingFields()); + myStorageSettings.setLanguageSearchParameterEnabled(defaults.isLanguageSearchParameterEnabled()); + mySearchParamRegistry.forceRefresh(); + } - @Test - public void testHasWithTargetReference() { - Organization org = new Organization(); - org.setId("ORG"); - org.setName("ORG"); - myOrganizationDao.update(org); + @Test + public void testHasWithTargetReference() { + Organization org = new Organization(); + org.setId("ORG"); + org.setName("ORG"); + myOrganizationDao.update(org); - Practitioner practitioner = new Practitioner(); - practitioner.setId("PRACT"); - practitioner.addName().setFamily("PRACT"); - myPractitionerDao.update(practitioner); + Practitioner practitioner = new Practitioner(); + practitioner.setId("PRACT"); + practitioner.addName().setFamily("PRACT"); + myPractitionerDao.update(practitioner); - PractitionerRole role = new PractitionerRole(); - role.setId("ROLE"); - role.getPractitioner().setReference("Practitioner/PRACT"); - role.getOrganization().setReference("Organization/ORG"); - myPractitionerRoleDao.update(role); + PractitionerRole role = new PractitionerRole(); + role.setId("ROLE"); + role.getPractitioner().setReference("Practitioner/PRACT"); + role.getOrganization().setReference("Organization/ORG"); + myPractitionerRoleDao.update(role); - SearchParameterMap params = new SearchParameterMap(); - HasAndListParam value = new HasAndListParam(); - value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "ORG"))); - params.add("_has", value); - IBundleProvider outcome = myPractitionerDao.search(params); - assertEquals(1, outcome.getResources(0, 1).size()); - } - @Test - public void testHasWithTargetReferenceQualified() { - Organization org = new Organization(); - org.setId("ORG"); - org.setName("ORG"); - myOrganizationDao.update(org); + SearchParameterMap params = new SearchParameterMap(); + HasAndListParam value = new HasAndListParam(); + value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "ORG"))); + params.add("_has", value); + IBundleProvider outcome = myPractitionerDao.search(params); + assertEquals(1, outcome.getResources(0, 1).size()); + } - Practitioner practitioner = new Practitioner(); - practitioner.setId("PRACT"); - practitioner.addName().setFamily("PRACT"); - myPractitionerDao.update(practitioner); + @Test + public void testHasWithTargetReferenceQualified() { + Organization org = new Organization(); + org.setId("ORG"); + org.setName("ORG"); + myOrganizationDao.update(org); - PractitionerRole role = new PractitionerRole(); - role.setId("ROLE"); - role.getPractitioner().setReference("Practitioner/PRACT"); - role.getOrganization().setReference("Organization/ORG"); - myPractitionerRoleDao.update(role); + Practitioner practitioner = new Practitioner(); + practitioner.setId("PRACT"); + practitioner.addName().setFamily("PRACT"); + myPractitionerDao.update(practitioner); - SearchParameterMap params = new SearchParameterMap(); - HasAndListParam value = new HasAndListParam(); - value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "Organization/ORG"))); - params.add("_has", value); - IBundleProvider outcome = myPractitionerDao.search(params); - assertEquals(1, outcome.getResources(0, 1).size()); - } + PractitionerRole role = new PractitionerRole(); + role.setId("ROLE"); + role.getPractitioner().setReference("Practitioner/PRACT"); + role.getOrganization().setReference("Organization/ORG"); + myPractitionerRoleDao.update(role); - @Test - public void testHasWithTargetId() { - Organization org = new Organization(); - org.setId("ORG"); - org.setName("ORG"); - myOrganizationDao.update(org); + SearchParameterMap params = new SearchParameterMap(); + HasAndListParam value = new HasAndListParam(); + value.addAnd(new HasOrListParam().addOr(new HasParam("PractitionerRole", "practitioner", "organization", "Organization/ORG"))); + params.add("_has", value); + IBundleProvider outcome = myPractitionerDao.search(params); + assertEquals(1, outcome.getResources(0, 1).size()); + } - Practitioner practitioner = new Practitioner(); - practitioner.setId("PRACT"); - practitioner.addName().setFamily("PRACT"); - myPractitionerDao.update(practitioner); + @Test + public void testHasWithTargetId() { + Organization org = new Organization(); + org.setId("ORG"); + org.setName("ORG"); + myOrganizationDao.update(org); - PractitionerRole role = new PractitionerRole(); - role.setId("ROLE"); - role.getPractitioner().setReference("Practitioner/PRACT"); - role.getOrganization().setReference("Organization/ORG"); - myPractitionerRoleDao.update(role); + Practitioner practitioner = new Practitioner(); + practitioner.setId("PRACT"); + practitioner.addName().setFamily("PRACT"); + myPractitionerDao.update(practitioner); - runInTransaction(() -> { - ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); - }); + PractitionerRole role = new PractitionerRole(); + role.setId("ROLE"); + role.getPractitioner().setReference("Practitioner/PRACT"); + role.getOrganization().setReference("Organization/ORG"); + myPractitionerRoleDao.update(role); - SearchParameterMap params = SearchParameterMap.newSynchronous(); - 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()); - } + runInTransaction(() -> { + ourLog.info("Links:\n * {}", myResourceLinkDao.findAll().stream().map(t -> t.toString()).collect(Collectors.joining("\n * "))); + }); - @Test - public void testSearchDoesntFailIfResourcesAreDeleted() { + SearchParameterMap params = SearchParameterMap.newSynchronous(); + 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(); - p.addIdentifier().setValue("1"); - myPatientDao.create(p); + @Test + public void testSearchDoesntFailIfResourcesAreDeleted() { - p = new Patient(); - p.addIdentifier().setValue("2"); - myPatientDao.create(p); + Patient p = new Patient(); + p.addIdentifier().setValue("1"); + myPatientDao.create(p); - p = new Patient(); - p.addIdentifier().setValue("3"); - Long id = myPatientDao.create(p).getId().getIdPartAsLong(); + p = new Patient(); + p.addIdentifier().setValue("2"); + myPatientDao.create(p); - IBundleProvider outcome = myPatientDao.search(new SearchParameterMap()); - assertEquals(3, outcome.size().intValue()); + p = new Patient(); + p.addIdentifier().setValue("3"); + Long id = myPatientDao.create(p).getId().getIdPartAsLong(); - runInTransaction(() -> { - ResourceTable table = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException()); - table.setDeleted(new Date()); - myResourceTableDao.save(table); - }); + IBundleProvider outcome = myPatientDao.search(new SearchParameterMap()); + assertEquals(3, outcome.size().intValue()); - assertEquals(2, outcome.getResources(0, 3).size()); + runInTransaction(() -> { + ResourceTable table = myResourceTableDao.findById(id).orElseThrow(() -> new IllegalArgumentException()); + table.setDeleted(new Date()); + myResourceTableDao.save(table); + }); - runInTransaction(() -> { - myResourceHistoryTableDao.deleteAll(); - }); + assertEquals(2, outcome.getResources(0, 3).size()); - assertEquals(0, outcome.getResources(0, 3).size()); - } + runInTransaction(() -> { + myResourceHistoryTableDao.deleteAll(); + }); - @Test - public void testToken_CodeableReference_Reference() { - // Setup + assertEquals(0, outcome.getResources(0, 3).size()); + } - ObservationDefinition obs = new ObservationDefinition(); - obs.setApprovalDate(new Date()); - String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); + @Test + public void testToken_CodeableReference_Reference() { + // Setup - ClinicalUseDefinition def = new ClinicalUseDefinition(); - def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); - String id = myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue(); + ObservationDefinition obs = new ObservationDefinition(); + obs.setApprovalDate(new Date()); + String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); - ClinicalUseDefinition def2 = new ClinicalUseDefinition(); - def2.getContraindication().getDiseaseSymptomProcedure().setConcept(new CodeableConcept().addCoding(new Coding("http://foo", "bar", "baz"))); - myClinicalUseDefinitionDao.create(def2, mySrd).getId().toUnqualifiedVersionless().getValue(); + ClinicalUseDefinition def = new ClinicalUseDefinition(); + def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); + 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)); - List outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd)); - assertThat(outcome, Matchers.contains(id)); + // Test - } + SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION_REFERENCE, new ReferenceParam(obsId)); + List outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd)); + assertThat(outcome, Matchers.contains(id)); - @Test - public void testToken_CodeableReference_Coding() { - // Setup + } - ObservationDefinition obs = new ObservationDefinition(); - obs.setApprovalDate(new Date()); - String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); + @Test + public void testToken_CodeableReference_Coding() { + // Setup - ClinicalUseDefinition def = new ClinicalUseDefinition(); - def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); - myClinicalUseDefinitionDao.create(def, mySrd).getId().toUnqualifiedVersionless().getValue(); + ObservationDefinition obs = new ObservationDefinition(); + obs.setApprovalDate(new Date()); + String obsId = myObservationDefinitionDao.create(obs, mySrd).getId().toUnqualifiedVersionless().getValue(); - 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(); + ClinicalUseDefinition def = new ClinicalUseDefinition(); + def.getContraindication().getDiseaseSymptomProcedure().setReference(new Reference(obsId)); + 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")); - List outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd)); - assertThat(outcome, Matchers.contains(id)); + // Test - } + SearchParameterMap map = SearchParameterMap.newSynchronous(ClinicalUseDefinition.SP_CONTRAINDICATION, new TokenParam("http://foo", "bar")); + List outcome = toUnqualifiedVersionlessIdValues(myClinicalUseDefinitionDao.search(map, mySrd)); + assertThat(outcome, Matchers.contains(id)); + + } - @Test - public void testIndexAddressDistrict() { - // Setup - Patient p = new Patient(); - p.addAddress() - .setDistrict("DISTRICT123"); - String id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless().getValue(); + @Test + public void testIndexAddressDistrict() { + // Setup + Patient p = new Patient(); + p.addAddress() + .setDistrict("DISTRICT123"); + String id = myPatientDao.create(p, mySrd).getId().toUnqualifiedVersionless().getValue(); - logAllStringIndexes(); + logAllStringIndexes(); - // Test - SearchParameterMap params = SearchParameterMap - .newSynchronous(Patient.SP_ADDRESS, new StringParam("DISTRICT123")); - IBundleProvider outcome = myPatientDao.search(params, mySrd); + // Test + SearchParameterMap params = SearchParameterMap + .newSynchronous(Patient.SP_ADDRESS, new StringParam("DISTRICT123")); + IBundleProvider outcome = myPatientDao.search(params, mySrd); - // Verify - assertThat(toUnqualifiedVersionlessIdValues(outcome), Matchers.contains(id)); + // Verify + assertThat(toUnqualifiedVersionlessIdValues(outcome), Matchers.contains(id)); - } + } - /** - * Index for - * [base]/Bundle?composition.patient.identifier=foo - */ - @ParameterizedTest - @CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) - public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { - // Setup 1 + /** + * Index for + * [base]/Bundle?composition.patient.identifier=foo + */ + @ParameterizedTest + @CsvSource({"urn:uuid:5c34dc2c-9b5d-4ec1-b30b-3e2d4371508b", "Patient/ABC"}) + public void testCreateAndSearchForFullyChainedSearchParameter(String thePatientId) { + // Setup 1 - myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); + myStorageSettings.setIndexMissingFields(JpaStorageSettings.IndexEnabledEnum.DISABLED); - SearchParameter sp = new SearchParameter(); - sp.setId("SearchParameter/Bundle-composition-patient-identifier"); - sp.setCode("composition.patient.identifier"); - sp.setName("composition.patient.identifier"); - sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); - sp.setStatus(Enumerations.PublicationStatus.ACTIVE); - sp.setType(Enumerations.SearchParamType.TOKEN); - sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); - sp.addBase(Enumerations.VersionIndependentResourceTypesAll.BUNDLE); - ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); - mySearchParameterDao.update(sp, mySrd); + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/Bundle-composition-patient-identifier"); + sp.setCode("composition.patient.identifier"); + sp.setName("composition.patient.identifier"); + sp.setUrl("http://example.org/SearchParameter/Bundle-composition-patient-identifier"); + sp.setStatus(Enumerations.PublicationStatus.ACTIVE); + sp.setType(Enumerations.SearchParamType.TOKEN); + sp.setExpression("Bundle.entry[0].resource.as(Composition).subject.resolve().as(Patient).identifier"); + sp.addBase(Enumerations.VersionIndependentResourceTypesAll.BUNDLE); + ourLog.info("SP: {}", myFhirContext.newJsonParser().setPrettyPrint(true).encodeResourceToString(sp)); + mySearchParameterDao.update(sp, mySrd); - mySearchParamRegistry.forceRefresh(); + mySearchParamRegistry.forceRefresh(); - // Test 1 + // Test 1 - Composition composition = new Composition(); - composition.addSubject().setReference(thePatientId); + Composition composition = new Composition(); + composition.addSubject().setReference(thePatientId); - Patient patient = new Patient(); - patient.setId(new IdType(thePatientId)); - patient.addIdentifier().setSystem("http://foo").setValue("bar"); + Patient patient = new Patient(); + patient.setId(new IdType(thePatientId)); + patient.addIdentifier().setSystem("http://foo").setValue("bar"); - Bundle bundle = new Bundle(); - bundle.setType(Bundle.BundleType.DOCUMENT); - bundle.addEntry().setResource(composition); - bundle.addEntry().setResource(patient); + Bundle bundle = new Bundle(); + bundle.setType(Bundle.BundleType.DOCUMENT); + bundle.addEntry().setResource(composition); + bundle.addEntry().setResource(patient); - myBundleDao.create(bundle, mySrd); + myBundleDao.create(bundle, mySrd); - Bundle bundle2 = new Bundle(); - bundle2.setType(Bundle.BundleType.DOCUMENT); - myBundleDao.create(bundle2, mySrd); + Bundle bundle2 = new Bundle(); + bundle2.setType(Bundle.BundleType.DOCUMENT); + myBundleDao.create(bundle2, mySrd); - // Verify 1 - runInTransaction(() -> { - logAllTokenIndexes(); + // Verify 1 + runInTransaction(() -> { + logAllTokenIndexes(); - List params = myResourceIndexedSearchParamTokenDao - .findAll() - .stream() - .filter(t -> t.getParamName().contains(".")) - .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) - .toList(); - assertThat(params.toString(), params, containsInAnyOrder( - "composition.patient.identifier http://foo|bar" - )); - }); + List params = myResourceIndexedSearchParamTokenDao + .findAll() + .stream() + .filter(t -> t.getParamName().contains(".")) + .map(t -> t.getParamName() + " " + t.getSystem() + "|" + t.getValue()) + .toList(); + assertThat(params.toString(), params, containsInAnyOrder( + "composition.patient.identifier http://foo|bar" + )); + }); - // Test 2 - IBundleProvider outcome; + // Test 2 + IBundleProvider outcome; - SearchParameterMap map = SearchParameterMap - .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); - outcome = myBundleDao.search(map, mySrd); - assertEquals(1, outcome.size()); + SearchParameterMap map = SearchParameterMap + .newSynchronous("composition.patient.identifier", new TokenParam("http://foo", "bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); - map = SearchParameterMap - .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); - outcome = myBundleDao.search(map, mySrd); - assertEquals(1, outcome.size()); - } + map = SearchParameterMap + .newSynchronous("composition", new ReferenceParam("patient.identifier", "http://foo|bar")); + outcome = myBundleDao.search(map, mySrd); + assertEquals(1, outcome.size()); + } @Test 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")); + } } diff --git a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java index b6f5fbf91eb..27fc6358c28 100644 --- a/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java +++ b/hapi-fhir-jpaserver-test-utilities/src/test/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidatorTest.java @@ -46,189 +46,186 @@ import static org.mockito.Mockito.lenient; @ExtendWith(MockitoExtension.class) public class SearchParameterDaoValidatorTest { - @Spy - private FhirContext myFhirContext = FhirContext.forR5Cached(); - @Mock - private ISearchParamRegistry mySearchParamRegistry; - @Spy - private JpaStorageSettings myStorageSettings = new JpaStorageSettings(); - @InjectMocks - private SearchParameterDaoValidator mySvc; + private static final String SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN = "SearchParameter/observation-code"; + private static final String SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE = "SearchParameter/observation-patient"; + 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"; + @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"; - private static final String SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE = "SearchParameter/observation-patient"; - 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"; + SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); + mySvc.validate(canonicalSp); + } - @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"); - } + @Test + public void testValidateSubscriptionWithCustomType() { + SearchParameter sp = new SearchParameter(); + sp.setId("SearchParameter/meal-chef"); + sp.setUrl("http://example.org/SearchParameter/meal-chef"); + sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE).setValue(new StringType("Meal"))); + 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"); - 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); - } + SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); + mySvc.validate(canonicalSp); + } - @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); + @ParameterizedTest + @MethodSource("extensionProvider") + public void testMethodValidate_nonUniqueComboAndCompositeSearchParamWithComponentOfTypeReference_isNotAllowed(Extension theExtension) { + SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation"); + sp.addExtension(theExtension); - SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); - mySvc.validate(canonicalSp); - } + sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); + sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE)); - @Test - public void testValidateSubscriptionWithCustomType() { - SearchParameter sp = new SearchParameter(); - sp.setId("SearchParameter/meal-chef"); - sp.setUrl("http://example.org/SearchParameter/meal-chef"); - sp.addExtension(new Extension(HapiExtensions.EXTENSION_SEARCHPARAM_CUSTOM_BASE_RESOURCE).setValue(new StringType("Meal"))); - 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"); + try { + mySvc.validate(sp); + fail(); + } catch (UnprocessableEntityException ex) { + assertTrue(ex.getMessage().startsWith("HAPI-2347: ")); + assertTrue(ex.getMessage().contains("Invalid component search parameter type: REFERENCE in component.definition: http://example.org/SearchParameter/observation-patient")); + } + } - SearchParameter canonicalSp = myVersionCanonicalizer.searchParameterToCanonical(sp); - mySvc.validate(canonicalSp); - } + @Test + 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 - @MethodSource("extensionProvider") - public void testMethodValidate_nonUniqueComboAndCompositeSearchParamWithComponentOfTypeReference_isNotAllowed(Extension theExtension) { - SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/patient-code", "patient-code", "Observation"); - sp.addExtension(theExtension); + sp.addComponent(new SearchParameterComponentComponent() + .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); + sp.addComponent(new SearchParameterComponentComponent() + .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE)); - sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); - sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE)); + mySvc.validate(sp); + } - try { - mySvc.validate(sp); - fail(); - } catch (UnprocessableEntityException ex) { - assertTrue(ex.getMessage().startsWith("HAPI-2347: ")); - assertTrue(ex.getMessage().contains("Invalid component search parameter type: REFERENCE in component.definition: http://example.org/SearchParameter/observation-patient")); - } - } + @ParameterizedTest + @MethodSource("comboSpProvider") + public void testMethodValidate_comboSearchParamsWithNumberUriComponents_isValid(SearchParameter theSearchParameter) { + theSearchParameter.addComponent(new SearchParameterComponentComponent() + .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI)); + theSearchParameter.addComponent(new SearchParameterComponentComponent() + .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER)); - @Test - 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))); + mySvc.validate(theSearchParameter); + } - sp.addComponent(new SearchParameterComponentComponent() - .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_TOKEN)); - sp.addComponent(new SearchParameterComponentComponent() - .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_REFERENCE)); + @Test + public void testMethodValidate_compositeSearchParamsWithNumberUriComponents_isNotAllowed() { + SearchParameter sp = createSearchParameter(COMPOSITE, "SearchParameter/component-value-uri-number", "component-value-uri-number", "Observation"); - 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 - @MethodSource("comboSpProvider") - public void testMethodValidate_comboSearchParamsWithNumberUriComponents_isValid(SearchParameter theSearchParameter) { - theSearchParameter.addComponent(new SearchParameterComponentComponent() - .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI)); - theSearchParameter.addComponent(new SearchParameterComponentComponent() - .setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER)); + try { + 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")); + } + } - 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 - public void testMethodValidate_compositeSearchParamsWithNumberUriComponents_isNotAllowed() { - 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_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")); - sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_URI)); - sp.addComponent(new SearchParameterComponentComponent().setDefinition(SP_COMPONENT_DEFINITION_OF_TYPE_NUMBER)); + mySvc.validate(theSearchParameter); + } - try { - 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")); - } - } + private static SearchParameter createSearchParameter(Enumerations.SearchParamType theType, String theId, String theCodeValue, String theExpression) { - @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) { + SearchParameter retVal = new SearchParameter(); + retVal.setId(theId); + retVal.setUrl("http://example.org/" + theId); + retVal.addBase(OBSERVATION); + retVal.setCode(theCodeValue); + retVal.setType(theType); + retVal.setStatus(ACTIVE); + retVal.setExpression(theExpression); - theSearchParameter.addComponent(new SearchParameter.SearchParameterComponentComponent() - .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")); + return retVal; + } - mySvc.validate(theSearchParameter); - } + static Stream 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 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(); - retVal.setId(theId); - retVal.setUrl("http://example.org/" + theId); - retVal.addBase(OBSERVATION); - retVal.setCode(theCodeValue); - retVal.setType(theType); - retVal.setStatus(ACTIVE); - retVal.setExpression(theExpression); + 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 + ); + } - return retVal; - } - - static Stream 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 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 compositeSpProvider() { - return Stream.concat(comboSpProvider(), Stream.of( - Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")) // composite SP - )); - } + static Stream compositeSpProvider() { + return Stream.concat(comboSpProvider(), Stream.of( + Arguments.of(createSearchParameter(Enumerations.SearchParamType.COMPOSITE, "SearchParameter/any-type", "any-type", "Observation")) // composite SP + )); + } } diff --git a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java index 229f400c1ed..4bf5ec2e9c7 100644 --- a/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java +++ b/hapi-fhir-jpaserver-uhnfhirtest/src/main/java/ca/uhn/fhirtest/config/CommonJpaStorageSettingsConfigurer.java @@ -6,5 +6,6 @@ public class CommonJpaStorageSettingsConfigurer { public CommonJpaStorageSettingsConfigurer(JpaStorageSettings theStorageSettings) { theStorageSettings.setIndexOnUpliftedRefchains(true); theStorageSettings.setMarkResourcesForReindexingUponSearchParameterChange(false); + theStorageSettings.setLanguageSearchParameterEnabled(true); } } diff --git a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java index f0d75caea5b..a64ec4a9b8c 100644 --- a/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java +++ b/hapi-fhir-storage/src/main/java/ca/uhn/fhir/jpa/dao/validation/SearchParameterDaoValidator.java @@ -98,10 +98,12 @@ public class SearchParameterDaoValidator { return; } + // Search parameters must have a base if (isCompositeWithoutBase(searchParameter)) { throw new UnprocessableEntityException(Msg.code(1113) + "SearchParameter.base is missing"); } + // Do we have a valid expression if (isCompositeWithoutExpression(searchParameter)) { // this is ok diff --git a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java index 6a74d538050..7467eecc2d8 100644 --- a/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java +++ b/hapi-fhir-test-utilities/src/main/java/ca/uhn/fhir/test/utilities/ITestDataBuilder.java @@ -80,6 +80,13 @@ public interface ITestDataBuilder { 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 */ diff --git a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java index dea829487ac..c5faca542c3 100644 --- a/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java +++ b/hapi-fhir-validation/src/main/java/org/hl7/fhir/common/hapi/validation/support/PrePopulatedValidationSupport.java @@ -16,12 +16,14 @@ import org.hl7.fhir.r4.model.ValueSet; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.Collectors; import javax.annotation.Nonnull; import javax.annotation.Nullable; @@ -34,10 +36,14 @@ import static org.apache.commons.lang3.StringUtils.isNotBlank; public class PrePopulatedValidationSupport extends BaseStaticResourceValidationSupport implements IValidationSupport, ILockable { - private final Map myCodeSystems; - private final Map myStructureDefinitions; - private final Map mySearchParameters; - private final Map myValueSets; + private final Map myUrlToCodeSystems; + private final Map myUrlToStructureDefinitions; + private final Map myUrlToSearchParameters; + private final Map myUrlToValueSets; + private final List myCodeSystems; + private final List myStructureDefinitions; + private final List mySearchParameters; + private final List myValueSets; private final Map myBinaries; private boolean myLocked; @@ -51,51 +57,67 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS /** * 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. - * @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. - * @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. **/ public PrePopulatedValidationSupport( FhirContext theFhirContext, - Map theStructureDefinitions, - Map theValueSets, - Map theCodeSystems) { - this(theFhirContext, theStructureDefinitions, theValueSets, theCodeSystems, new HashMap<>(), new HashMap<>()); + Map theUrlToStructureDefinitions, + Map theUrlToValueSets, + Map theUrlToCodeSystems) { + this( + theFhirContext, + theUrlToStructureDefinitions, + theUrlToValueSets, + theUrlToCodeSystems, + new HashMap<>(), + new HashMap<>()); } /** * 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. - * @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. - * @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. * @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. */ public PrePopulatedValidationSupport( FhirContext theFhirContext, - Map theStructureDefinitions, - Map theValueSets, - Map theCodeSystems, - Map theSearchParameters, + Map theUrlToStructureDefinitions, + Map theUrlToValueSets, + Map theUrlToCodeSystems, + Map theUrlToSearchParameters, Map theBinaries) { super(theFhirContext); Validate.notNull(theFhirContext, "theFhirContext must not be null"); - Validate.notNull(theStructureDefinitions, "theStructureDefinitions must not be null"); - Validate.notNull(theValueSets, "theValueSets must not be null"); - Validate.notNull(theCodeSystems, "theCodeSystems must not be null"); - Validate.notNull(theSearchParameters, "theSearchParameters must not be null"); + Validate.notNull(theUrlToStructureDefinitions, "theStructureDefinitions must not be null"); + Validate.notNull(theUrlToValueSets, "theValueSets must not be null"); + Validate.notNull(theUrlToCodeSystems, "theCodeSystems must not be null"); + Validate.notNull(theUrlToSearchParameters, "theSearchParameters must not be null"); Validate.notNull(theBinaries, "theBinaries must not be null"); - myStructureDefinitions = theStructureDefinitions; - myValueSets = theValueSets; - myCodeSystems = theCodeSystems; - mySearchParameters = theSearchParameters; + myUrlToStructureDefinitions = theUrlToStructureDefinitions; + myStructureDefinitions = + theUrlToStructureDefinitions.values().stream().distinct().collect(Collectors.toList()); + + 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; } @@ -127,7 +149,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS public void addCodeSystem(IBaseResource theCodeSystem) { validateNotLocked(); Set urls = processResourceAndReturnUrls(theCodeSystem, "CodeSystem"); - addToMap(theCodeSystem, myCodeSystems, urls); + addToMap(theCodeSystem, myCodeSystems, myUrlToCodeSystems, urls); } private Set processResourceAndReturnUrls(IBaseResource theResource, String theResourceName) { @@ -185,16 +207,18 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS public void addStructureDefinition(IBaseResource theStructureDefinition) { validateNotLocked(); Set url = processResourceAndReturnUrls(theStructureDefinition, "StructureDefinition"); - addToMap(theStructureDefinition, myStructureDefinitions, url); + addToMap(theStructureDefinition, myStructureDefinitions, myUrlToStructureDefinitions, url); } public void addSearchParameter(IBaseResource theSearchParameter) { validateNotLocked(); Set url = processResourceAndReturnUrls(theSearchParameter, "SearchParameter"); - addToMap(theSearchParameter, mySearchParameters, url); + addToMap(theSearchParameter, mySearchParameters, myUrlToSearchParameters, url); } - private void addToMap(T theResource, Map theMap, Collection theUrls) { + private void addToMap( + T theResource, List theList, Map theMap, Collection theUrls) { + theList.add(theResource); for (String urls : theUrls) { if (isNotBlank(urls)) { theMap.put(urls, theResource); @@ -228,7 +252,7 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS public void addValueSet(IBaseResource theValueSet) { validateNotLocked(); Set urls = processResourceAndReturnUrls(theValueSet, "ValueSet"); - addToMap(theValueSet, myValueSets, urls); + addToMap(theValueSet, myValueSets, myUrlToValueSets, urls); } /** @@ -259,36 +283,38 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS @Override public List fetchAllConformanceResources() { ArrayList retVal = new ArrayList<>(); - retVal.addAll(myCodeSystems.values()); - retVal.addAll(myStructureDefinitions.values()); - retVal.addAll(myValueSets.values()); + retVal.addAll(myCodeSystems); + retVal.addAll(myStructureDefinitions); + retVal.addAll(myValueSets); return retVal; } + @SuppressWarnings("unchecked") @Nullable @Override public List fetchAllSearchParameters() { - return toList(mySearchParameters); + return (List) Collections.unmodifiableList(mySearchParameters); } + @SuppressWarnings("unchecked") @Override public List fetchAllStructureDefinitions() { - return toList(myStructureDefinitions); + return (List) Collections.unmodifiableList(myStructureDefinitions); } @Override public IBaseResource fetchCodeSystem(String theSystem) { - return myCodeSystems.get(theSystem); + return myUrlToCodeSystems.get(theSystem); } @Override public IBaseResource fetchValueSet(String theUri) { - return myValueSets.get(theUri); + return myUrlToValueSets.get(theUri); } @Override public IBaseResource fetchStructureDefinition(String theUrl) { - return myStructureDefinitions.get(theUrl); + return myUrlToStructureDefinitions.get(theUrl); } @Override @@ -298,19 +324,23 @@ public class PrePopulatedValidationSupport extends BaseStaticResourceValidationS @Override public boolean isCodeSystemSupported(ValidationSupportContext theValidationSupportContext, String theSystem) { - return myCodeSystems.containsKey(theSystem); + return myUrlToCodeSystems.containsKey(theSystem); } @Override public boolean isValueSetSupported(ValidationSupportContext theValidationSupportContext, String theValueSetUrl) { - return myValueSets.containsKey(theValueSetUrl); + return myUrlToValueSets.containsKey(theValueSetUrl); } /** * Returns a count of all known resources */ public int countAll() { - return myBinaries.size() + myCodeSystems.size() + myStructureDefinitions.size() + myValueSets.size(); + return myBinaries.size() + + myCodeSystems.size() + + myStructureDefinitions.size() + + myValueSets.size() + + myStructureDefinitions.size(); } @Override diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportTest.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR4Test.java similarity index 93% rename from hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportTest.java rename to hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR4Test.java index 97fd08553c1..adc1b855c91 100644 --- a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportTest.java +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR4Test.java @@ -2,7 +2,6 @@ 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.r4.model.CodeSystem; 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.assertNull; -public class DefaultProfileValidationSupportTest { +public class DefaultProfileValidationSupportR4Test { private static FhirContext ourCtx = FhirContext.forR4Cached(); private DefaultProfileValidationSupport mySvc = new DefaultProfileValidationSupport(ourCtx); diff --git a/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR5Test.java b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR5Test.java new file mode 100644 index 00000000000..976fe35c87b --- /dev/null +++ b/hapi-fhir-validation/src/test/java/org/hl7/fhir/dstu3/hapi/validation/DefaultProfileValidationSupportR5Test.java @@ -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 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()); + } + +}