Skip to content

Commit 7880844

Browse files
committed
Implement paginated API calls
1 parent f5760dd commit 7880844

4 files changed

Lines changed: 492 additions & 31 deletions

File tree

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

Lines changed: 101 additions & 9 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;
@@ -36,12 +41,15 @@
3641
import java.util.Map;
3742
import java.util.Objects;
3843
import lombok.Builder;
44+
import lombok.extern.slf4j.Slf4j;
3945

4046
/**
4147
* Class representing the Fusion API, providing methods that correspond to available API endpoints
4248
*/
49+
@Slf4j
4350
public class Fusion {
4451

52+
private static final int DEFAULT_PAGE_SIZE = -1;
4553
private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
4654

4755
private final APIManager api;
@@ -195,6 +203,90 @@ private Map<String, Map<String, Object>> callForMap(String url) {
195203
return responseParser.parseResourcesUntyped(json);
196204
}
197205

206+
/**
207+
* Makes paginated API calls and aggregates all results transparently.
208+
* This method handles the pagination logic internally, making multiple API calls
209+
* as needed and combining the results into a single response string.
210+
*
211+
* @param url the API endpoint URL
212+
* @return aggregated JSON response containing all pages of data
213+
*/
214+
private String callAPIWithPagination(String url) {
215+
log.debug("Starting paginated request to URL: {}", url);
216+
217+
Map<String, String> headers = new HashMap<>();
218+
headers.put("x-jpmc-paginate", "true");
219+
if (DEFAULT_PAGE_SIZE > 0) {
220+
log.debug("Using page size: {}", DEFAULT_PAGE_SIZE);
221+
headers.put("x-jpmc-page-size", String.valueOf(DEFAULT_PAGE_SIZE));
222+
}
223+
224+
Gson gson = new Gson();
225+
JsonArray aggregatedResources = new JsonArray();
226+
String nextToken = null;
227+
int pageCount = 0;
228+
229+
do {
230+
pageCount++;
231+
if (nextToken != null) {
232+
headers.put("x-jpmc-next-token", nextToken);
233+
log.debug("Fetching page {} with next token", pageCount);
234+
} else {
235+
log.debug("Fetching page {}", pageCount);
236+
}
237+
238+
HttpResponse<String> response = this.api.callAPIWithResponse(url, headers);
239+
String pageJson = response.getBody();
240+
241+
JsonObject pageObject = JsonParser.parseString(pageJson).getAsJsonObject();
242+
if (pageObject.has("resources") && pageObject.get("resources").isJsonArray()) {
243+
JsonArray pageResources = pageObject.getAsJsonArray("resources");
244+
int pageResourceCount = pageResources.size();
245+
pageResources.forEach(aggregatedResources::add);
246+
log.debug("Retrieved {} resources from page {}", pageResourceCount, pageCount);
247+
}
248+
249+
nextToken = getHeaderValue(response.getHeaders(), "x-jpmc-next-token");
250+
251+
if (nextToken != null && !nextToken.isEmpty()) {
252+
log.debug("Next token received, more pages available");
253+
}
254+
255+
} while (nextToken != null && !nextToken.isEmpty());
256+
257+
log.debug(
258+
"Pagination complete. Total pages fetched: {}, Total resources: {}",
259+
pageCount,
260+
aggregatedResources.size());
261+
262+
JsonObject result = new JsonObject();
263+
result.add("resources", aggregatedResources);
264+
return gson.toJson(result);
265+
}
266+
267+
/**
268+
* Gets a header value from the response headers map (case-insensitive).
269+
*
270+
* @param headers the response headers map
271+
* @param headerName the header name to look for
272+
* @return the header value, or null if not found
273+
*/
274+
private String getHeaderValue(Map<String, java.util.List<String>> headers, String headerName) {
275+
if (headers == null || headerName == null) {
276+
return null;
277+
}
278+
279+
for (Map.Entry<String, java.util.List<String>> entry : headers.entrySet()) {
280+
if (entry.getKey() != null && entry.getKey().equalsIgnoreCase(headerName)) {
281+
java.util.List<String> values = entry.getValue();
282+
if (values != null && !values.isEmpty()) {
283+
return values.get(0);
284+
}
285+
}
286+
}
287+
return null;
288+
}
289+
198290
/**
199291
* Get a list of the catalogs available to the API account.
200292
*
@@ -203,7 +295,8 @@ private Map<String, Map<String, Object>> callForMap(String url) {
203295
* @throws OAuthException if a token could not be retrieved for authentication
204296
*/
205297
public Map<String, Catalog> listCatalogs() {
206-
String json = this.api.callAPI(rootURL.concat("catalogs"));
298+
String url = rootURL.concat("catalogs");
299+
String json = callAPIWithPagination(url);
207300
return responseParser.parseCatalogResponse(json);
208301
}
209302

@@ -223,7 +316,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
223316
/**
224317
* Get a filtered list of the data products in the specified catalog
225318
* <p>
226-
* Note that as of current version this search capability is not yet implemented
319+
* Note that as of the current version, this search capability is not yet implemented
227320
*
228321
* @param catalogName identifier of the catalog to be queried
229322
* @param contains a search keyword.
@@ -235,7 +328,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
235328
public Map<String, DataProduct> listProducts(String catalogName, String contains, boolean idContains) {
236329
// TODO: unimplemented logic implied by the method parameters
237330
String url = String.format("%1scatalogs/%2s/products", this.rootURL, catalogName);
238-
String json = this.api.callAPI(url);
331+
String json = callAPIWithPagination(url);
239332
return responseParser.parseDataProductResponse(json);
240333
}
241334

@@ -266,7 +359,7 @@ public Map<String, DataProduct> listProducts() {
266359
/**
267360
* Get a filtered list of the datasets in the specified catalog
268361
* <p>
269-
* Note that as of current version this search capability is not yet implemented
362+
* Note that as of the current version, this search capability is not yet implemented
270363
*
271364
* @param catalogName identifier of the catalog to be queried
272365
* @param contains a search keyword.
@@ -277,7 +370,7 @@ public Map<String, DataProduct> listProducts() {
277370
*/
278371
public Map<String, Dataset> listDatasets(String catalogName, String contains, boolean idContains) {
279372
String url = String.format("%1scatalogs/%2s/datasets", this.rootURL, catalogName);
280-
String json = this.api.callAPI(url);
373+
String json = callAPIWithPagination(url);
281374
return filterDatasets(responseParser.parseDatasetResponse(json, catalogName), contains, idContains);
282375
}
283376

@@ -361,7 +454,7 @@ public Map<String, Map<String, Object>> datasetResources(String dataset) {
361454
*/
362455
public Map<String, DatasetSeries> listDatasetMembers(String catalogName, String dataset) {
363456
String url = String.format("%1scatalogs/%2s/datasets/%3s/datasetseries", this.rootURL, catalogName, dataset);
364-
String json = this.api.callAPI(url);
457+
String json = callAPIWithPagination(url);
365458
return responseParser.parseDatasetSeriesResponse(json);
366459
}
367460

@@ -421,7 +514,7 @@ public Map<String, Map<String, Object>> datasetMemberResources(String dataset, S
421514
*/
422515
public Map<String, Attribute> listAttributes(String catalogName, String dataset) {
423516
String url = String.format("%1scatalogs/%2s/datasets/%3s/attributes", this.rootURL, catalogName, dataset);
424-
String json = this.api.callAPI(url);
517+
String json = callAPIWithPagination(url);
425518
return responseParser.parseAttributeResponse(json, catalogName, dataset);
426519
}
427520

@@ -463,11 +556,10 @@ public Map<String, Map<String, Object>> attributeResources(String catalogName, S
463556
* @throws OAuthException if a token could not be retrieved for authentication
464557
*/
465558
public Map<String, Distribution> listDistributions(String catalogName, String dataset, String seriesMember) {
466-
467559
String url = String.format(
468560
"%1scatalogs/%2s/datasets/%3s/datasetseries/%4s/distributions",
469561
this.rootURL, catalogName, dataset, seriesMember);
470-
String json = this.api.callAPI(url);
562+
String json = callAPIWithPagination(url);
471563
return responseParser.parseDistributionResponse(json);
472564
}
473565

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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,38 @@ 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+
if (customHeaders != null) {
86+
customHeaders.forEach((key, value) -> {
87+
if (!"Authorization".equalsIgnoreCase(key)) {
88+
requestHeaders.put(key, value);
89+
}
90+
});
91+
}
92+
93+
HttpResponse<String> response = httpClient.get(APIManager.encodeUrl(apiPath), requestHeaders);
94+
checkResponseStatus(response);
95+
return response;
96+
}
97+
6698
@Override
6799
public String callAPIToPost(String apiPath) throws APICallException {
68100
Map<String, String> requestHeaders = new HashMap<>();

0 commit comments

Comments
 (0)