Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

FM2-653 #556

Merged
merged 12 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -38,6 +43,8 @@
@Setter(AccessLevel.PACKAGE)
public class FhirLocationDaoImpl extends BaseFhirDao<Location> implements FhirLocationDao {

private static final int SUPPORTED_LOCATION_HIERARCHY_SEARCH_DEPTH = 9;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically, I use a succession of joins to go back up the ancestor tree. A bit annoying/hacky, but from a practical matter, seems unlikely there would ever be a hierarchy of more than 9 levels. We could make this a configurable parameter if we wanted.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we turn this into a GP? That way an implementation can adjust it to the actual depth of their location hierarchy.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, I will do that (and since it's configurable, set a more normal default of like 5)


@Autowired
LocationService locationService;

Expand All @@ -62,7 +69,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()));
Expand Down Expand Up @@ -112,8 +119,56 @@ 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) {
Copy link
Member Author

@mogoodrich mogoodrich Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All this through 148 is basically a series null / size checks to make sure that ReferenceAnd list only contains a single reference. We probably should change this parameter to just a ReferenceParam but I didn't want to untangle the locationReference utility method at this point.

Basically, the "partof:below" functionality I built currently only works correctly for a single parameter (and I don't have the need to build more now). Technically the plain "partof" works for multiple "partof", but would always return null since a location partof is 0..1

return;
}

List<ReferenceOrListParam> locationOrReference = locationAndReferences.getValuesAsQueryTokens();

if (locationOrReference == null || locationOrReference.isEmpty()) {
return;
}

if (locationOrReference.size() > 1) {
throw new IllegalArgumentException("Only one location reference is supported");
}

List<ReferenceParam> 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())) {
List<Criterion> 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 <= SUPPORTED_LOCATION_HIERARCHY_SEARCH_DEPTH) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a for loop?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean aesthetically, or is there a bug here I'm missing? :) For some reason I tend to prefer while loops.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just aesthetics. It looks like it works.

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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,8 @@ public class LocationSearchQueryTest extends BaseModuleContextSensitiveTest {

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";
Expand Down Expand Up @@ -752,6 +754,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<Location> resultList = get(locations);

assertThat(resultList, hasSize(equalTo(4)));
List<String> 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<Location> getLocationListWithoutNulls(SortSpec sort) {
SearchParameterMap theParams = new SearchParameterMap().setSortSpec(sort);
IBundleProvider locations = search(theParams);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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();

Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@
<location location_id="1" name="Test location 1" description="Test description" date_created="2005-01-01 00:00:00.0" retired="false" uuid="c0938432-1691-11df-97a5-7038c432"/>
<location location_id="2" name="Test location 2" description="Test description" date_created="2005-01-01 00:00:00.0" retired="false" uuid="7324f639-85e8-484e-b90e-d5f68aa9259c"/>
<location location_id="3" name="Test location 3" description="Test description" date_created="2005-01-01 00:00:00.0" retired="false" uuid="79188353-f8ab-4042-a87f-e27bc819a14c"/>
<location location_id="4" name="Test location 4" description="Test description" date_created="2005-01-01 00:00:00.0" retired="false" uuid="76cd2d30-2411-44ef-84ea-8b7473256a6a"/>
<location location_id="4" name="Test location 4" description="Test description" city_village="Scituate" country="USA" postal_code="02066" state_province="MA" date_created="2005-01-01 00:00:00.0" retired="false" uuid="76cd2d30-2411-44ef-84ea-8b7473256a6a"/>
<location location_id="5" name="Test location 5" description="Test description" city_village="Artuor" country="Kenya" postal_code="4069-3100" state_province="province" date_created="2005-01-01 00:00:00.0" retired="false" uuid="dc80983f-8b2c-4524-bdbd-b5e825e263a0"/>
<location location_id="6" name="Test location 6" description="Test description" city_village="Kerio" country="Kenya" parent_location="4" postal_code="4069-3100" state_province="province" date_created="2005-01-01 00:00:00.0" retired="false" uuid="396ad089-789a-4290-be8c-9cd88909f1fa"/>
<location location_id="7" name="Test location 7" description="Test description" city_village="Kerio" country="India" parent_location="5" date_created="2005-01-01 00:00:00.0" retired="false" uuid="5db6ae3c-867e-45a0-a1ce-f86219b64e1c"/>
<location location_id="8" name="Test location 8" description="Test description" city_village="Kerio" country="India" parent_location="4" date_changed="2010-03-31 00:00:00.0" date_created="2005-01-01 00:00:00.0" retired="false" uuid="cb82ab6d-9db2-451f-9ce2-0afc2b16eb13"/>
<location location_id="9" name="Test location 9" description="Test description" city_village="Kerio" country="India" parent_location="4" date_changed="2010-03-31 00:00:00.0" date_retired="2010-09-03 00:00:00.0" date_created="2005-01-01 00:00:00.0" retired="true" uuid="689f6212-95da-466b-aac7-acdef0689fef"/>
<location location_id="11" name="Test location 11" description="Test description" city_village="Kerio" country="India" parent_location="8" date_changed="2010-05-01 00:00:00.0" date_created="2005-01-01 00:00:00.0" retired="false" uuid="e7b8a6d4-3b2a-4f8b-8b6e-1f8b8e6e1f8b"/>
<location location_id="12" name="Test location 12" description="Test description" city_village="Kerio" country="India" parent_location="11" date_changed="2010-05-01 00:00:00.0" date_created="2005-01-01 00:00:00.0" retired="false" uuid="f47ac10b-58cc-4372-a567-0e02b2c3d479"/>
<location_tag location_tag_id="7" name="login" description="Identify login locations" creator="1" date_created="2005-01-01 00:00:00.0" retired="false" uuid="89f4f6f9-d0e7-4ff9-b8d1-994d69f035db"/>
<location_tag location_tag_id="8" name="test" description="Identify test locations" creator="1" date_created="2005-01-01 00:00:00.0" retired="false" uuid="c04a4bbb-0843-4a39-9d40-c105e05f0aac"/>
<location_tag_map location_id="2" location_tag_id="7"/>
Expand Down
Loading