Skip to content

Commit 0925367

Browse files
committed
Implement pagination for API calls
1 parent f5760dd commit 0925367

4 files changed

Lines changed: 343 additions & 15 deletions

File tree

src/main/java/io/github/jpmorganchase/fusion/Fusion.java

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import static io.github.jpmorganchase.fusion.filter.DatasetFilter.filterDatasets;
44

5+
import com.google.gson.Gson;
6+
import com.google.gson.JsonArray;
7+
import com.google.gson.JsonObject;
8+
import com.google.gson.JsonParser;
59
import io.github.jpmorganchase.fusion.api.APIManager;
610
import io.github.jpmorganchase.fusion.api.FusionAPIManager;
711
import io.github.jpmorganchase.fusion.api.exception.APICallException;
@@ -11,6 +15,7 @@
1115
import io.github.jpmorganchase.fusion.builders.APIConfiguredBuilders;
1216
import io.github.jpmorganchase.fusion.builders.Builders;
1317
import io.github.jpmorganchase.fusion.http.Client;
18+
import io.github.jpmorganchase.fusion.http.HttpResponse;
1419
import io.github.jpmorganchase.fusion.http.JdkClient;
1520
import io.github.jpmorganchase.fusion.model.*;
1621
import io.github.jpmorganchase.fusion.oauth.credential.BearerTokenCredentials;
@@ -195,6 +200,72 @@ private Map<String, Map<String, Object>> callForMap(String url) {
195200
return responseParser.parseResourcesUntyped(json);
196201
}
197202

203+
/**
204+
* Makes paginated API calls and aggregates all results transparently.
205+
* This method handles the pagination logic internally, making multiple API calls
206+
* as needed and combining the results into a single response string.
207+
*
208+
* @param url the API endpoint URL
209+
* @param pageSize the number of items per page (null to use API default)
210+
* @return aggregated JSON response containing all pages of data
211+
*/
212+
private String callAPIWithPagination(String url, Integer pageSize) {
213+
Map<String, String> headers = new HashMap<>();
214+
headers.put("x-jpmc-paginate", "true");
215+
if (pageSize != null) {
216+
headers.put("x-jpmc-page-size", String.valueOf(pageSize));
217+
}
218+
219+
Gson gson = new Gson();
220+
JsonArray aggregatedResources = new JsonArray();
221+
String nextToken = null;
222+
223+
do {
224+
if (nextToken != null) {
225+
headers.put("x-next-token", nextToken);
226+
}
227+
228+
HttpResponse<String> response = this.api.callAPIWithResponse(url, headers);
229+
String pageJson = response.getBody();
230+
231+
JsonObject pageObject = JsonParser.parseString(pageJson).getAsJsonObject();
232+
if (pageObject.has("resources") && pageObject.get("resources").isJsonArray()) {
233+
JsonArray pageResources = pageObject.getAsJsonArray("resources");
234+
pageResources.forEach(aggregatedResources::add);
235+
}
236+
237+
nextToken = getHeaderValue(response.getHeaders(), "x-next-token");
238+
239+
} while (nextToken != null && !nextToken.isEmpty());
240+
241+
JsonObject result = new JsonObject();
242+
result.add("resources", aggregatedResources);
243+
return gson.toJson(result);
244+
}
245+
246+
/**
247+
* Gets a header value from the response headers map (case-insensitive).
248+
*
249+
* @param headers the response headers map
250+
* @param headerName the header name to look for
251+
* @return the header value, or null if not found
252+
*/
253+
private String getHeaderValue(Map<String, java.util.List<String>> headers, String headerName) {
254+
if (headers == null || headerName == null) {
255+
return null;
256+
}
257+
258+
for (Map.Entry<String, java.util.List<String>> entry : headers.entrySet()) {
259+
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(headerName)) {
260+
java.util.List<String> values = entry.getValue();
261+
if (values != null && !values.isEmpty()) {
262+
return values.get(0);
263+
}
264+
}
265+
}
266+
return null;
267+
}
268+
198269
/**
199270
* Get a list of the catalogs available to the API account.
200271
*
@@ -223,7 +294,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
223294
/**
224295
* Get a filtered list of the data products in the specified catalog
225296
* <p>
226-
* Note that as of current version this search capability is not yet implemented
297+
* Note that as of the current version, this search capability is not yet implemented
227298
*
228299
* @param catalogName identifier of the catalog to be queried
229300
* @param contains a search keyword.
@@ -235,7 +306,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
235306
public Map<String, DataProduct> listProducts(String catalogName, String contains, boolean idContains) {
236307
// TODO: unimplemented logic implied by the method parameters
237308
String url = String.format("%1scatalogs/%2s/products", this.rootURL, catalogName);
238-
String json = this.api.callAPI(url);
309+
String json = callAPIWithPagination(url, null);
239310
return responseParser.parseDataProductResponse(json);
240311
}
241312

@@ -266,7 +337,7 @@ public Map<String, DataProduct> listProducts() {
266337
/**
267338
* Get a filtered list of the datasets in the specified catalog
268339
* <p>
269-
* Note that as of current version this search capability is not yet implemented
340+
* Note that as of the current version, this search capability is not yet implemented
270341
*
271342
* @param catalogName identifier of the catalog to be queried
272343
* @param contains a search keyword.
@@ -277,7 +348,7 @@ public Map<String, DataProduct> listProducts() {
277348
*/
278349
public Map<String, Dataset> listDatasets(String catalogName, String contains, boolean idContains) {
279350
String url = String.format("%1scatalogs/%2s/datasets", this.rootURL, catalogName);
280-
String json = this.api.callAPI(url);
351+
String json = callAPIWithPagination(url, null);
281352
return filterDatasets(responseParser.parseDatasetResponse(json, catalogName), contains, idContains);
282353
}
283354

