diff --git a/api/src/main/java/org/openmrs/module/fhir2/FhirConstants.java b/api/src/main/java/org/openmrs/module/fhir2/FhirConstants.java index 9e4ac92e2..3bfa37c24 100644 --- a/api/src/main/java/org/openmrs/module/fhir2/FhirConstants.java +++ b/api/src/main/java/org/openmrs/module/fhir2/FhirConstants.java @@ -177,6 +177,8 @@ private FhirConstants() { public static final String ADMINISTERING_ENCOUNTER_ROLE_PROPERTY = "fhir2.administeringEncounterRoleUuid"; + public static final String SUPPORTED_LOCATION_HIERARCHY_SEARCH_DEPTH = "fhir.supportedLocationHierarchySearchDepth"; + public static final String GLOBAL_PROPERTY_MILD = "allergy.concept.severity.mild"; public static final String GLOBAL_PROPERTY_SEVERE = "allergy.concept.severity.severe"; diff --git a/api/src/main/java/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImpl.java b/api/src/main/java/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImpl.java index 9d3ebfc14..a84f206e5 100644 --- a/api/src/main/java/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImpl.java +++ b/api/src/main/java/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImpl.java @@ -10,18 +10,23 @@ package org.openmrs.module.fhir2.api.dao.impl; import static org.hibernate.criterion.Restrictions.eq; +import static org.hibernate.criterion.Restrictions.or; import javax.annotation.Nonnull; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import ca.uhn.fhir.rest.param.ReferenceAndListParam; +import ca.uhn.fhir.rest.param.ReferenceOrListParam; +import ca.uhn.fhir.rest.param.ReferenceParam; import ca.uhn.fhir.rest.param.StringAndListParam; import ca.uhn.fhir.rest.param.TokenAndListParam; import lombok.AccessLevel; import lombok.Setter; import org.hibernate.Criteria; +import org.hibernate.criterion.Criterion; import org.hibernate.sql.JoinType; import org.openmrs.Location; import org.openmrs.LocationAttribute; @@ -29,6 +34,7 @@ import org.openmrs.LocationTag; import org.openmrs.api.LocationService; import org.openmrs.module.fhir2.FhirConstants; +import org.openmrs.module.fhir2.api.FhirGlobalPropertyService; import org.openmrs.module.fhir2.api.dao.FhirLocationDao; import org.openmrs.module.fhir2.api.search.param.SearchParameterMap; import org.springframework.beans.factory.annotation.Autowired; @@ -41,6 +47,9 @@ public class FhirLocationDaoImpl extends BaseFhirDao implements FhirLo @Autowired LocationService locationService; + @Autowired + private FhirGlobalPropertyService globalPropertyService; + @Override protected void setupSearchParams(Criteria criteria, SearchParameterMap theParams) { theParams.getParameters().forEach(entry -> { @@ -62,7 +71,7 @@ protected void setupSearchParams(Criteria criteria, SearchParameterMap theParams break; case FhirConstants.LOCATION_REFERENCE_SEARCH_HANDLER: entry.getValue() - .forEach(param -> handleParentLocation(criteria, (ReferenceAndListParam) param.getParam())); + .forEach(param -> handleLocationReference(criteria, (ReferenceAndListParam) param.getParam())); break; case FhirConstants.TAG_SEARCH_HANDLER: entry.getValue().forEach(param -> handleTag(criteria, (TokenAndListParam) param.getParam())); @@ -112,8 +121,60 @@ private void handleTag(Criteria criteria, TokenAndListParam tags) { } } - private void handleParentLocation(Criteria criteria, ReferenceAndListParam parent) { - handleLocationReference("loc", parent).ifPresent(loc -> criteria.createAlias("parentLocation", "loc").add(loc)); + private void handleLocationReference(Criteria criteria, ReferenceAndListParam locationAndReferences) { + + if (locationAndReferences == null) { + return; + } + + List locationOrReference = locationAndReferences.getValuesAsQueryTokens(); + + if (locationOrReference == null || locationOrReference.isEmpty()) { + return; + } + + if (locationOrReference.size() > 1) { + throw new IllegalArgumentException("Only one location reference is supported"); + } + + List locationReferences = locationOrReference.get(0).getValuesAsQueryTokens(); + + if (locationReferences == null || locationReferences.isEmpty()) { + return; + } + + if (locationReferences.size() > 1) { + throw new IllegalArgumentException("Only one location reference is supported"); + } + + ReferenceParam locationReference = locationReferences.get(0); + + // **NOTE: this is a *bug* in the current HAPI FHIR implementation, "below" should be the "queryParameterQualifier", not the resource type; likely need update this when/fix the HAPI FHIR implementation is fixed** + // this is to support queries of the type "Location?partof=below:uuid" + if ("below".equalsIgnoreCase(locationReference.getResourceType())) { + + int searchDepth = globalPropertyService + .getGlobalPropertyAsInteger(FhirConstants.SUPPORTED_LOCATION_HIERARCHY_SEARCH_DEPTH, 5); + + List belowReferenceCriteria = new ArrayList<>(); + + // we need to add a join to the parentLocation for each level of hierarchy we want to search, and add "equals" criterion for each level + int depth = 1; + while (depth <= searchDepth) { + belowReferenceCriteria.add(eq("ancestor" + depth + ".uuid", locationReference.getIdPart())); + criteria.createAlias(depth == 1 ? "parentLocation" : "ancestor" + (depth - 1) + ".parentLocation", + "ancestor" + depth, JoinType.LEFT_OUTER_JOIN); + depth++; + } + + // "or" these call together so that we return the location if any of the joined ancestor location uuids match + criteria.add(or(belowReferenceCriteria.toArray(new Criterion[0]))); + } else { + // this is to support queries of the type "Location?partof=uuid" or chained search like "Location?partof:Location=Location:name=xxx" + handleLocationReference("loc", locationAndReferences) + .ifPresent(loc -> criteria.createAlias("parentLocation", "loc").add(loc)); + } + } @Override diff --git a/api/src/test/java/org/openmrs/module/fhir2/MockedGlobalPropertyServiceConfiguration.java b/api/src/test/java/org/openmrs/module/fhir2/MockedGlobalPropertyServiceConfiguration.java new file mode 100644 index 000000000..e69de29bb diff --git a/api/src/test/java/org/openmrs/module/fhir2/api/impl/FhirLocationServiceImplTest.java b/api/src/test/java/org/openmrs/module/fhir2/api/impl/FhirLocationServiceImplTest.java index 227ef8e6f..57c6ef097 100644 --- a/api/src/test/java/org/openmrs/module/fhir2/api/impl/FhirLocationServiceImplTest.java +++ b/api/src/test/java/org/openmrs/module/fhir2/api/impl/FhirLocationServiceImplTest.java @@ -48,8 +48,6 @@ @RunWith(MockitoJUnitRunner.class) public class FhirLocationServiceImplTest { - private static final Integer LOCATION_ID = 123; - private static final String LOCATION_UUID = "a1758922-b132-4ead-8ebe-5e2b4eaf43a1"; private static final String LOCATION_NAME = "Test location 1"; diff --git a/api/src/test/java/org/openmrs/module/fhir2/api/search/LocationSearchQueryTest.java b/api/src/test/java/org/openmrs/module/fhir2/api/search/LocationSearchQueryTest.java index eb0ff3f44..210a418e3 100644 --- a/api/src/test/java/org/openmrs/module/fhir2/api/search/LocationSearchQueryTest.java +++ b/api/src/test/java/org/openmrs/module/fhir2/api/search/LocationSearchQueryTest.java @@ -95,6 +95,8 @@ public class LocationSearchQueryTest extends BaseFhirContextSensitiveTest { private static final String LOCATION_PARENT_NAME = "Test location 5"; + private static final String LOCATION_ANCESTOR_TEST_UUID = "76cd2d30-2411-44ef-84ea-8b7473256a6a"; + private static final String DATE_CREATED = "2005-01-01"; private static final String DATE_CHANGED = "2010-03-31"; @@ -749,6 +751,31 @@ public void searchForLocations_shouldSortLocationsByCityAsRequested() { } } + @Test + public void searchForLocations_shouldReturnCorrectLocationsByAncestorUUID() { + + ReferenceParam param = new ReferenceParam("below", null, LOCATION_ANCESTOR_TEST_UUID); + ReferenceAndListParam ancestorLocation = new ReferenceAndListParam().addAnd(new ReferenceOrListParam().add(param)); + + SearchParameterMap theParams = new SearchParameterMap().addParameter(FhirConstants.LOCATION_REFERENCE_SEARCH_HANDLER, + ancestorLocation); + + IBundleProvider locations = search(theParams); + + assertThat(locations, notNullValue()); + assertThat(locations.size(), equalTo(4)); + + List resultList = get(locations); + + assertThat(resultList, hasSize(equalTo(4))); + List locationNames = resultList.stream().map(Location::getName).collect(Collectors.toList()); + assertThat(locationNames, not(hasItem("Test location 4"))); // element search for ("below" search on part should *not* be inclusive) + assertThat(locationNames, hasItem("Test location 6")); // child element + assertThat(locationNames, hasItem("Test location 8")); // child element + assertThat(locationNames, hasItem("Test location 11")); // grandchild element + assertThat(locationNames, hasItem("Test location 12")); // great grandchild element + } + private List getLocationListWithoutNulls(SortSpec sort) { SearchParameterMap theParams = new SearchParameterMap().setSortSpec(sort); IBundleProvider locations = search(theParams); diff --git a/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r3/LocationFhirResourceProviderIntegrationTest.java b/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r3/LocationFhirResourceProviderIntegrationTest.java index fb4211425..80d7d3d28 100644 --- a/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r3/LocationFhirResourceProviderIntegrationTest.java +++ b/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r3/LocationFhirResourceProviderIntegrationTest.java @@ -48,6 +48,8 @@ public class LocationFhirResourceProviderIntegrationTest extends BaseFhirR3Integ private static final String PARENT_LOCATION_UUID = "76cd2d30-2411-44ef-84ea-8b7473256a6a"; + private static final String LOCATION_ANCESTOR_TEST_UUID = "76cd2d30-2411-44ef-84ea-8b7473256a6a"; + private static final String JSON_CREATE_LOCATION_DOCUMENT = "org/openmrs/module/fhir2/providers/LocationWebTest_create.json"; private static final String XML_CREATE_LOCATION_DOCUMENT = "org/openmrs/module/fhir2/providers/LocationWebTest_create.xml"; @@ -462,6 +464,7 @@ public void shouldSearchForExistingLocationsAsJson() throws Exception { assertThat(entries, everyItem(hasResource(instanceOf(Location.class)))); assertThat(entries, everyItem(hasResource(validResource()))); + // search by address and parent location response = get("/Location?address-city=Kerio&partof=" + PARENT_LOCATION_UUID + "&_sort=name") .accept(FhirMediaTypes.JSON).go(); @@ -483,6 +486,30 @@ public void shouldSearchForExistingLocationsAsJson() throws Exception { assertThat(entries, containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), hasResource(hasProperty("name", equalTo("Test location 8"))))); assertThat(entries, everyItem(hasResource(validResource()))); + + // search by ancestors + response = get("/Location?partof:below=" + LOCATION_ANCESTOR_TEST_UUID).accept(FhirMediaTypes.JSON).go(); + + assertThat(response, isOk()); + assertThat(response.getContentType(), is(FhirMediaTypes.JSON.toString())); + assertThat(response.getContentAsString(), notNullValue()); + + results = readBundleResponse(response); + + assertThat(results, notNullValue()); + assertThat(results.getType(), equalTo(Bundle.BundleType.SEARCHSET)); + assertThat(results.hasEntry(), is(true)); + + entries = results.getEntry(); + assertThat(entries.size(), is(4)); + + assertThat(entries, + containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), + hasResource(hasProperty("name", equalTo("Test location 8"))), + hasResource(hasProperty("name", equalTo("Test location 11"))), + hasResource(hasProperty("name", equalTo("Test location 12"))))); + assertThat(entries, everyItem(hasResource(validResource()))); + } @Test @@ -526,6 +553,30 @@ public void shouldSearchForExistingLocationsAsXML() throws Exception { assertThat(entries, containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), hasResource(hasProperty("name", equalTo("Test location 8"))))); assertThat(entries, everyItem(hasResource(validResource()))); + + // search by ancestors + response = get("/Location?partof:below=" + LOCATION_ANCESTOR_TEST_UUID).accept(FhirMediaTypes.XML).go(); + + assertThat(response, isOk()); + assertThat(response.getContentType(), is(FhirMediaTypes.XML.toString())); + assertThat(response.getContentAsString(), notNullValue()); + + results = readBundleResponse(response); + + assertThat(results, notNullValue()); + assertThat(results.getType(), equalTo(Bundle.BundleType.SEARCHSET)); + assertThat(results.hasEntry(), is(true)); + + entries = results.getEntry(); + assertThat(entries.size(), is(4)); + + assertThat(entries, + containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), + hasResource(hasProperty("name", equalTo("Test location 8"))), + hasResource(hasProperty("name", equalTo("Test location 11"))), + hasResource(hasProperty("name", equalTo("Test location 12"))))); + assertThat(entries, everyItem(hasResource(validResource()))); + } @Test @@ -540,7 +591,7 @@ public void shouldReturnCountForLocationAsJson() throws Exception { assertThat(result, notNullValue()); assertThat(result.getType(), equalTo(Bundle.BundleType.SEARCHSET)); - assertThat(result, hasProperty("total", equalTo(9))); + assertThat(result, hasProperty("total", equalTo(11))); } @Test @@ -555,7 +606,7 @@ public void shouldReturnCountForLocationAsXml() throws Exception { assertThat(result, notNullValue()); assertThat(result.getType(), equalTo(Bundle.BundleType.SEARCHSET)); - assertThat(result, hasProperty("total", equalTo(9))); + assertThat(result, hasProperty("total", equalTo(11))); } @Test diff --git a/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r4/LocationFhirResourceProviderIntegrationTest.java b/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r4/LocationFhirResourceProviderIntegrationTest.java index 87483be9a..b9e6cd567 100644 --- a/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r4/LocationFhirResourceProviderIntegrationTest.java +++ b/integration-tests/src/test/java/org/openmrs/module/fhir2/providers/r4/LocationFhirResourceProviderIntegrationTest.java @@ -48,6 +48,8 @@ public class LocationFhirResourceProviderIntegrationTest extends BaseFhirR4Integ private static final String PARENT_LOCATION_UUID = "76cd2d30-2411-44ef-84ea-8b7473256a6a"; + private static final String LOCATION_ANCESTOR_TEST_UUID = "76cd2d30-2411-44ef-84ea-8b7473256a6a"; + private static final String JSON_CREATE_LOCATION_DOCUMENT = "org/openmrs/module/fhir2/providers/LocationWebTest_create.json"; private static final String XML_CREATE_LOCATION_DOCUMENT = "org/openmrs/module/fhir2/providers/LocationWebTest_create.xml"; @@ -553,6 +555,7 @@ public void shouldSearchForExistingLocationsAsJson() throws Exception { assertThat(entries, everyItem(hasResource(instanceOf(Location.class)))); assertThat(entries, everyItem(hasResource(validResource()))); + // search by address and parent location response = get("/Location?address-city=Kerio&partof=" + PARENT_LOCATION_UUID + "&_sort=name") .accept(FhirMediaTypes.JSON).go(); @@ -574,6 +577,30 @@ public void shouldSearchForExistingLocationsAsJson() throws Exception { assertThat(entries, containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), hasResource(hasProperty("name", equalTo("Test location 8"))))); assertThat(entries, everyItem(hasResource(validResource()))); + + // search by ancestors + response = get("/Location?partof:below=" + LOCATION_ANCESTOR_TEST_UUID).accept(FhirMediaTypes.JSON).go(); + + assertThat(response, isOk()); + assertThat(response.getContentType(), is(FhirMediaTypes.JSON.toString())); + assertThat(response.getContentAsString(), notNullValue()); + + results = readBundleResponse(response); + + assertThat(results, notNullValue()); + assertThat(results.getType(), equalTo(Bundle.BundleType.SEARCHSET)); + assertThat(results.hasEntry(), is(true)); + + entries = results.getEntry(); + assertThat(entries.size(), is(4)); + + assertThat(entries, + containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), + hasResource(hasProperty("name", equalTo("Test location 8"))), + hasResource(hasProperty("name", equalTo("Test location 11"))), + hasResource(hasProperty("name", equalTo("Test location 12"))))); + assertThat(entries, everyItem(hasResource(validResource()))); + } @Test @@ -617,6 +644,30 @@ public void shouldSearchForExistingLocationsAsXML() throws Exception { assertThat(entries, containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), hasResource(hasProperty("name", equalTo("Test location 8"))))); assertThat(entries, everyItem(hasResource(validResource()))); + + // search by ancestors + response = get("/Location?partof:below=" + LOCATION_ANCESTOR_TEST_UUID).accept(FhirMediaTypes.XML).go(); + + assertThat(response, isOk()); + assertThat(response.getContentType(), is(FhirMediaTypes.XML.toString())); + assertThat(response.getContentAsString(), notNullValue()); + + results = readBundleResponse(response); + + assertThat(results, notNullValue()); + assertThat(results.getType(), equalTo(Bundle.BundleType.SEARCHSET)); + assertThat(results.hasEntry(), is(true)); + + entries = results.getEntry(); + assertThat(entries.size(), is(4)); + + assertThat(entries, + containsInRelativeOrder(hasResource(hasProperty("name", equalTo("Test location 6"))), + hasResource(hasProperty("name", equalTo("Test location 8"))), + hasResource(hasProperty("name", equalTo("Test location 11"))), + hasResource(hasProperty("name", equalTo("Test location 12"))))); + assertThat(entries, everyItem(hasResource(validResource()))); + } @Test @@ -631,7 +682,7 @@ public void shouldReturnCountForLocationAsJson() throws Exception { assertThat(result, notNullValue()); assertThat(result.getType(), equalTo(Bundle.BundleType.SEARCHSET)); - assertThat(result, hasProperty("total", equalTo(9))); + assertThat(result, hasProperty("total", equalTo(11))); } @Test @@ -646,7 +697,7 @@ public void shouldReturnCountForLocationAsXml() throws Exception { assertThat(result, notNullValue()); assertThat(result.getType(), equalTo(Bundle.BundleType.SEARCHSET)); - assertThat(result, hasProperty("total", equalTo(9))); + assertThat(result, hasProperty("total", equalTo(11))); } @Test diff --git a/omod/src/main/resources/config.xml b/omod/src/main/resources/config.xml index de01d74a7..1bb44e26b 100644 --- a/omod/src/main/resources/config.xml +++ b/omod/src/main/resources/config.xml @@ -175,4 +175,10 @@ The UUID for the Location Attribute Type representing the Location Type + + ${project.parent.artifactId}.supportedLocationHierarchySearchDepth + Defines how many levels of searching to before when using a Location?partof:below={uuid} search. Generally set this number to the number of levels in your location hierarchy. + 5 + + diff --git a/test-data/src/main/resources/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImplTest_initial_data.xml b/test-data/src/main/resources/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImplTest_initial_data.xml index d8b0157d0..fc2167a08 100644 --- a/test-data/src/main/resources/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImplTest_initial_data.xml +++ b/test-data/src/main/resources/org/openmrs/module/fhir2/api/dao/impl/FhirLocationDaoImplTest_initial_data.xml @@ -12,12 +12,14 @@ - + + +