Skip to content

Commit

Permalink
Merge pull request #32 from onaio/paginate_list_entries
Browse files Browse the repository at this point in the history
Add support for pagination of list entries
  • Loading branch information
lincmba authored Feb 7, 2024
2 parents 2872066 + ea59d43 commit d1d1f21
Show file tree
Hide file tree
Showing 4 changed files with 321 additions and 16 deletions.
36 changes: 36 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,42 @@ URL:
This approach ensures consistency and clarity when accessing various endpoint
types through the gateway.

### Custom Headers

#### FHIR-Gateway-Mode

##### Overview

The FHIR Gateway Mode allows for custom processing of responses from the FHIR
server. The mode is triggered by a HTTP Header sent by the client named
`FHIR-Gateway-Mode` with a value e.g. `list-entries`(Currently only supported).

##### FHIR-Gateway-Mode: list-entries

This mode is used when using the `/List` endpoint. Normally, fetching using this
endpoint returns a list of references which can then be used to query for the
actual resources. With this header value configured the response is instead a
Bundle that contains all the actual (referenced) resources.

###### Pagination

Pagination is supported in fetching the data from a FHIR server. This can be
useful when dealing with List resources that have a large number of referenced
entries like Locations.

To enable pagination, you need to include two parameters in the request URL:

- `_page`: This parameter specifies the page number and has a default value
of 1.
- `_count`: This parameter sets the number of items per page and has a default
value of 20.

Example:

```
[GET] /List?_id=<some-id>&_count=<page-size>&_page=<page-number>&_sort=<some-sort>
```

#### Important Note:

