Skip to content

Commit

Permalink
Implement List Mode Content Fetch ⚡️ (#40)
Browse files Browse the repository at this point in the history
* Implement List Mode Content Fetch ⚡️
- Support returning all List resource References in result
- Add Unit Tests
  • Loading branch information
ndegwamartin authored Jul 7, 2023
1 parent 7be1789 commit dbb0978
Show file tree
Hide file tree
Showing 10 changed files with 233 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ public boolean canAccess() {
}

@Override
public String postProcess(HttpResponse response) throws IOException {
public String postProcess(RequestDetailsReader request, HttpResponse response)
throws IOException {
Preconditions.checkState(HttpUtil.isResponseValid(response));
String content = CharStreams.toString(HttpUtil.readerFromEntity(response.getEntity()));
IParser parser = fhirContext.newJsonParser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,12 @@
*/
package com.google.fhir.gateway.plugin;

import static com.google.fhir.gateway.plugin.PermissionAccessChecker.Factory.PROXY_TO_ENV;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.parser.IParser;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.annotations.VisibleForTesting;
import com.google.fhir.gateway.ProxyConstants;
Expand All @@ -36,7 +41,11 @@
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import org.apache.http.HttpResponse;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.util.TextUtils;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ListResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

Expand All @@ -56,6 +65,10 @@ public class OpenSRPSyncAccessDecision implements AccessDecision {

private final List<String> organizationIds;
private IgnoredResourcesConfig config;
private Gson gson = new Gson();

private FhirContext fhirR4Context = FhirContext.forR4();
private IParser fhirR4JsonParser = fhirR4Context.newJsonParser();

public OpenSRPSyncAccessDecision(
String applicationId,
Expand Down Expand Up @@ -133,8 +146,58 @@ private void addSyncFilters(
}

@Override
public String postProcess(HttpResponse response) throws IOException {
return null;
public String postProcess(RequestDetailsReader request, HttpResponse response)
throws IOException {

String resultContent = null;
String listMode = request.getHeader(Constants.FHIR_GATEWAY_MODE);

switch (listMode) {
case Constants.LIST_ENTRIES:
resultContent = postProcessModeListEntries(response);
default:
break;
}
return resultContent;
}

/**
* Generates a Bundle result from making a batch search request with the contained entries in the
* List as parameters
*
* @param response HTTPResponse
* @return String content of the result Bundle
*/
private String postProcessModeListEntries(HttpResponse response) throws IOException {

String resultContent = null;
IBaseResource responseResource =
fhirR4JsonParser.parseResource((new BasicResponseHandler().handleResponse(response)));

if (responseResource instanceof ListResource && ((ListResource) responseResource).hasEntry()) {

Bundle requestBundle = new Bundle();
requestBundle.setType(Bundle.BundleType.BATCH);
Bundle.BundleEntryComponent bundleEntryComponent;

for (ListResource.ListEntryComponent listEntryComponent :
((ListResource) responseResource).getEntry()) {

bundleEntryComponent = new Bundle.BundleEntryComponent();
bundleEntryComponent.setRequest(
new Bundle.BundleEntryRequestComponent()
.setMethod(Bundle.HTTPVerb.GET)
.setUrl(listEntryComponent.getItem().getReference()));

requestBundle.addEntry(bundleEntryComponent);
}

Bundle responseBundle =
createFhirClientForR4().transaction().withBundle(requestBundle).execute();

resultContent = fhirR4JsonParser.encodeResourceToString(responseBundle);
}
return resultContent;
}

/**
Expand Down Expand Up @@ -214,7 +277,6 @@ private boolean isResourceTypeRequest(String requestPath) {
protected IgnoredResourcesConfig getIgnoredResourcesConfigFileConfiguration(String configFile) {
if (configFile != null && !configFile.isEmpty()) {
try {
Gson gson = new Gson();
config = gson.fromJson(new FileReader(configFile), IgnoredResourcesConfig.class);
if (config == null || config.entries == null) {
throw new IllegalArgumentException("A map with a single `entries` array expected!");
Expand Down Expand Up @@ -286,6 +348,10 @@ private boolean shouldSkipDataFiltering(ServletRequestDetails servletRequestDeta
return false;
}

private IGenericClient createFhirClientForR4() {
return fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV));
}

@VisibleForTesting
protected void setSkippedResourcesConfig(IgnoredResourcesConfig config) {
this.config = config;
Expand All @@ -308,4 +374,19 @@ public String toString() {
+ '}';
}
}

@VisibleForTesting
protected void setFhirR4Context(FhirContext fhirR4Context) {
this.fhirR4Context = fhirR4Context;
}

@VisibleForTesting
protected void setFhirR4JsonParser(IParser fhirR4JsonParser) {
this.fhirR4JsonParser = fhirR4JsonParser;
}

public static final class Constants {
public static final String FHIR_GATEWAY_MODE = "fhir-gateway-mode";
public static final String LIST_ENTRIES = "list-entries";
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import ca.uhn.fhir.context.FhirContext;
import com.google.common.io.Resources;
import com.google.fhir.gateway.HttpFhirClient;
import com.google.fhir.gateway.interfaces.RequestDetailsReader;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
Expand All @@ -39,6 +40,9 @@ public class AccessGrantedAndUpdateListTest {
@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private HttpResponse responseMock;

@Mock(answer = Answers.RETURNS_DEEP_STUBS)
private RequestDetailsReader requestDetailsMock;

private static final FhirContext fhirContext = FhirContext.forR4();

private AccessGrantedAndUpdateList testInstance;
Expand All @@ -55,14 +59,14 @@ public void postProcessNewPatientPut() throws IOException {
testInstance =
AccessGrantedAndUpdateList.forPatientResource(
TEST_LIST_ID, httpFhirClientMock, fhirContext);
testInstance.postProcess(responseMock);
testInstance.postProcess(requestDetailsMock, responseMock);
}

@Test
public void postProcessNewPatientPost() throws IOException {
testInstance =
AccessGrantedAndUpdateList.forPatientResource(
TEST_LIST_ID, httpFhirClientMock, fhirContext);
testInstance.postProcess(responseMock);
testInstance.postProcess(requestDetailsMock, responseMock);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,39 @@
*/
package com.google.fhir.gateway.plugin;

import static com.google.fhir.gateway.plugin.PermissionAccessChecker.Factory.PROXY_TO_ENV;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;

import ca.uhn.fhir.context.FhirContext;
import ca.uhn.fhir.rest.api.RequestTypeEnum;
import ca.uhn.fhir.rest.api.RestOperationTypeEnum;
import ca.uhn.fhir.rest.client.api.IGenericClient;
import ca.uhn.fhir.rest.gclient.ITransaction;
import ca.uhn.fhir.rest.gclient.ITransactionTyped;
import ca.uhn.fhir.rest.server.servlet.ServletRequestDetails;
import com.google.common.collect.Maps;
import com.google.common.io.Resources;
import com.google.fhir.gateway.HttpFhirClient;
import com.google.fhir.gateway.ProxyConstants;
import com.google.fhir.gateway.interfaces.RequestDetailsReader;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.apache.http.HttpResponse;
import org.hl7.fhir.r4.model.Bundle;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Answers;
import org.mockito.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.MockitoJUnitRunner;

@RunWith(MockitoJUnitRunner.class)
Expand All @@ -44,6 +61,8 @@ public class OpenSRPSyncAccessDecisionTest {

private OpenSRPSyncAccessDecision testInstance;

@Mock private HttpFhirClient httpFhirClientMock;

@Test
public void preprocessShouldAddAllFiltersWhenIdsForLocationsOrganisationsAndCareTeamsAreProvided()
throws IOException {
Expand Down Expand Up @@ -300,6 +319,84 @@ public void preProcessShouldSkipAddingFiltersWhenResourceInSyncFilterIgnoredReso
}
}

@Test
public void testPostProcessWithListModeHeaderShouldFetchListEntriesBundle() throws IOException {
locationIds.add("Location-1");
testInstance = Mockito.spy(createOpenSRPSyncAccessDecisionTestInstance());

FhirContext fhirR4Context = mock(FhirContext.class);
IGenericClient iGenericClient = mock(IGenericClient.class);
ITransaction iTransaction = mock(ITransaction.class);
ITransactionTyped<Bundle> iClientExecutable = mock(ITransactionTyped.class);

Mockito.when(fhirR4Context.newRestfulGenericClient(System.getenv(PROXY_TO_ENV)))
.thenReturn(iGenericClient);
Mockito.when(iGenericClient.transaction()).thenReturn(iTransaction);
Mockito.when(iTransaction.withBundle(any(Bundle.class))).thenReturn(iClientExecutable);

Bundle resultBundle = new Bundle();
resultBundle.setType(Bundle.BundleType.BATCHRESPONSE);
resultBundle.setId("bundle-result-id");

Mockito.when(iClientExecutable.execute()).thenReturn(resultBundle);

ArgumentCaptor<Bundle> bundleArgumentCaptor = ArgumentCaptor.forClass(Bundle.class);

testInstance.setFhirR4Context(fhirR4Context);

RequestDetailsReader requestDetailsSpy = Mockito.mock(RequestDetailsReader.class);

Mockito.when(requestDetailsSpy.getHeader(OpenSRPSyncAccessDecision.Constants.FHIR_GATEWAY_MODE))
.thenReturn(OpenSRPSyncAccessDecision.Constants.LIST_ENTRIES);

URL listUrl = Resources.getResource("test_list_resource.json");
String testListJson = Resources.toString(listUrl, StandardCharsets.UTF_8);

HttpResponse fhirResponseMock = Mockito.mock(HttpResponse.class, Answers.RETURNS_DEEP_STUBS);

TestUtil.setUpFhirResponseMock(fhirResponseMock, testListJson);

String resultContent = testInstance.postProcess(requestDetailsSpy, fhirResponseMock);

Mockito.verify(iTransaction).withBundle(bundleArgumentCaptor.capture());
Bundle requestBundle = bundleArgumentCaptor.getValue();

// Verify modified request to the server
Assert.assertNotNull(requestBundle);
Assert.assertEquals(Bundle.BundleType.BATCH, requestBundle.getType());
List<Bundle.BundleEntryComponent> requestBundleEntries = requestBundle.getEntry();
Assert.assertEquals(2, requestBundleEntries.size());

Assert.assertEquals(Bundle.HTTPVerb.GET, requestBundleEntries.get(0).getRequest().getMethod());
Assert.assertEquals(
"Group/proxy-list-entry-id-1", requestBundleEntries.get(0).getRequest().getUrl());

Assert.assertEquals(Bundle.HTTPVerb.GET, requestBundleEntries.get(1).getRequest().getMethod());
Assert.assertEquals(
"Group/proxy-list-entry-id-2", requestBundleEntries.get(1).getRequest().getUrl());

// Verify returned result content from the server request
Assert.assertNotNull(resultContent);
Assert.assertEquals(
"{\"resourceType\":\"Bundle\",\"id\":\"bundle-result-id\",\"type\":\"batch-response\"}",
resultContent);
}

@Test
public void testPostProcessWithoutListModeHeaderShouldShouldReturnNull() throws IOException {
testInstance = createOpenSRPSyncAccessDecisionTestInstance();

RequestDetailsReader requestDetailsSpy = Mockito.mock(RequestDetailsReader.class);
Mockito.when(requestDetailsSpy.getHeader(OpenSRPSyncAccessDecision.Constants.FHIR_GATEWAY_MODE))
.thenReturn("");

String resultContent =
testInstance.postProcess(requestDetailsSpy, Mockito.mock(HttpResponse.class));

// Verify no special Post-Processing happened
Assert.assertNull(resultContent);
}

private OpenSRPSyncAccessDecision createOpenSRPSyncAccessDecisionTestInstance() {
OpenSRPSyncAccessDecision accessDecision =
new OpenSRPSyncAccessDecision(
Expand Down
35 changes: 35 additions & 0 deletions plugins/src/test/resources/test_list_resource.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"resourceType": "List",
"id": "proxy-test-list-id",
"identifier": [
{
"use": "official",
"value": "proxy-test-list-id"
}
],
"status": "current",
"mode": "working",
"title": "Proxy Test List",
"code": {
"coding": [
{
"system": "http://ona.io",
"code": "supply-chain",
"display": "Proxy Test List"
}
],
"text": "My Proxy Test List"
},
"entry": [
{
"item": {
"reference": "Group/proxy-list-entry-id-1"
}
},
{
"item": {
"reference": "Group/proxy-list-entry-id-2"
}
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ public boolean authorizeRequest(RequestDetails requestDetails) {
if (HttpUtil.isResponseValid(response)) {
try {
// For post-processing rationale/example see b/207589782#comment3.
content = outcome.postProcess(response);
content = outcome.postProcess(new RequestDetailsToReader(requestDetails), response);
} catch (Exception e) {
// Note this is after a successful fetch/update of the FHIR store. That success must be
// passed to the client even if the access related post-processing fails.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public boolean canAccess() {
}

@Override
public String postProcess(HttpResponse response) throws IOException {
public String postProcess(RequestDetailsReader requestDetails, HttpResponse response)
throws IOException {
Preconditions.checkState(HttpUtil.isResponseValid(response));
String content = CharStreams.toString(HttpUtil.readerFromEntity(response.getEntity()));
IParser parser = fhirContext.newJsonParser();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,5 +56,6 @@ public interface AccessDecision {
* reads the response; otherwise null. Note that we should try to avoid reading the whole
* content in memory whenever it is not needed for post-processing.
*/
String postProcess(HttpResponse response) throws IOException;
String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response)
throws IOException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public boolean canAccess() {
}

@Override
public String postProcess(HttpResponse response) {
public String postProcess(RequestDetailsReader requestDetailsReader, HttpResponse response) {
return null;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,9 @@ public RequestMutation getRequestMutation(RequestDetailsReader requestDetailsRea
return RequestMutation.builder().queryParams(paramMutations).build();
}

public String postProcess(HttpResponse response) throws IOException {
@Override
public String postProcess(
RequestDetailsReader requestDetailsReader, HttpResponse response) throws IOException {
return null;
}
};
Expand Down

0 comments on commit dbb0978

Please sign in to comment.