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

Location Hierarchy endpoint lineage ids enhancement and refactor ✨ #118

Merged
merged 8 commits into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all 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
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 List<Location> getLocationHierarchyLocations(
String locationId,
Location parentLocation,
List<String> preFetchAdminLevels,
List<String> postFetchAdminLevels,
Boolean filterInventory,
String lastUpdated) {
List<Location> descendants;

Location parentLocation = getLocationById(locationId);

Check warning on line 135 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L135

Added line #L135 was not covered by tests
if (CacheHelper.INSTANCE.skipCache()) {
descendants = getDescendants(locationId, parentLocation, preFetchAdminLevels);
} else {
Expand Down Expand Up @@ -216,28 +216,39 @@
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)));

Check warning on line 233 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L233

Added line #L233 was not covered by tests
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);

Check warning on line 243 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L243

Added line #L243 was not covered by tests
} else {
LocationHierarchy locationHierarchy =
getLocationHierarchy(
identifier,
preFetchAdminLevels,
postFetchAdminLevels,
filterInventory,
lastUpdated);
null);
return Utils.createBundle(Collections.singletonList(locationHierarchy));
}
}
Expand All @@ -251,6 +262,11 @@
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)));

Check warning on line 269 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L269

Added line #L269 was not covered by tests
List<String> preFetchAdminLevels =
generateAdminLevels(
String.valueOf(Constants.DEFAULT_MIN_ADMIN_LEVEL), administrativeLevelMax);
Expand All @@ -261,19 +277,23 @@
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)

Check warning on line 286 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L286

Added line #L286 was not covered by tests
: getPaginatedLocationsBackwardCompatibility(
request, selectedSyncLocations);
} else {
List<String> locationIds =
practitionerDetailsEndpointHelper.getPractitionerLocationIdsByByKeycloakId(
practitionerId);
return getPaginatedLocations(request, locationIds);
return filterModeLineage
? getPaginatedLocations(request, locationIds)
: getPaginatedLocationsBackwardCompatibility(

Check warning on line 295 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L294-L295

Added lines #L294 - L295 were not covered by tests
request, selectedSyncLocations);
}

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

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;

Check warning on line 443 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L439-L443

Added lines #L439 - L443 were not covered by tests
}

if (resourceLocations.isEmpty()) {
resultBundle =
Utils.createEmptyBundle(
request.getRequestURL() + "?" + request.getQueryString());

Check warning on line 449 in plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java

View check run for this annotation

Codecov / codecov/patch

plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java#L447-L449

Added lines #L447 - L449 were not covered by tests
} 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 @@
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 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