22
33import 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 ;
59import io .github .jpmorganchase .fusion .api .APIManager ;
610import io .github .jpmorganchase .fusion .api .FusionAPIManager ;
711import io .github .jpmorganchase .fusion .api .exception .APICallException ;
1115import io .github .jpmorganchase .fusion .builders .APIConfiguredBuilders ;
1216import io .github .jpmorganchase .fusion .builders .Builders ;
1317import io .github .jpmorganchase .fusion .http .Client ;
18+ import io .github .jpmorganchase .fusion .http .HttpResponse ;
1419import io .github .jpmorganchase .fusion .http .JdkClient ;
1520import io .github .jpmorganchase .fusion .model .*;
1621import io .github .jpmorganchase .fusion .oauth .credential .BearerTokenCredentials ;
3641import java .util .Map ;
3742import java .util .Objects ;
3843import 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
50+ @ SuppressWarnings ({"LombokSetterMayBeUsed" , "LombokGetterMayBeUsed" })
4351public class Fusion {
4452
4553 private static final DateTimeFormatter dateTimeFormatter = DateTimeFormatter .ofPattern ("yyyy-MM-dd" );
4654
55+ @ SuppressWarnings ({"FieldCanBeLocal" , "FieldMayBeFinal" })
56+ private static int defaultPageSize = -1 ;
57+
4758 private final APIManager api ;
4859 private String defaultCatalog ;
4960 private final String defaultPath ;
@@ -195,6 +206,91 @@ private Map<String, Map<String, Object>> callForMap(String url) {
195206 return responseParser .parseResourcesUntyped (json );
196207 }
197208
209+ /**
210+ * Makes paginated API calls and aggregates all results transparently.
211+ * This method handles the pagination logic internally, making multiple API calls
212+ * as needed and combining the results into a single response string.
213+ *
214+ * @param url the API endpoint URL
215+ * @return aggregated JSON response containing all pages of data
216+ */
217+ private String callAPIWithPagination (String url ) {
218+ log .debug ("Starting paginated request to URL: {}" , url );
219+
220+ Map <String , String > headers = new HashMap <>();
221+ headers .put ("x-jpmc-paginate" , "true" );
222+ if (defaultPageSize > 0 ) {
223+ log .debug ("Using page size: {}" , defaultPageSize );
224+ headers .put ("x-jpmc-page-size" , String .valueOf (defaultPageSize ));
225+ }
226+
227+ Gson gson = new Gson ();
228+ JsonArray aggregatedResources = new JsonArray ();
229+ String nextToken = null ;
230+ int pageCount = 0 ;
231+
232+ do {
233+ pageCount ++;
234+ if (nextToken != null ) {
235+ headers .put ("x-jpmc-next-token" , nextToken );
236+ log .debug ("Fetching page {} with next token" , pageCount );
237+ } else {
238+ log .debug ("Fetching page {}" , pageCount );
239+ }
240+
241+ HttpResponse <String > response = this .api .callAPIWithResponse (url , headers );
242+ String pageJson = response .getBody ();
243+
244+ JsonObject pageObject = JsonParser .parseString (pageJson ).getAsJsonObject ();
245+ if (pageObject .has ("resources" ) && pageObject .get ("resources" ).isJsonArray ()) {
246+ JsonArray pageResources = pageObject .getAsJsonArray ("resources" );
247+ int pageResourceCount = pageResources .size ();
248+ pageResources .forEach (aggregatedResources ::add );
249+ log .debug ("Retrieved {} resources from page {}" , pageResourceCount , pageCount );
250+ }
251+
252+ nextToken = getHeaderValue (response .getHeaders (), "x-jpmc-next-token" );
253+
254+ if (nextToken != null && !nextToken .isEmpty ()) {
255+ log .debug ("Next token received, more pages available" );
256+ }
257+
258+ } while (nextToken != null && !nextToken .isEmpty ());
259+
260+ log .debug (
261+ "Pagination complete. Total pages fetched: {}, Total resources: {}" ,
262+ pageCount ,
263+ aggregatedResources .size ());
264+
265+ JsonObject result = new JsonObject ();
266+ result .add ("resources" , aggregatedResources );
267+ return gson .toJson (result );
268+ }
269+
270+ /**
271+ * Gets a header value from the response headers map (case-insensitive).
272+ *
273+ * @param headers the response headers map
274+ * @param headerName the header name to look for
275+ * @return the header value, or null if not found
276+ */
277+ @ SuppressWarnings ("SameParameterValue" )
278+ private String getHeaderValue (Map <String , List <String >> headers , String headerName ) {
279+ if (headers == null || headerName == null ) {
280+ return null ;
281+ }
282+
283+ for (Map .Entry <String , List <String >> entry : headers .entrySet ()) {
284+ if (entry .getKey () != null && entry .getKey ().equalsIgnoreCase (headerName )) {
285+ List <String > values = entry .getValue ();
286+ if (values != null && !values .isEmpty ()) {
287+ return values .get (0 );
288+ }
289+ }
290+ }
291+ return null ;
292+ }
293+
198294 /**
199295 * Get a list of the catalogs available to the API account.
200296 *
@@ -203,7 +299,8 @@ private Map<String, Map<String, Object>> callForMap(String url) {
203299 * @throws OAuthException if a token could not be retrieved for authentication
204300 */
205301 public Map <String , Catalog > listCatalogs () {
206- String json = this .api .callAPI (rootURL .concat ("catalogs" ));
302+ String url = rootURL .concat ("catalogs" );
303+ String json = callAPIWithPagination (url );
207304 return responseParser .parseCatalogResponse (json );
208305 }
209306
@@ -223,7 +320,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
223320 /**
224321 * Get a filtered list of the data products in the specified catalog
225322 * <p>
226- * Note that as of current version this search capability is not yet implemented
323+ * Note that as of the current version, this search capability is not yet implemented
227324 *
228325 * @param catalogName identifier of the catalog to be queried
229326 * @param contains a search keyword.
@@ -235,7 +332,7 @@ public Map<String, Map<String, Object>> catalogResources(String catalogName) {
235332 public Map <String , DataProduct > listProducts (String catalogName , String contains , boolean idContains ) {
236333 // TODO: unimplemented logic implied by the method parameters
237334 String url = String .format ("%1scatalogs/%2s/products" , this .rootURL , catalogName );
238- String json = this . api . callAPI (url );
335+ String json = callAPIWithPagination (url );
239336 return responseParser .parseDataProductResponse (json );
240337 }
241338
@@ -266,7 +363,7 @@ public Map<String, DataProduct> listProducts() {
266363 /**
267364 * Get a filtered list of the datasets in the specified catalog
268365 * <p>
269- * Note that as of current version this search capability is not yet implemented
366+ * Note that as of the current version, this search capability is not yet implemented
270367 *
271368 * @param catalogName identifier of the catalog to be queried
272369 * @param contains a search keyword.
@@ -277,7 +374,7 @@ public Map<String, DataProduct> listProducts() {
277374 */
278375 public Map <String , Dataset > listDatasets (String catalogName , String contains , boolean idContains ) {
279376 String url = String .format ("%1scatalogs/%2s/datasets" , this .rootURL , catalogName );
280- String json = this . api . callAPI (url );
377+ String json = callAPIWithPagination (url );
281378 return filterDatasets (responseParser .parseDatasetResponse (json , catalogName ), contains , idContains );
282379 }
283380
@@ -400,7 +497,7 @@ public Map<String, Map<String, Object>> datasetResources(String dataset) {
400497 */
401498 public Map <String , DatasetSeries > listDatasetMembers (String catalogName , String dataset ) {
402499 String url = String .format ("%1scatalogs/%2s/datasets/%3s/datasetseries" , this .rootURL , catalogName , dataset );
403- String json = this . api . callAPI (url );
500+ String json = callAPIWithPagination (url );
404501 return responseParser .parseDatasetSeriesResponse (json );
405502 }
406503
@@ -460,7 +557,7 @@ public Map<String, Map<String, Object>> datasetMemberResources(String dataset, S
460557 */
461558 public Map <String , Attribute > listAttributes (String catalogName , String dataset ) {
462559 String url = String .format ("%1scatalogs/%2s/datasets/%3s/attributes" , this .rootURL , catalogName , dataset );
463- String json = this . api . callAPI (url );
560+ String json = callAPIWithPagination (url );
464561 return responseParser .parseAttributeResponse (json , catalogName , dataset );
465562 }
466563
@@ -502,11 +599,10 @@ public Map<String, Map<String, Object>> attributeResources(String catalogName, S
502599 * @throws OAuthException if a token could not be retrieved for authentication
503600 */
504601 public Map <String , Distribution > listDistributions (String catalogName , String dataset , String seriesMember ) {
505-
506602 String url = String .format (
507603 "%1scatalogs/%2s/datasets/%3s/datasetseries/%4s/distributions" ,
508604 this .rootURL , catalogName , dataset , seriesMember );
509- String json = this . api . callAPI (url );
605+ String json = callAPIWithPagination (url );
510606 return responseParser .parseDistributionResponse (json );
511607 }
512608
0 commit comments