diff --git a/exec/pom.xml b/exec/pom.xml
index d4cfed8..8141e2c 100755
--- a/exec/pom.xml
+++ b/exec/pom.xml
@@ -4,7 +4,7 @@
org.smartregister
opensrp-gateway-plugin
- 2.2.3
+ 2.2.4
exec
@@ -70,7 +70,7 @@
org.smartregister
plugins
- 2.2.3
+ 2.2.4
diff --git a/plugins/pom.xml b/plugins/pom.xml
index 5968a84..6e0fcec 100644
--- a/plugins/pom.xml
+++ b/plugins/pom.xml
@@ -4,7 +4,7 @@
org.smartregister
opensrp-gateway-plugin
- 2.2.3
+ 2.2.4
plugins
diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java
index 6d6383f..94038c3 100644
--- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java
+++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/LocationHierarchyEndpointHelper.java
@@ -181,6 +181,7 @@ public List getDescendants(
allLocations.add(parentLocation);
}
if (childLocationBundle != null) {
+ Utils.fetchAllBundlePagesAndInject(r4FHIRClient, childLocationBundle);
childLocationBundle.getEntry().parallelStream()
.forEach(
childLocation -> {
@@ -193,24 +194,6 @@ public List 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;
diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java
index 4f71f91..e4ee255 100755
--- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java
+++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/PermissionAccessChecker.java
@@ -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;
@@ -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;
@@ -103,20 +103,22 @@ public AccessDecision checkAccess(RequestDetailsReader requestDetails) {
private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
Map> 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 =
@@ -129,6 +131,38 @@ private void initSyncAccessDecision(RequestDetailsReader requestDetailsReader) {
userRoles);
}
+ @VisibleForTesting
+ protected static String generateSyncStrategyIdsCacheKey(
+ String userId, String syncStrategy, Map 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)
@@ -216,8 +250,7 @@ private Composition readCompositionResource(String applicationId, FhirContext fh
return compositionEntry != null ? (Composition) compositionEntry.getResource() : null;
}
- Pair fetchCompositionAndPractitionerDetails(
- String subject, String applicationId, FhirContext fhirContext) {
+ PractitionerDetails fetchPractitionerDetails(String subject) {
fhirContext.registerCustomType(PractitionerDetails.class);
IGenericClient client = Utils.createFhirClientForR4(fhirContext);
@@ -227,44 +260,33 @@ Pair 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 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 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> getSyncStrategyIds(
- String subjectId,
- String applicationId,
- FhirContext fhirContext,
- RequestDetailsReader requestDetailsReader) {
- Pair 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);
}
@@ -272,8 +294,9 @@ private Map> getSyncStrategyIds(
private List getLocationUuids(String[] syncLocations) {
List 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)));
@@ -353,16 +376,9 @@ private Map> 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<>();
}
@@ -370,15 +386,7 @@ private Map> collateSyncStrategyIds(
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(
diff --git a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java
index 3e4ff94..c0c4ca3 100644
--- a/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java
+++ b/plugins/src/main/java/org/smartregister/fhir/gateway/plugins/Utils.java
@@ -1,19 +1,28 @@
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;
@@ -21,6 +30,7 @@
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 {
@@ -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 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;
+ }
}
diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java
index 284c2df..2c688e2 100755
--- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java
+++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/PermissionAccessCheckerTest.java
@@ -451,4 +451,16 @@ public void testAccessDeniedWhenSingleRoleMissingForTypeBundleResources() throws
assertThat(canAccess, equalTo(false));
}
+
+ @Test
+ public void testGenerateSyncStrategyIdsCacheKey() {
+ String testUserId = "my-test-user-id";
+ Map 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);
+ }
}
diff --git a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java
index 4e126dd..98e0ba0 100644
--- a/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java
+++ b/plugins/src/test/java/org/smartregister/fhir/gateway/plugins/UtilsTest.java
@@ -1,8 +1,13 @@
package org.smartregister.fhir.gateway.plugins;
import java.nio.charset.StandardCharsets;
+import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Base64;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
import org.hl7.fhir.instance.model.api.IBaseBundle;
import org.hl7.fhir.r4.model.Base64BinaryType;
@@ -10,11 +15,13 @@
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.Composition;
import org.hl7.fhir.r4.model.Identifier;
+import org.hl7.fhir.r4.model.Meta;
import org.hl7.fhir.r4.model.Reference;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentMatchers;
+import org.mockito.MockedStatic;
import org.mockito.Mockito;
import com.google.gson.JsonArray;
@@ -22,18 +29,23 @@
import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.client.api.IGenericClient;
+import ca.uhn.fhir.rest.client.impl.GenericClient;
import ca.uhn.fhir.rest.gclient.ICriterion;
+import ca.uhn.fhir.rest.gclient.IGetPage;
+import ca.uhn.fhir.rest.gclient.IGetPageTyped;
import ca.uhn.fhir.rest.gclient.IQuery;
import ca.uhn.fhir.rest.gclient.IUntypedQuery;
public class UtilsTest {
private FhirContext fhirContextMock;
+ private GenericClient genericClientMock;
@Before
public void setUp() {
fhirContextMock = Mockito.mock(FhirContext.class);
IGenericClient clientMock = Mockito.mock(IGenericClient.class);
+ genericClientMock = Mockito.mock(GenericClient.class);
Mockito.when(fhirContextMock.newRestfulGenericClient(Mockito.anyString()))
.thenReturn(clientMock);
@@ -230,4 +242,110 @@ public void testReadApplicationConfigBinaryResourceReturnsBinary() {
Assert.assertEquals("{\"appId\":\"test-app\",\"appTitle\":\"Test App\"}", decodedJson);
}
+
+ @Test
+ public void testGenerateHashConsistency() throws NoSuchAlgorithmException {
+ String input = "consistentTest";
+ String hash1 = Utils.generateHash(input);
+ String hash2 = Utils.generateHash(input);
+ Assert.assertEquals(hash1, hash2);
+ }
+
+ @Test
+ public void testGenerateHashDifferentInputs() throws NoSuchAlgorithmException {
+ String input1 = "inputOne";
+ String input2 = "inputTwo";
+ String hash1 = Utils.generateHash(input1);
+ String hash2 = Utils.generateHash(input2);
+ Assert.assertNotEquals(hash1, hash2);
+ }
+
+ @Test
+ public void testFetchAllBundlePagesAndInject() {
+ Bundle firstPageBundle = new Bundle();
+ firstPageBundle.setMeta(new Meta().setLastUpdated(new Date()));
+ firstPageBundle.addLink().setRelation(Bundle.LINK_NEXT).setUrl("nextPageUrl");
+
+ Bundle secondPageBundle = new Bundle();
+ secondPageBundle.setMeta(new Meta().setLastUpdated(new Date()));
+ secondPageBundle.addEntry(new Bundle.BundleEntryComponent());
+
+ IGetPage loadPageMock = Mockito.mock(IGetPage.class);
+ IGetPageTyped iGetPageTypedMock = Mockito.mock(IGetPageTyped.class);
+ Mockito.doReturn(loadPageMock).when(genericClientMock).loadPage();
+ Mockito.doReturn(iGetPageTypedMock).when(loadPageMock).next(firstPageBundle);
+ Mockito.doReturn(secondPageBundle).when(iGetPageTypedMock).execute();
+ Utils.fetchAllBundlePagesAndInject(genericClientMock, firstPageBundle);
+
+ Assert.assertEquals(1, firstPageBundle.getEntry().size());
+ Assert.assertNull(firstPageBundle.getLink(Bundle.LINK_NEXT));
+ Assert.assertNotNull(firstPageBundle.getMeta().getLastUpdated());
+ Mockito.verify(genericClientMock.loadPage(), Mockito.times(1)).next(firstPageBundle);
+ }
+
+ @Test
+ public void testCleanUpBundleLinksServerBaseUrlMultiplePaginationNextLink() {
+ Bundle resultBundle = new Bundle();
+ resultBundle
+ .addLink()
+ .setRelation(Bundle.LINK_NEXT)
+ .setUrl(
+ "http://old-base-url:8080/fhir?_getpages=c380a770-4ecc-45fa-b9c4-003c5b37e1f4&_getpagesoffset=2&_count=1&_pretty=true&_bundletype=searchset");
+
+ Mockito.when(genericClientMock.getUrlBase()).thenReturn("http://new-base-url");
+
+ Utils.cleanUpBundlePaginationNextLinkServerBaseUrl(genericClientMock, resultBundle);
+
+ List links = resultBundle.getLink();
+
+ Bundle.BundleLinkComponent nextLink =
+ links.stream()
+ .filter(link -> Bundle.LINK_NEXT.equals(link.getRelation()))
+ .findFirst()
+ .orElse(null);
+ Assert.assertNotNull(nextLink);
+ Assert.assertEquals(
+ "http://new-base-url?_getpages=c380a770-4ecc-45fa-b9c4-003c5b37e1f4&_getpagesoffset=2&_count=1&_pretty=true&_bundletype=searchset",
+ nextLink.getUrl());
+ }
+
+ @Test
+ public void testGenericCleanBaseUrl() {
+ String cleanHostUrl =
+ Utils.cleanBaseUrl(
+ "http://old-base-url/nextPage?param=value", "http://new-base-url");
+ Assert.assertEquals("http://new-base-url/nextPage?param=value", cleanHostUrl);
+ }
+
+ @Test
+ public void testGenerateSyncStrategyIdsCacheKeyWithSyncLocations() {
+ String userId = "user123";
+ String syncStrategy = Constants.SyncStrategy.RELATED_ENTITY_LOCATION;
+ Map parameters = new HashMap<>();
+ parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"});
+
+ MockedStatic mockUtils = Mockito.mockStatic(Utils.class);
+ mockUtils.when(() -> Utils.generateHash("location1")).thenReturn("hashedLocation1");
+ mockUtils.when(() -> Utils.getSortedInput("location1", ",")).thenReturn("location1");
+
+ String result =
+ PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
+ userId, syncStrategy, parameters);
+ Assert.assertEquals("hashedLocation1", result);
+ mockUtils.close();
+ }
+
+ @Test
+ public void testGenerateSyncStrategyIdsCacheKeyDefaultStrategy() {
+ String userId = "user123";
+ String syncStrategy = "someOtherStrategy";
+ Map parameters = new HashMap<>();
+ parameters.put(Constants.SYNC_LOCATIONS_SEARCH_PARAM, new String[] {"location1"});
+
+ String result =
+ PermissionAccessChecker.generateSyncStrategyIdsCacheKey(
+ userId, syncStrategy, parameters);
+
+ Assert.assertEquals(userId, result);
+ }
}
diff --git a/pom.xml b/pom.xml
index 57c55c4..69134af 100755
--- a/pom.xml
+++ b/pom.xml
@@ -11,7 +11,7 @@
org.smartregister
opensrp-gateway-plugin
- 2.2.3
+ 2.2.4
pom