Skip to content

Commit

Permalink
Merge pull request #108 from onaio/cherry-picked-enhancements
Browse files Browse the repository at this point in the history
FHIR Gateway Extensions enhancements
  • Loading branch information
pld authored Jan 22, 2025
2 parents 3471eb3 + bd0022a commit 3e4502f
Show file tree
Hide file tree
Showing 8 changed files with 288 additions and 77 deletions.
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.3</version>
<version>2.2.4</version>
</parent>

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

<artifactId>plugins</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ public List<Location> getDescendants(
allLocations.add(parentLocation);
}
if (childLocationBundle != null) {
Utils.fetchAllBundlePagesAndInject(r4FHIRClient, childLocationBundle);
childLocationBundle.getEntry().parallelStream()
.forEach(
childLocation -> {
Expand All @@ -193,24 +194,6 @@ public List<Location> getDescendants(
null,
adminLevels));
});

while (childLocationBundle.getLink(Bundle.LINK_NEXT) != null) {
childLocationBundle =
getFhirClientForR4().loadPage().next(childLocationBundle).execute();

childLocationBundle.getEntry().parallelStream()
.forEach(
childLocation -> {
Location childLocationEntity =
(Location) childLocation.getResource();
allLocations.add(childLocationEntity);
allLocations.addAll(
getDescendants(
childLocationEntity.getIdElement().getIdPart(),
null,
adminLevels));
});
}
}

return allLocations;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.smartregister.fhir.gateway.plugins;

import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
Expand All @@ -11,7 +12,6 @@
import javax.inject.Named;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import org.hl7.fhir.r4.model.Binary;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.CareTeam;
Expand Down Expand Up @@ -103,20 +103,22 @@ public AccessDecision checkAccess(RequestDetailsReader requestDetails) {
private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
Map<String, List<String>> syncStrategyIds;

Composition composition = fetchComposition();
String syncStrategy = readSyncStrategyFromComposition(composition);

if (CacheHelper.INSTANCE.skipCache()) {
syncStrategyIds =
getSyncStrategyIds(
jwt.getSubject(), applicationId, fhirContext, requestDetailsReader);
getSyncStrategyIds(jwt.getSubject(), syncStrategy, requestDetailsReader);
} else {
syncStrategyIds =
CacheHelper.INSTANCE.cache.get(
jwt.getSubject(),
userId ->
generateSyncStrategyIdsCacheKey(
jwt.getSubject(),
syncStrategy,
requestDetailsReader.getParameters()),
key ->
getSyncStrategyIds(
userId,
applicationId,
fhirContext,
requestDetailsReader));
jwt.getSubject(), syncStrategy, requestDetailsReader));
}

this.syncAccessDecision =
Expand All @@ -129,6 +131,38 @@ private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
userRoles);
}

@VisibleForTesting
protected static String generateSyncStrategyIdsCacheKey(
String userId, String syncStrategy, Map<String, String[]> parameters) {

String key = null;
switch (syncStrategy) {
case Constants.SyncStrategy.RELATED_ENTITY_LOCATION:
try {

String[] syncLocations =
parameters.getOrDefault(
Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {});

if (syncLocations.length == 0) {
key = userId;
} else {
key = Utils.generateHash(Utils.getSortedInput(syncLocations[0], ","));
}

} catch (NoSuchAlgorithmException exception) {
logger.error(exception.getMessage());
}

break;

default:
key = userId;
}

return key;
}

