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

Build LocationHierarchy Location hierarchy on sync strategy #68

Merged
merged 8 commits into from
Jul 16, 2024
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
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,40 @@ Example:
[GET] /LocationHierarchy?_id=<some-location-id>&mode=list&_count=<page-size>&_page=<page-number>&_sort=<some-sort>
```

##### LocationHierarchy Dynamic Identifier

The `LocationHierarchy` endpoint has the following supported functionalities
when the `_id` is not provided as a parameter.

1. Build location hierarchies of the **_User Assigned Locations_**: The
`LocationHierarchy` endpoint will build location hierarchies of all user
assigned locations

Example:

```
[GET] /LocationHierarchy
```

2. Build location hierarchies of the **_User Selected Locations_**: The
`LocationHierarchy` endpoint will build locations hierarchies of the
locations provided by the user via the `_syncLocations` parameter.

###### Conditions for User Selected LocationsHierarchies

- The deployment/app user should have Related Entity Location as its sync
strategy
- The deployment/app user should have `ALL_LOCATIONS` role on keycloak.
- The request should have `_syncLocations` parameter set

Example:

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

All other valid parameters can be used on this endpoint.

##### LocationHierarchy Administrative Level Filters

The LocationHierarchy endpoint supports filtering by administrative levels. This
Expand All @@ -356,6 +390,8 @@ following search parameters are available:
include in the response. Locations at this level and above will be included.
- `administrativeLevelMax`: Specifies the maximum administrative level to
include in the response. Locations at this level and below will be included.
If not set, it defaults to the value of `DEFAULT_MAX_ADMIN_LEVEL` set in the
`Constants.java` file.

Behavior based on parameters:

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.0.0</version>
<version>2.0.1</version>
</parent>

<artifactId>exec</artifactId>
Expand Down Expand Up @@ -70,7 +70,7 @@
<dependency>
<groupId>org.smartregister</groupId>
<artifactId>plugins</artifactId>
<version>2.0.0</version>
<version>2.0.1</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.0.0</version>
<version>2.0.1</version>
</parent>

<artifactId>plugins</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ public enum CacheHelper {

Cache<String, List<Location>> locationListCache;

Cache<String, String> stringCache;

CacheHelper() {
cache =
Caffeine.newBuilder()
Expand All @@ -35,6 +37,11 @@ public enum CacheHelper {
.expireAfterWrite(getCacheExpiryDurationInSeconds(), TimeUnit.SECONDS)
.maximumSize(DEFAULT_CACHE_SIZE)
.build();
stringCache =
Caffeine.newBuilder()
.expireAfterWrite(getCacheExpiryDurationInSeconds(), TimeUnit.SECONDS)
.maximumSize(DEFAULT_CACHE_SIZE)
.build();
}

private int getCacheExpiryDurationInSeconds() {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package org.smartregister.fhir.gateway.plugins;

import java.util.List;
import java.util.Map;

import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.common.annotations.VisibleForTesting;

public class JwtUtils {
@VisibleForTesting static final String REALM_ACCESS_CLAIM = "realm_access";
@VisibleForTesting static final String ROLES = "roles";
@VisibleForTesting static final String FHIR_CORE_APPLICATION_ID_CLAIM = "fhir_core_app_id";

public static List<String> getUserRolesFromJWT(DecodedJWT jwt) {
Claim claim = jwt.getClaim(REALM_ACCESS_CLAIM);
Map<String, Object> roles = claim.asMap();
return (List<String>) roles.get(ROLES);
}

public static String getApplicationIdFromJWT(DecodedJWT jwt) {
return com.google.fhir.gateway.JwtUtil.getClaimOrDie(jwt, FHIR_CORE_APPLICATION_ID_CLAIM);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@
import static org.smartregister.utils.Constants.LOCATION_RESOURCE_NOT_FOUND;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import javax.annotation.Nullable;
import javax.servlet.http.HttpServletRequest;

import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Location;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
Expand All @@ -21,8 +25,10 @@
import org.smartregister.model.location.LocationHierarchy;
import org.smartregister.model.location.LocationHierarchyTree;

import com.auth0.jwt.interfaces.DecodedJWT;
import com.google.fhir.gateway.ExceptionUtil;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.gclient.ReferenceClientParam;
Expand Down Expand Up @@ -60,6 +66,15 @@ public LocationHierarchy getLocationHierarchy(String locationId, List<String> ad
return locationHierarchy;
}

public List<LocationHierarchy> getLocationHierarchies(
List<String> locationIds, List<String> adminLevels) {
List<LocationHierarchy> locationHierarchies = new ArrayList<>();
for (String locationId : locationIds) {
locationHierarchies.add(getLocationHierarchy(locationId, adminLevels));
}
return locationHierarchies;
}

public LocationHierarchy getLocationHierarchyCore(String locationId, List<String> adminLevels) {
Location location = getLocationById(locationId);

Expand Down Expand Up @@ -151,8 +166,127 @@ public List<Location> getDescendants(
return location;
}

public Bundle getPaginatedLocations(HttpServletRequest request) {
String identifier = request.getParameter(Constants.IDENTIFIER);
public Bundle handleIdentifierRequest(HttpServletRequest request, String identifier) {
String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL);
String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL);
List<String> adminLevels =
generateAdminLevels(administrativeLevelMin, administrativeLevelMax);
String mode = request.getParameter(Constants.MODE);
if (Constants.LIST.equals(mode)) {
List<String> locationIds = Collections.singletonList(identifier);
return getPaginatedLocations(request, locationIds);
} else {
LocationHierarchy locationHierarchy = getLocationHierarchy(identifier, adminLevels);
return Utils.createBundle(Collections.singletonList(locationHierarchy));
}
}

public Bundle handleNonIdentifierRequest(
HttpServletRequest request,
PractitionerDetailsEndpointHelper practitionerDetailsEndpointHelper,
DecodedJWT verifiedJwt) {
String mode = request.getParameter(Constants.MODE);
String syncLocationsParam = request.getParameter(Constants.SYNC_LOCATIONS);
String administrativeLevelMin = request.getParameter(Constants.MIN_ADMIN_LEVEL);
String administrativeLevelMax = request.getParameter(Constants.MAX_ADMIN_LEVEL);
List<String> adminLevels =
generateAdminLevels(administrativeLevelMin, administrativeLevelMax);
List<String> selectedSyncLocations = extractSyncLocations(syncLocationsParam);
String practitionerId = verifiedJwt.getSubject();
List<String> userRoles = JwtUtils.getUserRolesFromJWT(verifiedJwt);
String applicationId = JwtUtils.getApplicationIdFromJWT(verifiedJwt);
String syncStrategy = getSyncStrategyByAppId(applicationId);

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);

} else {
List<Location> locations =
practitionerDetailsEndpointHelper
.getPractitionerDetailsByKeycloakId(practitionerId)
.getFhirPractitionerDetails()
.getLocations();
List<String> locationIds = new ArrayList<>();
for (Location location : locations) {
locationIds.add(location.getIdElement().getIdPart());
}
return getPaginatedLocations(request, locationIds);
}

} else {
if (Constants.SyncStrategy.RELATED_ENTITY_LOCATION.equalsIgnoreCase(syncStrategy)
&& userRoles.contains(Constants.ROLE_ALL_LOCATIONS)
&& !selectedSyncLocations.isEmpty()) {
List<LocationHierarchy> locationHierarchies =
getLocationHierarchies(selectedSyncLocations, adminLevels);
List<Resource> resourceList =
locationHierarchies.stream()
.map(locationHierarchy -> (Resource) locationHierarchy)
.collect(Collectors.toList());
return Utils.createBundle(resourceList);
} else {
List<LocationHierarchy> locationHierarchies =
practitionerDetailsEndpointHelper
.getPractitionerDetailsByKeycloakId(practitionerId)
.getFhirPractitionerDetails()
.getLocationHierarchyList();
List<Resource> resourceList =
locationHierarchies.stream()
.map(locationHierarchy -> (Resource) locationHierarchy)
.collect(Collectors.toList());
return Utils.createBundle(resourceList);
}
}
}

public String getSyncStrategyByAppId(String applicationId) {
String syncStrategy;
if (CacheHelper.INSTANCE.skipCache()) {
syncStrategy = getSyncStrategy(applicationId);
} else {
syncStrategy =
CacheHelper.INSTANCE.stringCache.get(
applicationId, key -> getSyncStrategy(applicationId));
}
return syncStrategy;
}

private String getSyncStrategy(String applicationId) {
FhirContext fhirContext = FhirContext.forR4();
IGenericClient client = Utils.createFhirClientForR4(fhirContext);

Bundle compositionBundle =
client.search()
.forResource(Composition.class)
.where(Composition.IDENTIFIER.exactly().identifier(applicationId))
.returnBundle(Bundle.class)
.execute();

Bundle.BundleEntryComponent compositionEntry = compositionBundle.getEntryFirstRep();
Composition composition;
if (compositionEntry != null) {
composition = (Composition) compositionEntry.getResource();
} else {
composition = null;
}
String binaryResourceReference = Utils.getBinaryResourceReference(composition);
Binary binary =
Utils.readApplicationConfigBinaryResource(binaryResourceReference, fhirContext);
return Utils.findSyncStrategy(binary);
}

public List<String> extractSyncLocations(String syncLocationsParam) {
List<String> selectedSyncLocations = new ArrayList<>();
if (syncLocationsParam != null && !syncLocationsParam.isEmpty()) {
Collections.addAll(selectedSyncLocations, syncLocationsParam.split(","));
}
return selectedSyncLocations;
}

public Bundle getPaginatedLocations(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);
Expand All @@ -171,17 +305,21 @@ public Bundle getPaginatedLocations(HttpServletRequest request) {
: Constants.PAGINATION_DEFAULT_PAGE_NUMBER;

int start = Math.max(0, (page - 1)) * count;
Location parentLocation = getLocationById(identifier);
List<Location> locations =
getLocationHierarchyLocations(identifier, parentLocation, adminLevels);
List<Resource> resourceLocations = new ArrayList<>(locations);
int totalEntries = locations.size();

List<Resource> resourceLocations = new ArrayList<>();
for (String identifier : locationIds) {
Location parentLocation = getLocationById(identifier);
List<Location> locations =
getLocationHierarchyLocations(identifier, parentLocation, adminLevels);
resourceLocations.addAll(locations);
}
int totalEntries = resourceLocations.size();

int end = Math.min(start + count, resourceLocations.size());
List<Resource> paginatedResourceLocations = resourceLocations.subList(start, end);
Bundle resultBundle;

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