diff --git a/src/main/java/com/stripe/model/SearchPagingIterable.java b/src/main/java/com/stripe/model/SearchPagingIterable.java new file mode 100644 index 00000000000..f99803d35ff --- /dev/null +++ b/src/main/java/com/stripe/model/SearchPagingIterable.java @@ -0,0 +1,22 @@ +package com.stripe.model; + +import java.util.Iterator; + +/** + * Provides an {@code Iterable} target that automatically iterates across all API + * pages and which is suitable for use with a {@code foreach} loop. + * + *

Please note SearchPagingIterable is in beta and is subject to change or removal at any time. + */ +public class SearchPagingIterable implements Iterable { + private StripeSearchResultInterface page; + + SearchPagingIterable(final StripeSearchResultInterface page) { + this.page = page; + } + + @Override + public Iterator iterator() { + return new SearchPagingIterator<>(page); + } +} diff --git a/src/main/java/com/stripe/model/SearchPagingIterator.java b/src/main/java/com/stripe/model/SearchPagingIterator.java new file mode 100644 index 00000000000..1e45b76ad5d --- /dev/null +++ b/src/main/java/com/stripe/model/SearchPagingIterator.java @@ -0,0 +1,82 @@ +package com.stripe.model; + +import com.stripe.Stripe; +import com.stripe.net.ApiResource; +import com.stripe.net.RequestOptions; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; + +/** Please note SearchPagingIterator is in beta and is subject to change or removal at any time. */ +public class SearchPagingIterator extends ApiResource implements Iterator { + private final String url; + + @SuppressWarnings("rawtypes") + private final Class collectionType; + + private StripeSearchResultInterface currentSearchResult; + private Iterator currentDataIterator; + + private String nextPage; + + SearchPagingIterator(final StripeSearchResultInterface stripeSearchResult) { + this.url = Stripe.getApiBase() + stripeSearchResult.getUrl(); + this.nextPage = stripeSearchResult.getNextPage(); + + this.collectionType = stripeSearchResult.getClass(); + + this.currentSearchResult = stripeSearchResult; + this.currentDataIterator = stripeSearchResult.getData().iterator(); + } + + @Override + public boolean hasNext() { + return currentDataIterator.hasNext() || currentSearchResult.getHasMore(); + } + + @Override + public T next() { + // if we've run out of data on the current page, try to fetch another + // one + if (!currentDataIterator.hasNext() && currentSearchResult.getHasMore()) { + try { + Map params = new HashMap<>(); + + // copy all the parameters from the initial request + Map initialParams = currentSearchResult.getRequestParams(); + if (initialParams != null) { + params.putAll(initialParams); + } + + // then put our new page start in + params.put("next_page", this.nextPage); + + this.currentSearchResult = search(params, currentSearchResult.getRequestOptions()); + this.nextPage = this.currentSearchResult.getNextPage(); + + this.currentDataIterator = currentSearchResult.getData().iterator(); + } catch (final Exception e) { + throw new RuntimeException("Unable to lazy-load stripe objects", e); + } + } + + if (currentDataIterator.hasNext()) { + final T next = currentDataIterator.next(); + return next; + } + + throw new NoSuchElementException(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + + @SuppressWarnings("unchecked") + private StripeSearchResultInterface search( + final Map params, final RequestOptions options) throws Exception { + return ApiResource.requestSearchResult(url, params, collectionType, options); + } +} diff --git a/src/main/java/com/stripe/model/StripeSearchResult.java b/src/main/java/com/stripe/model/StripeSearchResult.java new file mode 100644 index 00000000000..8c0c563f61a --- /dev/null +++ b/src/main/java/com/stripe/model/StripeSearchResult.java @@ -0,0 +1,63 @@ +package com.stripe.model; + +import com.stripe.net.RequestOptions; +import java.util.List; +import java.util.Map; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.Setter; + +/** + * Provides a representation of a single page worth of data from a Stripe API search method. Please + * note, StripeSearchResult is beta functionality and is subject to change or removal at any time. + */ +@Getter +@Setter +@EqualsAndHashCode(callSuper = false) +public abstract class StripeSearchResult extends StripeObject + implements StripeSearchResultInterface { + String object; + + @Getter(onMethod_ = {@Override}) + List data; + + @Getter(onMethod_ = {@Override}) + Boolean hasMore; + + @Getter(onMethod_ = {@Override}) + String url; + + @Getter(onMethod_ = {@Override}) + String nextPage; + + @Getter(onMethod_ = {@Override}) + @Setter(onMethod = @__({@Override})) + private transient RequestOptions requestOptions; + + @Getter(onMethod_ = {@Override}) + @Setter(onMethod = @__({@Override})) + private Map requestParams; + + public Iterable autoPagingIterable() { + return new SearchPagingIterable<>(this); + } + + public Iterable autoPagingIterable(Map params) { + this.setRequestParams(params); + return new SearchPagingIterable<>(this); + } + + /** + * Constructs an iterable that can be used to iterate across all objects across all pages. As page + * boundaries are encountered, the next page will be fetched automatically for continued + * iteration. + * + * @param params request parameters (will override the parameters from the initial list request) + * @param options request options (will override the options from the initial list request) + */ + public Iterable autoPagingIterable(Map params, RequestOptions options) { + this.setRequestOptions(options); + this.setRequestParams(params); + return new SearchPagingIterable<>(this); + } +} diff --git a/src/main/java/com/stripe/model/StripeSearchResultInterface.java b/src/main/java/com/stripe/model/StripeSearchResultInterface.java new file mode 100644 index 00000000000..01caf0915cd --- /dev/null +++ b/src/main/java/com/stripe/model/StripeSearchResultInterface.java @@ -0,0 +1,9 @@ +package com.stripe.model; + +/** + * Please note, StripeSearchResultInterface is beta functionality and is subject to change or + * removal at any time. + */ +public interface StripeSearchResultInterface extends StripeCollectionInterface { + String getNextPage(); +} diff --git a/src/main/java/com/stripe/net/ApiResource.java b/src/main/java/com/stripe/net/ApiResource.java index c23e7a5beed..0b241340f70 100644 --- a/src/main/java/com/stripe/net/ApiResource.java +++ b/src/main/java/com/stripe/net/ApiResource.java @@ -21,6 +21,7 @@ import com.stripe.model.StripeObjectInterface; import com.stripe.model.StripeRawJsonObject; import com.stripe.model.StripeRawJsonObjectDeserializer; +import com.stripe.model.StripeSearchResultInterface; import com.stripe.util.StringUtils; import java.io.InputStream; import java.io.UnsupportedEncodingException; @@ -223,6 +224,35 @@ public static > T requestCollection( return collection; } + public static > T requestSearchResult( + String url, ApiRequestParams params, Class clazz, RequestOptions options) + throws StripeException { + checkNullTypedParams(url, params); + return requestSearchResult(url, params.toMap(), clazz, options); + } + + /** + * Similar to #request, but specific for use with searchResult types that come from the API + * + *

SearchResults, like collections need a little extra work because we need to plumb request + * options and params through so that we can iterate to the next page if necessary. + * + *

Please note, requestSearchResult is beta functionality and is subject to charge or removal + * at any time. + */ + public static > T requestSearchResult( + String url, Map params, Class clazz, RequestOptions options) + throws StripeException { + T searchResult = request(RequestMethod.GET, url, params, clazz, options); + + if (searchResult != null) { + searchResult.setRequestOptions(options); + searchResult.setRequestParams(params); + } + + return searchResult; + } + /** * Invalidate null typed parameters. *