private boolean checkUserHasRole(String resourceName, String requestType) {
return StringUtils.isNotBlank(resourceName)
&& (checkIfRoleExists(getAdminRoleName(resourceName), this.userRoles)
Expand Down Expand Up @@ -216,8 +250,7 @@ private Composition readCompositionResource(String applicationId, FhirContext fh
return compositionEntry != null ? (Composition) compositionEntry.getResource() : null;
}

Pair<Composition, PractitionerDetails> fetchCompositionAndPractitionerDetails(
String subject, String applicationId, FhirContext fhirContext) {
PractitionerDetails fetchPractitionerDetails(String subject) {
fhirContext.registerCustomType(PractitionerDetails.class);

IGenericClient client = Utils.createFhirClientForR4(fhirContext);
Expand All @@ -227,53 +260,43 @@ Pair<Composition, PractitionerDetails> fetchCompositionAndPractitionerDetails(
PractitionerDetails practitionerDetails =
practitionerDetailsEndpointHelper.getPractitionerDetailsByKeycloakId(subject);

Composition composition = readCompositionResource(applicationId, fhirContext);

if (composition == null)
throw new IllegalStateException(
"No Composition resource found for application id '" + applicationId + "'");

if (practitionerDetails == null)
throw new IllegalStateException(
"No PractitionerDetail resource found for user with id '" + subject + "'");

return Pair.of(composition, practitionerDetails);
return practitionerDetails;
}

Pair<String, PractitionerDetails> fetchSyncStrategyDetails(
String subject, String applicationId, FhirContext fhirContext) {
private Composition fetchComposition() {
Composition composition = readCompositionResource(applicationId, fhirContext);
if (composition == null)
throw new IllegalStateException(
"No Composition resource found for application id '" + applicationId + "'");

Pair<Composition, PractitionerDetails> compositionPractitionerDetailsPair =
fetchCompositionAndPractitionerDetails(subject, applicationId, fhirContext);
Composition composition = compositionPractitionerDetailsPair.getLeft();
PractitionerDetails practitionerDetails = compositionPractitionerDetailsPair.getRight();
return composition;
}

private String readSyncStrategyFromComposition(Composition composition) {
String binaryResourceReference = Utils.getBinaryResourceReference(composition);
Binary binary =
Utils.readApplicationConfigBinaryResource(binaryResourceReference, fhirContext);

return Pair.of(Utils.findSyncStrategy(binary), practitionerDetails);
return Utils.findSyncStrategy(binary);
}

private Map<String, List<String>> getSyncStrategyIds(
String subjectId,
String applicationId,
FhirContext fhirContext,
RequestDetailsReader requestDetailsReader) {
Pair<String, PractitionerDetails> syncStrategyDetails =
fetchSyncStrategyDetails(subjectId, applicationId, fhirContext);
String subjectId, String syncStrategy, RequestDetailsReader requestDetailsReader) {

String syncStrategy = syncStrategyDetails.getLeft();
PractitionerDetails practitionerDetails = syncStrategyDetails.getRight();
PractitionerDetails practitionerDetails = fetchPractitionerDetails(subjectId);

return collateSyncStrategyIds(syncStrategy, practitionerDetails, requestDetailsReader);
}

private List<String> getLocationUuids(String[] syncLocations) {
List<String> locationUuids = new ArrayList<>();
String syncLocationParam;
for (int i = 0; i < syncLocations.length; i++) {
syncLocationParam = syncLocations[i];

for (String syncLocation : syncLocations) {
syncLocationParam = syncLocation;
if (!syncLocationParam.isEmpty())
locationUuids.addAll(
Set.of(syncLocationParam.split(Constants.PARAM_VALUES_SEPARATOR)));
Expand Down Expand Up @@ -353,32 +376,17 @@ private Map<String, List<String>> collateSyncStrategyIds(
&& practitionerDetails.getFhirPractitionerDetails()
!= null
? PractitionerDetailsEndpointHelper.getAttributedLocations(
PractitionerDetailsEndpointHelper.getLocationsHierarchy(
practitionerDetails
.getFhirPractitionerDetails()
.getLocations()
.stream()
.map(
location ->
location.getIdElement()
.getIdPart())
.collect(Collectors.toList())))
practitionerDetails
.getFhirPractitionerDetails()
.getLocationHierarchyList())
: new HashSet<>();
}

} else
throw new IllegalStateException(
"'" + syncStrategy + "' sync strategy NOT supported!!");

resultMap =
!syncStrategyIds.isEmpty()
? Map.of(syncStrategy, new ArrayList<>(syncStrategyIds))
: null;

if (resultMap == null) {
throw new IllegalStateException(
"No Sync strategy ids found for selected sync strategy " + syncStrategy);
}
resultMap = Map.of(syncStrategy, new ArrayList<>(syncStrategyIds));

} else
throw new IllegalStateException(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,26 +1,36 @@
package org.smartregister.fhir.gateway.plugins;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Base64;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.UUID;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.apache.commons.codec.binary.Hex;
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.Reference;
import org.hl7.fhir.r4.model.Resource;
import org.hl7.fhir.r4.model.StringType;
import org.hl7.fhir.r4.model.UriType;

import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonObject;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.client.impl.GenericClient;

public class Utils {

Expand Down Expand Up @@ -193,4 +203,84 @@ public static String findSyncStrategy(byte[] binaryDataBytes) {

return syncStrategy;
}

public static String generateHash(String input) throws NoSuchAlgorithmException {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hashBytes = digest.digest(input.getBytes());
return Hex.encodeHexString(hashBytes);
}

public static String getSortedInput(String input, String separator) {
return getSortedInput(Arrays.stream(input.split(separator)), separator);
}

public static String getSortedInput(Stream<String> inputStream, String separator) {
return inputStream.sorted(Comparator.naturalOrder()).collect(Collectors.joining(separator));
}

/**
* This is a recursive function which updates the result bundle with results of all pages
* whenever there's an entry for Bundle.LINK_NEXT
*
* @param fhirClient the Generic FHIR Client instance
* @param resultBundle the result bundle from the first request
*/
public static void fetchAllBundlePagesAndInject(
IGenericClient fhirClient, Bundle resultBundle) {

if (resultBundle.getLink(Bundle.LINK_NEXT) != null) {

cleanUpBundlePaginationNextLinkServerBaseUrl((GenericClient) fhirClient, resultBundle);

Bundle pageResultBundle = fhirClient.loadPage().next(resultBundle).execute();

resultBundle.getEntry().addAll(pageResultBundle.getEntry());
resultBundle.setLink(pageResultBundle.getLink());

fetchAllBundlePagesAndInject(fhirClient, resultBundle);
}

resultBundle.setLink(
resultBundle.getLink().stream()
.filter(
bundleLinkComponent ->
!Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation()))
.collect(Collectors.toList()));
resultBundle.getMeta().setLastUpdated(resultBundle.getMeta().getLastUpdated());
}

public static void cleanUpBundlePaginationNextLinkServerBaseUrl(
GenericClient fhirClient, Bundle resultBundle) {
String cleanUrl =
cleanHapiPaginationLinkBaseUrl(
resultBundle.getLink(Bundle.LINK_NEXT).getUrl(), fhirClient.getUrlBase());
resultBundle
.getLink()
.replaceAll(
bundleLinkComponent ->
Bundle.LINK_NEXT.equals(bundleLinkComponent.getRelation())
? new Bundle.BundleLinkComponent(
new StringType(Bundle.LINK_NEXT),
new UriType(cleanUrl))
: bundleLinkComponent);
}

public static String cleanBaseUrl(String originalUrl, String fhirServerBaseUrl) {
int hostStartIndex = originalUrl.indexOf("://") + 3;
int pathStartIndex = originalUrl.indexOf("/", hostStartIndex);

// If the URL has no path, assume it ends right after the host
if (pathStartIndex == -1) {
pathStartIndex = originalUrl.length();
}

return fhirServerBaseUrl + originalUrl.substring(pathStartIndex);
}

public static String cleanHapiPaginationLinkBaseUrl(
String originalUrl, String fhirServerBaseUrl) {
return originalUrl.indexOf('?') > -1
? fhirServerBaseUrl + originalUrl.substring(originalUrl.indexOf('?'))
: fhirServerBaseUrl;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -451,4 +451,16 @@ public void testAccessDeniedWhenSingleRoleMissingForTypeBundleResources() throws

assertThat(canAccess, equalTo(false));
}

@Test
public void testGenerateSyncStrategyIdsCacheKey() {
String testUserId = "my-test-user-id";
Map<String, String[]> strategyIdMap =
Map.of(Constants.SyncStrategy.CARE_TEAM, new String[] {"id-1, id-2,id-3"});
String cacheKey =
PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
testUserId, Constants.SyncStrategy.CARE_TEAM, strategyIdMap);

Assert.assertEquals(testUserId, cacheKey);
}
}
Loading

0 comments on commit 3e4502f

Please sign in to comment.