Developers, please update your client applications accordingly to accommodate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,14 @@ public class Constants {
public static final String KEYCLOAK_UUID = "keycloak-uuid";
public static final String IDENTIFIER = "identifier";

public static final String PAGINATION_PAGE_SIZE = "_count";

public static final String PAGINATION_PAGE_NUMBER = "_page";

public static final int PAGINATION_DEFAULT_PAGE_SIZE = 20;

public static final int PAGINATION_DEFAULT_PAGE_NUMBER = 1;

public interface Literals {
String EQUALS = "=";
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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.IBaseBundle;
import org.hl7.fhir.instance.model.api.IBaseResource;
import org.hl7.fhir.r4.model.Bundle;
import org.hl7.fhir.r4.model.ListResource;
Expand All @@ -29,6 +30,7 @@

import com.google.common.annotations.VisibleForTesting;
import com.google.fhir.gateway.ExceptionUtil;
import com.google.fhir.gateway.ProxyConstants;
import com.google.fhir.gateway.interfaces.AccessDecision;
import com.google.fhir.gateway.interfaces.RequestDetailsReader;
import com.google.fhir.gateway.interfaces.RequestMutation;
Expand Down Expand Up @@ -168,7 +170,7 @@ public String postProcess(RequestDetailsReader request, HttpResponse response)

switch (gatewayMode) {
case SyncAccessDecisionConstants.LIST_ENTRIES:
resultContentBundle = postProcessModeListEntries(responseResource);
resultContentBundle = postProcessModeListEntries(responseResource, request);
break;

default:
Expand Down Expand Up @@ -216,11 +218,16 @@ private static OperationOutcome createOperationOutcome(String exception) {

@NotNull
private static Bundle processListEntriesGatewayModeByListResource(
ListResource responseListResource) {
ListResource responseListResource, int start, int count) {
Bundle requestBundle = new Bundle();
requestBundle.setType(Bundle.BundleType.BATCH);

for (ListResource.ListEntryComponent listEntryComponent : responseListResource.getEntry()) {
int end = start + count;

List<ListResource.ListEntryComponent> entries = responseListResource.getEntry();

for (int i = start; i < Math.min(end, entries.size()); i++) {
ListResource.ListEntryComponent listEntryComponent = entries.get(i);
requestBundle.addEntry(
createBundleEntryComponent(
Bundle.HTTPVerb.GET,
Expand All @@ -230,7 +237,8 @@ private static Bundle processListEntriesGatewayModeByListResource(
return requestBundle;
}

private Bundle processListEntriesGatewayModeByBundle(IBaseResource responseResource) {
private Bundle processListEntriesGatewayModeByBundle(
IBaseResource responseResource, int start, int count) {
Bundle requestBundle = new Bundle();
requestBundle.setType(Bundle.BundleType.BATCH);

Expand All @@ -242,6 +250,8 @@ private Bundle processListEntriesGatewayModeByBundle(IBaseResource responseResou
bundleEntryComponent ->
((ListResource) bundleEntryComponent.getResource())
.getEntry().stream())
.skip(start)
.limit(count)
.map(
listEntryComponent ->
createBundleEntryComponent(
Expand Down Expand Up @@ -271,25 +281,85 @@ static Bundle.BundleEntryComponent createBundleEntryComponent(
* Generates a Bundle result from making a batch search request with the contained entries in
* the List as parameters
*
* @param responseResource FHIR Resource result returned byt the HTTPResponse
* @param responseResource FHIR Resource result returned by the HTTPResponse
* @return String content of the result Bundle
*/
private Bundle postProcessModeListEntries(IBaseResource responseResource) {

private Bundle postProcessModeListEntries(
IBaseResource responseResource, RequestDetailsReader request) {

Map<String, String[]> parameters = new HashMap<>(request.getParameters());
String[] pageSize = parameters.get(Constants.PAGINATION_PAGE_SIZE);
String[] pageNumber = parameters.get(Constants.PAGINATION_PAGE_NUMBER);

int totalEntries = 0;
int count =
pageSize != null && pageSize.length > 0
? Integer.parseInt(pageSize[0])
: Constants.PAGINATION_DEFAULT_PAGE_SIZE;
int page =
pageNumber != null && pageNumber.length > 0
? Integer.parseInt(pageNumber[0])
: Constants.PAGINATION_DEFAULT_PAGE_NUMBER;

int start = Math.max(0, (page - 1)) * count;
Bundle requestBundle = null;

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

totalEntries = ((ListResource) responseResource).getEntry().size();
requestBundle =
processListEntriesGatewayModeByListResource((ListResource) responseResource);
processListEntriesGatewayModeByListResource(
(ListResource) responseResource, start, count);

} else if (responseResource instanceof Bundle) {
List<Bundle.BundleEntryComponent> entries = ((Bundle) responseResource).getEntry();
for (Bundle.BundleEntryComponent entry : entries) {
if (entry.getResource() instanceof ListResource) {
totalEntries = ((ListResource) entry.getResource()).getEntry().size();
break;
}
}

requestBundle = processListEntriesGatewayModeByBundle(responseResource);
requestBundle = processListEntriesGatewayModeByBundle(responseResource, start, count);
}

Bundle resultBundle = fhirR4Client.transaction().withBundle(requestBundle).execute();

// add total
resultBundle.setTotal(requestBundle.getEntry().size());

// add pagination links
int nextPage = page < totalEntries / count ? page + 1 : 0; // 0 indicates no next page
int prevPage = page > 1 ? page - 1 : 0; // 0 indicates no previous page

Bundle.BundleLinkComponent selfLink = new Bundle.BundleLinkComponent();
List<Bundle.BundleLinkComponent> link = new ArrayList<>();
String selfUrl = constructUpdatedUrl(request, parameters);
selfLink.setRelation(IBaseBundle.LINK_SELF);
selfLink.setUrl(selfUrl);
link.add(selfLink);
resultBundle.setLink(link);

if (nextPage > 0) {
parameters.put(
Constants.PAGINATION_PAGE_NUMBER, new String[] {String.valueOf(nextPage)});
String nextUrl = constructUpdatedUrl(request, parameters);
Bundle.BundleLinkComponent nextLink = new Bundle.BundleLinkComponent();
nextLink.setRelation(IBaseBundle.LINK_NEXT);
nextLink.setUrl(nextUrl);
resultBundle.addLink(nextLink);
}
if (prevPage > 0) {
parameters.put(
Constants.PAGINATION_PAGE_NUMBER, new String[] {String.valueOf(prevPage)});
String prevUrl = constructUpdatedUrl(request, parameters);
Bundle.BundleLinkComponent previousLink = new Bundle.BundleLinkComponent();
previousLink.setRelation(IBaseBundle.LINK_PREV);
previousLink.setUrl(prevUrl);
resultBundle.addLink(previousLink);
}

return fhirR4Client.transaction().withBundle(requestBundle).execute();
return resultBundle;
}

private String getSyncTagUrl(String syncStrategy) {
Expand Down Expand Up @@ -369,10 +439,33 @@ private boolean isSyncUrl(RequestDetailsReader requestDetailsReader) {
return false;
}

private static String constructUpdatedUrl(
RequestDetailsReader requestDetails, Map<String, String[]> parameters) {
StringBuilder updatedUrlBuilder = new StringBuilder(requestDetails.getFhirServerBase());

updatedUrlBuilder.append("/").append(requestDetails.getRequestPath());

updatedUrlBuilder.append("?");
for (Map.Entry<String, String[]> entry : parameters.entrySet()) {
String paramName = entry.getKey();
String[] paramValues = entry.getValue();

for (String paramValue : paramValues) {
updatedUrlBuilder.append(paramName).append("=").append(paramValue).append("&");
}
}

// Remove the trailing '&' if present
if (updatedUrlBuilder.charAt(updatedUrlBuilder.length() - 1) == '&') {
updatedUrlBuilder.deleteCharAt(updatedUrlBuilder.length() - 1);
}

return updatedUrlBuilder.toString();
}

private boolean isResourceTypeRequest(String requestPath) {
if (!TextUtils.isEmpty(requestPath)) {
String[] sections =
requestPath.split(com.google.fhir.gateway.ProxyConstants.HTTP_URL_SEPARATOR);
String[] sections = requestPath.split(ProxyConstants.HTTP_URL_SEPARATOR);

return sections.length == 1 || (sections.length == 2 && TextUtils.isEmpty(sections[1]));
}
Expand Down
Loading

0 comments on commit d1d1f21

Please sign in to comment.