@@ -421,7 +492,7 @@ public Map<String, Map<String, Object>> datasetMemberResources(String dataset, S
421492
*/
422493
public Map<String, Attribute> listAttributes(String catalogName, String dataset) {
423494
String url = String.format("%1scatalogs/%2s/datasets/%3s/attributes", this.rootURL, catalogName, dataset);
424-
String json = this.api.callAPI(url);
495+
String json = callAPIWithPagination(url, null);
425496
return responseParser.parseAttributeResponse(json, catalogName, dataset);
426497
}
427498

src/main/java/io/github/jpmorganchase/fusion/api/APIManager.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@
33
import io.github.jpmorganchase.fusion.api.exception.APICallException;
44
import io.github.jpmorganchase.fusion.api.operations.APIDownloadOperations;
55
import io.github.jpmorganchase.fusion.api.operations.APIUploadOperations;
6+
import io.github.jpmorganchase.fusion.http.HttpResponse;
67
import java.io.UnsupportedEncodingException;
78
import java.net.MalformedURLException;
89
import java.net.URL;
910
import java.net.URLEncoder;
11+
import java.util.Map;
1012

1113
public interface APIManager extends APIDownloadOperations, APIUploadOperations {
1214

@@ -19,6 +21,16 @@ public interface APIManager extends APIDownloadOperations, APIUploadOperations {
1921
*/
2022
String callAPI(String apiPath) throws APICallException;
2123

24+
/**
25+
* Sends a GET request to the specified API endpoint with custom headers and returns the full HTTP response.
26+
*
27+
* @param apiPath the API endpoint path to which the GET request will be sent
28+
* @param headers additional HTTP headers to include in the request
29+
* @return the full {@code HttpResponse} including headers and body
30+
* @throws APICallException if the response status indicates an error or the request fails
31+
*/
32+
HttpResponse<String> callAPIWithResponse(String apiPath, Map<String, String> headers) throws APICallException;
33+
2234
String callAPIToPost(String apiPath) throws APICallException;
2335

2436
/**

src/main/java/io/github/jpmorganchase/fusion/api/FusionAPIManager.java

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,39 @@ public String callAPI(String apiPath) throws APICallException {
6363
return response.getBody();
6464
}
6565

66+
/**
67+
* Sends a GET request to the specified API endpoint with custom headers and returns the full HTTP response.
68+
*
69+
* <p>This method constructs the necessary authorization headers using a bearer token from
70+
* the {@code tokenProvider}, merges them with the provided custom headers, and sends a GET
71+
* request to the specified {@code apiPath} using the {@code httpClient}. It checks the HTTP
72+
* response status for errors and returns the full response including headers.
73+
*
74+
* @param apiPath the API endpoint path to which the GET request will be sent
75+
* @param customHeaders additional HTTP headers to include in the request
76+
* @return the full {@code HttpResponse} including status, headers, and body
77+
* @throws APICallException if the response status indicates an error or the request fails
78+
*/
79+
@Override
80+
public HttpResponse<String> callAPIWithResponse(String apiPath, Map<String, String> customHeaders)
81+
throws APICallException {
82+
Map<String, String> requestHeaders = new HashMap<>();
83+
requestHeaders.put("Authorization", "Bearer " + tokenProvider.getSessionBearerToken());
84+
85+
// Merge custom headers, allowing them to override defaults except Authorization
86+
if (customHeaders != null) {
87+
customHeaders.forEach((key, value) -> {
88+
if (!"Authorization".equalsIgnoreCase(key)) {
89+
requestHeaders.put(key, value);
90+
}
91+
});
92+
}
93+
94+
HttpResponse<String> response = httpClient.get(APIManager.encodeUrl(apiPath), requestHeaders);
95+
checkResponseStatus(response);
96+
return response;
97+
}
98+
6699
@Override
67100
public String callAPIToPost(String apiPath) throws APICallException {
68101
Map<String, String> requestHeaders = new HashMap<>();

0 commit comments

Comments
 (0)