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

Add support for pagination of list entries #32

Merged
merged 7 commits into from
Feb 7, 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 @@ -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;
lincmba marked this conversation as resolved.
Show resolved Hide resolved
}

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
Loading