Feature/chained location.near search (#5668)

* Added support for chained location search

* Added unit tests

* Assigned new error code

* Reworked according to review
This commit is contained in:
Jens Kristian Villadsen 2024-02-12 19:47:50 +01:00 committed by GitHub
parent 12eb2d6f35
commit 1deca0756c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 338 additions and 3 deletions

View File

@ -296,7 +296,8 @@ public class QueryStack {
String theReferenceTargetType,
String theParamName,
String theChain,
boolean theAscending) {
boolean theAscending,
SearchParameterMap theParams) {
BaseJoiningPredicateBuilder firstPredicateBuilder = mySqlBuilder.getOrCreateFirstPredicateBuilder();
ResourceLinkPredicateBuilder resourceLinkPredicateBuilder = mySqlBuilder.createReferencePredicateBuilder(this);
@ -378,13 +379,31 @@ public class QueryStack {
* sort on a target that was a reference or a quantity, but if someone needed
* that we could implement it here.
*/
case SPECIAL: {
if (LOCATION_POSITION.equals(targetSearchParameter.getPath())) {
List<List<IQueryParameterType>> params = theParams.get(theParamName);
if (params != null && !params.isEmpty() && !params.get(0).isEmpty()) {
IQueryParameterType locationParam = params.get(0).get(0);
final SpecialParam specialParam = new SpecialParam().setValue(locationParam.getValueAsQueryToken(myFhirContext));
ParsedLocationParam location = ParsedLocationParam.from(theParams, specialParam);
double latitudeValue = location.getLatitudeValue();
double longitudeValue = location.getLongitudeValue();
final CoordsPredicateBuilder coordsPredicateBuilder = mySqlBuilder.addCoordsPredicateBuilder(resourceLinkPredicateBuilder.getColumnTargetResourceId());
mySqlBuilder.addSortCoordsNear(coordsPredicateBuilder, latitudeValue, longitudeValue, theAscending);
} else {
String msg = myFhirContext.getLocalizer().getMessageSanitized(QueryStack.class, "cantSortOnCoordParamWithoutValues", theParamName);
throw new InvalidRequestException(Msg.code(2497) + msg);
}
return;
}
}
case NUMBER:
case REFERENCE:
case COMPOSITE:
case QUANTITY:
case URI:
case HAS:
case SPECIAL:
default:
throw new InvalidRequestException(Msg.code(2290) + "Unable to sort on a chained parameter "
+ theParamName + "." + theChain + " as this parameter. Can not sort on chains of target type: "

View File

@ -932,7 +932,7 @@ public class SearchBuilder implements ISearchBuilder<JpaPid> {
break;
case REFERENCE:
theQueryStack.addSortOnResourceLink(
myResourceName, referenceTargetType, paramName, chainName, ascending);
myResourceName, referenceTargetType, paramName, chainName, ascending, theParams);
break;
case TOKEN:
theQueryStack.addSortOnToken(myResourceName, paramName, ascending);

View File

@ -1,14 +1,24 @@
package ca.uhn.fhir.jpa.provider.r4;
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.jpa.api.dao.IFhirResourceDao;
import ca.uhn.fhir.jpa.provider.BaseResourceProviderR4Test;
import ca.uhn.fhir.jpa.search.builder.QueryStack;
import ca.uhn.fhir.jpa.util.CoordCalculatorTestUtil;
import ca.uhn.fhir.rest.api.SortOrderEnum;
import ca.uhn.fhir.rest.api.SortSpec;
import ca.uhn.fhir.rest.gclient.ICriterion;
import ca.uhn.fhir.rest.gclient.ICriterionInternal;
import ca.uhn.fhir.rest.gclient.IParam;
import ca.uhn.fhir.rest.server.exceptions.InvalidRequestException;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.instance.model.api.IIdType;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Enumerations;
import org.hl7.fhir.r4.model.Identifier;
import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.PractitionerRole;
import org.hl7.fhir.r4.model.Reference;
import org.hl7.fhir.r4.model.SearchParameter;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
@ -16,12 +26,19 @@ import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.junit.jupiter.params.provider.ValueSource;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsString;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.fail;
public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test {
@ -345,6 +362,242 @@ public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test {
}
}
@Test
void shouldSortPractitionerRolesByLocationNear() {
double latitude = 43.65513;
double longitude = -79.4173869;
double distance = 100.0; // km
Location toronto = createLocationWithName("Toronto", 43.70, -79.42);
Location mississauga = createLocationWithName("Mississauga", 43.59, -79.64);
Location hamilton = createLocationWithName("Hamilton", 43.26, -79.87);
Location kitchener = createLocationWithName("Kitchener", 43.45, -80.49);
Location stCatharines = createLocationWithName("St. Catharines", 43.16, -79.24);
Location oshawa = createLocationWithName("Oshawa", 43.92, -78.86);
Location ottawa = createLocationWithName("Ottawa", 45.42, -75.69);
Location london = createLocationWithName("London", 42.98, -81.25);
Location barrie = createLocationWithName("Barrie", 44.39, -79.69);
Location windsor = createLocationWithName("Windsor", 42.31, -83.04);
createPractitionerRole(toronto);
createPractitionerRole(ottawa);
createPractitionerRole(mississauga);
createPractitionerRole(hamilton);
createPractitionerRole(kitchener);
createPractitionerRole(london);
createPractitionerRole(stCatharines);
createPractitionerRole(oshawa);
createPractitionerRole(windsor);
createPractitionerRole(barrie);
Bundle sortedPractitionerRoles = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.near", SortOrderEnum.ASC))
.execute();
List<String> list = sortedPractitionerRoles.getEntry()
.stream()
.map(entry -> (PractitionerRole) entry.getResource())
.flatMap(practitionerRole -> practitionerRole.getIdentifier().stream())
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue).toList();
List<String> referenceList = Arrays.asList(
"PractitionerRole-Toronto",
"PractitionerRole-Mississauga",
"PractitionerRole-St. Catharines",
"PractitionerRole-Oshawa",
"PractitionerRole-Hamilton",
"PractitionerRole-Barrie",
"PractitionerRole-Kitchener");
assertArrayEquals(referenceList.toArray(), list.toArray());
Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.near", SortOrderEnum.DESC))
.execute();
list = sortedPractitionerRolesDesc.getEntry()
.stream()
.map(entry -> (PractitionerRole) entry.getResource())
.flatMap(practitionerRole -> practitionerRole.getIdentifier().stream())
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue).toList();
Collections.reverse(referenceList);
assertArrayEquals(referenceList.toArray(), list.toArray());
}
@Test
void shouldSortPractitionerRoleByLocationNameAndThenByLocationNearInSortChain() {
double latitude = 56.15392798473292;
double longitude = 10.214247324883443;
double distance = 1000.0; // km
Location city1 = createLocationWithName("city", 56.4572068307235, 10.03257493847164); // randers
Location city2 = createLocationWithName("city", 55.37805615936569, 10.373173394141986); // odense
Location city3 = createLocationWithName("city", 57.03839389334237, 9.897971178848938); // aalborg
Location city4 = createLocationWithName("city", 53.59504499156986, 9.94180650612504); // hamburg
Location capital1 = createLocationWithName("capital", 55.67252420131149, 12.521336649310285); // copenhagen
Location capital2 = createLocationWithName("capital", 59.91879265293977, 10.743073107764332); // oslo
Location capital3 = createLocationWithName("capital", 51.53542091927589, -0.1535161240530497); // london
createPractitionerRole(city1, "city1");
createPractitionerRole(city2, "city2");
createPractitionerRole(city3, "city3");
createPractitionerRole(city4, "city4");
createPractitionerRole(capital1, "capital1");
createPractitionerRole(capital2, "capital2");
createPractitionerRole(capital3, "capital3");
Bundle sortedPractitionerRoles = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.name", SortOrderEnum.ASC, new SortSpec("location.near", SortOrderEnum.ASC)))
.execute();
List<String> sortedValues = sortedPractitionerRoles
.getEntry()
.stream()
.map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier()
.stream()
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue)
.collect(Collectors.toList()))
.flatMap(List::stream)
.toList();
List<String> referenceList = Arrays.asList(
"PractitionerRole-capital1",
"PractitionerRole-capital2",
"PractitionerRole-capital3",
"PractitionerRole-city1",
"PractitionerRole-city2",
"PractitionerRole-city3",
"PractitionerRole-city4");
assertArrayEquals(referenceList.toArray(), sortedValues.toArray());
Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.name", SortOrderEnum.ASC, new SortSpec("location.near", SortOrderEnum.DESC)))
.execute();
List<String> sortedValuesDesc = sortedPractitionerRolesDesc
.getEntry()
.stream()
.map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier()
.stream()
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue)
.collect(Collectors.toList()))
.flatMap(List::stream)
.toList();
referenceList = Arrays.asList(
"PractitionerRole-capital3",
"PractitionerRole-capital2",
"PractitionerRole-capital1",
"PractitionerRole-city4",
"PractitionerRole-city3",
"PractitionerRole-city2",
"PractitionerRole-city1"
);
assertArrayEquals(referenceList.toArray(), sortedValuesDesc.toArray());
}
@Test
void shouldSortPractitionerRoleByLocationNearAndThenByLocationNameInSortChain() {
double latitude = 56.15392798473292;
double longitude = 10.214247324883443;
double distance = 1000.0; // km
createPractitionerRole(createLocationWithName("a-close-city", 56.4572068307235, 10.03257493847164));
createPractitionerRole(createLocationWithName("b-close-city", 56.4572068307235, 10.03257493847164));
createPractitionerRole(createLocationWithName("c-close-city", 56.4572068307235, 10.03257493847164));
createPractitionerRole(createLocationWithName("x-far-city", 51.53542091927589, -0.1535161240530497));
createPractitionerRole(createLocationWithName("y-far-city", 51.53542091927589, -0.1535161240530497));
createPractitionerRole(createLocationWithName("z-far-city", 51.53542091927589, -0.1535161240530497));
Bundle sortedPractitionerRoles = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.near", SortOrderEnum.ASC, new SortSpec("location.name")))
.execute();
List<String> sortedValues = sortedPractitionerRoles
.getEntry()
.stream()
.map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier()
.stream()
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue)
.collect(Collectors.toList()))
.flatMap(List::stream)
.toList();
assertArrayEquals(
Arrays.asList(
"PractitionerRole-a-close-city",
"PractitionerRole-b-close-city",
"PractitionerRole-c-close-city",
"PractitionerRole-x-far-city",
"PractitionerRole-y-far-city",
"PractitionerRole-z-far-city"
).toArray(), sortedValues.toArray());
Bundle sortedPractitionerRolesDesc = (Bundle) myClient.search()
.forResource(PractitionerRole.class)
.where(new RawSearchCriterion("location.near", latitude + "|" + longitude + "|" + distance + "|km"))
.sort(new SortSpec("location.near", SortOrderEnum.DESC, new SortSpec("location.name")))
.execute();
List<String> sortedValuesDesc = sortedPractitionerRolesDesc
.getEntry()
.stream()
.map(entry -> ((PractitionerRole) entry.getResource()).getIdentifier()
.stream()
.filter(identifier -> PRACTITIONER_ROLE_SYSTEM.equals(identifier.getSystem()))
.map(Identifier::getValue)
.collect(Collectors.toList()))
.flatMap(List::stream)
.toList();
assertArrayEquals(
Arrays.asList(
"PractitionerRole-x-far-city",
"PractitionerRole-y-far-city",
"PractitionerRole-z-far-city",
"PractitionerRole-a-close-city",
"PractitionerRole-b-close-city",
"PractitionerRole-c-close-city"
).toArray(), sortedValuesDesc.toArray());
}
@Test
void shouldThrowExceptionWhenSortingByChainedNearWithoutProvidingNearValue() {
assertThrows(InvalidRequestException.class, () -> {
myClient.search()
.forResource(PractitionerRole.class)
.sort(new SortSpec("location.near", SortOrderEnum.ASC))
.execute();
}, "HTTP 400 : HAPI-2307: Can not sort on coordinate parameter \"location\" unless this parameter is also specified as a search parameter with a latitude/longitude value");
}
private void createLocation(String id, double latitude, double longitude) {
Location loc = new Location();
loc.setId(id);
@ -354,5 +607,68 @@ public class ResourceProviderR4DistanceTest extends BaseResourceProviderR4Test {
myLocationDao.update(loc, mySrd);
}
public static final String PRACTITIONER_ROLE_SYSTEM = "http://api.someSystem.com/PractitionerRole";
private Location createLocationWithName(String locationName, double latitude, double longitude) {
Location location = new Location();
location.setStatus(Location.LocationStatus.ACTIVE);
location.setName(locationName);
location.addIdentifier()
.setSystem("http://api.someSystem.com/Location")
.setValue("TestLocation-" + locationName + "-" + UUID.randomUUID());
location.getPosition().setLatitude(new BigDecimal(latitude));
location.getPosition().setLongitude(new BigDecimal(longitude));
return doCreateResourceAndReturnInstance(location);
}
private void createPractitionerRole(Location location, String city) {
PractitionerRole practitionerRole = new PractitionerRole();
practitionerRole.setActive(true);
practitionerRole.addLocation(new Reference(location));
practitionerRole.addIdentifier()
.setSystem(PRACTITIONER_ROLE_SYSTEM)
.setValue("PractitionerRole-" + city);
doCreateResourceAndReturnInstance(practitionerRole);
}
private void createPractitionerRole(Location location) {
PractitionerRole practitionerRole = new PractitionerRole();
practitionerRole.setActive(true);
practitionerRole.addLocation(new Reference(location));
practitionerRole.addIdentifier()
.setSystem(PRACTITIONER_ROLE_SYSTEM)
.setValue("PractitionerRole-" + location.getName());
doCreateResourceAndReturnInstance(practitionerRole);
}
public <T extends IBaseResource> T doCreateResourceAndReturnInstance(IBaseResource theResource) {
IFhirResourceDao dao = myDaoRegistry.getResourceDao(theResource.getClass());
return (T) dao.create(theResource, mySrd).getResource();
}
static class RawSearchCriterion implements ICriterion<IParam>, ICriterionInternal {
private final String name;
private final String value;
RawSearchCriterion(String name, String value) {
this.name = name;
this.value = value;
}
@Override
public String getParameterValue(FhirContext theContext) {
return value;
}
@Override
public String getParameterName() {
return name;
}
}
}