Skip to content

Commit

Permalink
Merge pull request #118 from onaio/linear-optimization
Browse files Browse the repository at this point in the history
Location Hierarchy endpoint lineage ids enhancement and refactor ✨
  • Loading branch information
ndegwamartin authored Feb 5, 2025
2 parents 5eac4bd + 28c1b36 commit 6fe8901
Show file tree
Hide file tree
Showing 12 changed files with 525 additions and 62 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
target/
.idea/
.env
.run/
34 changes: 32 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -404,7 +404,7 @@ Example:
[GET] /LocationHierarchy?_id=<some-location-id>&administrativeLevelMin=2&administrativeLevelMax=4&_count=<page-size>&_page=<page-number>&_sort=<some-sort>
```

##### Inventory Filters
##### LocationHierarchy Inventory Filters

The `LocationHierarchy` endpoint supports filtering by inventory availability,
allowing users to specify whether they want to retrieve only locations that have
Expand All @@ -429,7 +429,7 @@ Example:
[GET] /LocationHierarchy?_id=<some-location-id>&filterInventory=true&_count=<page-size>&_page=<page-number>&_sort=<some-sort>
```

##### LastUpdated Filters
##### LocationHierarchy LastUpdated Filters

The `LocationHierarchy` endpoint supports filtering by the lastUpdated timestamp
of locations. This filter allows users to retrieve locations based on the last
Expand Down Expand Up @@ -465,6 +465,36 @@ Example:
GET /LocationHierarchy?_id=<some-location-id>&mode=list&_summary=count
```

##### LocationHierarchy Filter By Lineage Ids

The `LocationHierarchy` endpoint supports filtering by the lineage ids of
locations. This filter allows users to retrieve locations based on the location
ids of all the ancestors of the location. This makes fetching descendants of
locations highly efficient. The following search parameter is available:

- `filter_mode_lineage`: A boolean parameter that specifies whether the response
should be filtered using location lineage ids.

Behavior based on the lastUpdated parameter:

- `filter_mode_lineage` Not Defined or **false**: The endpoint will include all
locations as before (without the feature added)
- `filter_mode_lineage` Defined and **true** or _missing value_: The response
will
- include only those locations whose parent location passed is in the ancestry
of the location.

Note: This filter only works when in list mode i.e `mode=list` is set as one of
the parameters. Also note, enabling this flag requires that you populate all
relevant Location resources on your server with the location ids of all their
ancestors. See https://github.com/onaio/fhir-gateway-extension/issues/110

Example:

```
[GET] /LocationHierarchy?filter_mode_lineage=true&_syncLocations=<some-location-id>,<some-location-id>,<some-location-id>
```

#### Important Note:

Developers, please update your client applications accordingly to accommodate
Expand Down
4 changes: 2 additions & 2 deletions exec/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.smartregister</groupId>
<artifactId>opensrp-gateway-plugin</artifactId>
<version>2.2.7</version>
<version>3.0.0</version>
</parent>

<artifactId>exec</artifactId>
Expand Down Expand Up @@ -70,7 +70,7 @@
<dependency>
<groupId>org.smartregister</groupId>
<artifactId>plugins</artifactId>
<version>2.2.7</version>
<version>3.0.0</version>
</dependency>

<dependency>
Expand Down
2 changes: 1 addition & 1 deletion plugins/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
<parent>
<groupId>org.smartregister</groupId>
<artifactId>opensrp-gateway-plugin</artifactId>
<version>2.2.7</version>
<version>3.0.0</version>
</parent>

<artifactId>plugins</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public class Constants {
public static final String CORS_ALLOW_ORIGIN_ENV = "CORS_ALLOW_ORIGIN";
public static final String UNDERSCORE = "_";
public static final String[] CLIENT_ROLES = {ROLE_WEB_CLIENT, ROLE_ANDROID_CLIENT};
public static final String FILTER_MODE_LINEAGE = "filter_mode_lineage";

public interface Literals {
String EQUALS = "=";
Expand All @@ -74,4 +75,11 @@ public interface Header {
@Deprecated String FHIR_GATEWAY_MODE = "fhir-gateway-mode";
String MODE = "mode";
}

public interface Meta {
interface Tag {
String SYSTEM_LOCATION_HIERARCHY =
"http://smartregister.org/CodeSystem/location-lineage";
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import java.util.Map;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
Expand Down Expand Up @@ -126,13 +127,12 @@ public LocationHierarchy getLocationHierarchyCore(

public List<Location> getLocationHierarchyLocations(
String locationId,
Location parentLocation,
List<String> preFetchAdminLevels,
List<String> postFetchAdminLevels,
Boolean filterInventory,
String lastUpdated) {
List<Location> descendants;

Location parentLocation = getLocationById(locationId);
if (CacheHelper.INSTANCE.skipCache()) {
descendants = getDescendants(locationId, parentLocation, preFetchAdminLevels);
} else {
Expand Down Expand Up @@ -216,28 +216,39 @@ public List<Location> getDescendants(
return location;
}

public @Nullable Bundle getLocationById(List<String> ids) {
return getFhirClientForR4()
.fetchResourceFromUrl(Bundle.class, "Location?_id=" + StringUtils.join(ids, ","));
}

public Bundle handleIdentifierRequest(HttpServletRequest request, String identifier) {
String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL);
String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL);
String mode = request.getParameter(Constants.MODE);
Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY));
String lastUpdated = "";
boolean filterModeLineage =
request.getParameterMap().containsKey(Constants.FILTER_MODE_LINEAGE)
&& (StringUtils.isBlank(request.getParameter(Constants.FILTER_MODE_LINEAGE))
|| Boolean.parseBoolean(
request.getParameter(Constants.FILTER_MODE_LINEAGE)));
List<String> preFetchAdminLevels =
generateAdminLevels(
String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax);
List<String> postFetchAdminLevels =
generateAdminLevels(administrativeLevelMin, administrativeLevelMax);
if (Constants.LIST.equals(mode)) {
List<String> locationIds = Collections.singletonList(identifier);
return getPaginatedLocations(request, locationIds);
return filterModeLineage
? getPaginatedLocations(request, locationIds)
: getPaginatedLocationsBackwardCompatibility(request, locationIds);
} else {
LocationHierarchy locationHierarchy =
getLocationHierarchy(
identifier,
preFetchAdminLevels,
postFetchAdminLevels,
filterInventory,
lastUpdated);
null);
return Utils.createBundle(Collections.singletonList(locationHierarchy));
}
}
Expand All @@ -251,6 +262,11 @@ public Bundle handleNonIdentifierRequest(
String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL);
String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL);
Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY));
boolean filterModeLineage =
request.getParameterMap().containsKey(Constants.FILTER_MODE_LINEAGE)
&& (StringUtils.isBlank(request.getParameter(Constants.FILTER_MODE_LINEAGE))
|| Boolean.parseBoolean(
request.getParameter(Constants.FILTER_MODE_LINEAGE)));
List<String> preFetchAdminLevels =
generateAdminLevels(
String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax);
Expand All @@ -261,19 +277,23 @@ public Bundle handleNonIdentifierRequest(
List<String> userRoles = JwtUtils.getUserRolesFromJWT(verifiedJwt);
String applicationId = JwtUtils.getApplicationIdFromJWT(verifiedJwt);
String syncStrategy = getSyncStrategyByAppId(applicationId);
String lastUpdated = "";

if (Constants.LIST.equals(mode)) {
if (Constants.SyncStrategy.RELATED_ENTITY_LOCATION.equalsIgnoreCase(syncStrategy)
&& userRoles.contains(Constants.ROLE_ALL_LOCATIONS)
&& !selectedSyncLocations.isEmpty()) {
return getPaginatedLocations(request, selectedSyncLocations);

return filterModeLineage
? getPaginatedLocations(request, selectedSyncLocations)
: getPaginatedLocationsBackwardCompatibility(
request, selectedSyncLocations);
} else {
List<String> locationIds =
practitionerDetailsEndpointHelper.getPractitionerLocationIdsByByKeycloakId(
practitionerId);
return getPaginatedLocations(request, locationIds);
return filterModeLineage
? getPaginatedLocations(request, locationIds)
: getPaginatedLocationsBackwardCompatibility(
request, selectedSyncLocations);
}

} else {
Expand All @@ -286,7 +306,7 @@ public Bundle handleNonIdentifierRequest(
preFetchAdminLevels,
postFetchAdminLevels,
filterInventory,
lastUpdated);
null);
List<Resource> resourceList =
locationHierarchies != null
? locationHierarchies.stream()
Expand All @@ -304,7 +324,7 @@ public Bundle handleNonIdentifierRequest(
preFetchAdminLevels,
postFetchAdminLevels,
filterInventory,
lastUpdated);
null);
List<Resource> resourceList =
locationHierarchies != null
? locationHierarchies.stream()
Expand Down Expand Up @@ -387,13 +407,90 @@ public Bundle getPaginatedLocations(HttpServletRequest request, List<String> loc

int start = Math.max(0, (page - 1)) * count;

List<Location> resourceLocations =
locationIds.stream()
.map(locationId -> fetchAllDescendants(locationId, preFetchAdminLevels))
.flatMap(descendant -> descendant.getEntry().stream())
.map(bundleEntryComponent -> (Location) bundleEntryComponent.getResource())
.collect(Collectors.toList());

// Get the parents
Bundle parentLocation = getLocationById(locationIds);
if (parentLocation != null) {
List<Bundle.BundleEntryComponent> locationBundleEntryComponents =
parentLocation.getEntry();
for (Bundle.BundleEntryComponent locationBundleEntryComponent :
locationBundleEntryComponents) {
resourceLocations.add((Location) locationBundleEntryComponent.getResource());
}
}

// Apply the post filter
resourceLocations =
postFetchFilters(
resourceLocations, postFetchAdminLevels, filterInventory, lastUpdated);

int totalEntries = resourceLocations.size();

int end = Math.min(start + count, resourceLocations.size());
List<Location> paginatedResourceLocations = resourceLocations.subList(start, end);
Bundle resultBundle;
if (Constants.COUNT.equals(summary)) {
resultBundle =
Utils.createEmptyBundle(
request.getRequestURL() + "?" + request.getQueryString());
resultBundle.setTotal(totalEntries);
return resultBundle;
}

if (resourceLocations.isEmpty()) {
resultBundle =
Utils.createEmptyBundle(
request.getRequestURL() + "?" + request.getQueryString());
} else {
resultBundle = Utils.createBundle(paginatedResourceLocations);
StringBuilder urlBuilder = new StringBuilder(request.getRequestURL());
Utils.addPaginationLinks(
urlBuilder, resultBundle, page, totalEntries, count, parameters);
}

return resultBundle;
}

@Deprecated(since = "3.0.0", forRemoval = true)
public Bundle getPaginatedLocationsBackwardCompatibility(
HttpServletRequest request, List<String> locationIds) {
String pageSize = request.getParameter(Constants.PAGINATION_PAGE_SIZE);
String pageNumber = request.getParameter(Constants.PAGINATION_PAGE_NUMBER);
String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL);
String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL);
Boolean filterInventory = Boolean.valueOf(request.getParameter(Constants.FILTER_INVENTORY));
String lastUpdated = request.getParameter(Constants.LAST_UPDATED);
String summary = request.getParameter(Constants.SUMMARY);
List<String> preFetchAdminLevels =
generateAdminLevels(
String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax);
List<String> postFetchAdminLevels =
generateAdminLevels(administrativeLevelMin, administrativeLevelMax);
Map<String, String[]> parameters = new HashMap<>(request.getParameterMap());

int count =
pageSize != null
? Integer.parseInt(pageSize)
: Constants.PAGINATION_DEFAULT_PAGE_SIZE;
int page =
pageNumber != null
? Integer.parseInt(pageNumber)
: Constants.PAGINATION_DEFAULT_PAGE_NUMBER;

int start = Math.max(0, (page - 1)) * count;

List<Resource> resourceLocations =
locationIds.parallelStream()
.flatMap(
identifier ->
getLocationHierarchyLocations(
identifier,
getLocationById(identifier),
preFetchAdminLevels,
postFetchAdminLevels,
filterInventory,
Expand Down Expand Up @@ -427,6 +524,31 @@ public Bundle getPaginatedLocations(HttpServletRequest request, List<String> loc
return resultBundle;
}

public Bundle fetchAllDescendants(String locationId, List<String> preFetchAdminLevels) {
StringBuilder queryStringFilter = new StringBuilder("Location?");
if (StringUtils.isNotBlank(locationId)) {
queryStringFilter
.append("&_tag=")
.append(Constants.Meta.Tag.SYSTEM_LOCATION_HIERARCHY)
.append("%7C")
.append(locationId)
.append(',');
}

if (preFetchAdminLevels != null && !preFetchAdminLevels.isEmpty()) {
queryStringFilter.append("&type=");
for (String adminLevel : preFetchAdminLevels) {
queryStringFilter
.append(Constants.DEFAULT_ADMIN_LEVEL_TYPE_URL)
.append("%7C")
.append(adminLevel)
.append(',');
}
}

return (Bundle) getFhirClientForR4().search().byUrl(queryStringFilter.toString()).execute();
}

public List<String> generateAdminLevels(
String administrativeLevelMin, String administrativeLevelMax) {
List<String> adminLevels = new ArrayList<>();
Expand Down Expand Up @@ -496,9 +618,9 @@ public boolean adminLevelFilter(Location location, List<String> postFetchAdminLe
}

public boolean lastUpdatedFilter(Location location, String lastUpdated) {
Date locationlastUpdated = location.getMeta().getLastUpdated();
Date metaLocationLastUpdated = location.getMeta().getLastUpdated();
OffsetDateTime locationLastUpdated =
locationlastUpdated.toInstant().atOffset(ZoneOffset.UTC);
metaLocationLastUpdated.toInstant().atOffset(ZoneOffset.UTC);
return locationLastUpdated.isAfter(OffsetDateTime.parse(lastUpdated))
|| locationLastUpdated.isEqual(OffsetDateTime.parse(lastUpdated));
}
Expand Down
Loading

0 comments on commit 6fe8901

Please sign in to comment.