, Item> {
+
+ static class SinglePageEndpoint, I> extends PaginatedEndpoint
{
+ private final P page;
+
+ SinglePageEndpoint(P page, Class itemType) {
+ super(null, null, (Class
) page.getClass(), itemType, null);
+ this.page = page;
+ }
+
+ @Nonnull
+ @Override
+ public PaginatedEndpointPages
pages() {
+ return PaginatedEndpointPages.fromSinglePage(pageType, page);
+ }
+
+ }
+
+ static PaginatedEndpoint, I> fromSinglePage(I[] array, Class itemType) {
+ return fromSinglePage(new GitHubPageArrayAdapter<>(array), itemType);
+ }
+
+ static , I> PaginatedEndpoint
fromSinglePage(P page, Class itemType) {
+ return new SinglePageEndpoint<>(page, itemType);
+ }
+
+ static PaginatedEndpoint, I> ofArrayEndpoint(GitHubClient client,
+ GitHubRequest request,
+ Class receiverType,
+ Consumer itemInitializer) {
+ Class itemType = (Class) receiverType.getComponentType();
+ return new PaginatedArrayEndpoint(client, request, receiverType, itemType, itemInitializer);
+ }
+
+ /**
+ * Eagerly walk {@link Iterator} of {@link GitHubPage} and return the result in an array.
+ *
+ * @param pageIterator
+ * the {@link Iterator} of {@link GitHubPage} to read
+ * @return an array of all elements from the {@link Iterator} of pages
+ * @throws IOException
+ * if an I/O exception occurs.
+ */
+ static List toList(final Iterator extends GitHubPage> pageIterator, Class itemType)
+ throws IOException {
+ try {
+ ArrayList pageList = new ArrayList<>();
+ pageIterator.forEachRemaining(page -> {
+ pageList.addAll(Arrays.asList(page.getItems()));
+ });
+ return pageList;
+ } catch (GHException e) {
+ // if there was an exception inside the iterator it is wrapped as a GHException
+ // if the wrapped exception is an IOException, throw that
+ if (e.getCause() instanceof IOException) {
+ throw (IOException) e.getCause();
+ } else {
+ throw e;
+ }
+ }
+ }
+
+ protected final GitHubClient client;
+ protected final Consumer- itemInitializer;
+
+ protected final Class
- itemType;
+ /**
+ * Page size. 0 is default.
+ */
+ protected int pageSize = 0;
+
+ protected final Class pageType;
+
+ protected final GitHubRequest request;
+
+ /**
+ * Instantiates a new git hub page contents iterable.
+ *
+ * @param client
+ * the client
+ * @param request
+ * the request
+ * @param pageType
+ * the receiver type
+ * @param itemInitializer
+ * the item initializer
+ */
+ PaginatedEndpoint(GitHubClient client,
+ GitHubRequest request,
+ Class pageType,
+ Class
- itemType,
+ Consumer
- itemInitializer) {
+ this.client = client;
+ this.request = request;
+ this.pageType = pageType;
+ this.itemType = itemType;
+ this.itemInitializer = itemInitializer;
+ }
+
+ @Nonnull
+ public final PaginatedEndpointItems items() {
+ return new PaginatedEndpointItems<>(this.pages());
+ }
+
+ /**
+ *
+ * @return
+ */
+ @Nonnull
+ public PaginatedEndpointPages pages() {
+ return new PaginatedEndpointPages<>(client, pageType, request, pageSize, itemInitializer);
+ }
+
+ /**
+ * Eagerly walk {@link Iterable} and return the result in an array.
+ *
+ * @return the list
+ * @throws IOException
+ * if an I/O exception occurs.
+ */
+ @Nonnull
+ public final Item[] toArray() throws IOException {
+ return toList().toArray((Item[]) Array.newInstance(itemType, 0));
+ }
+
+ /**
+ * Eagerly walk {@link Iterable} and return the result in a list.
+ *
+ * @return the list
+ * @throws IOException
+ * if an I/O Exception occurs
+ */
+ @Nonnull
+ public final List
- toList() throws IOException {
+ return Collections.unmodifiableList(toList(pages(), itemType));
+ }
+
+ /**
+ * Eagerly walk {@link Iterable} and return the result in a set.
+ *
+ * @return the set
+ * @throws IOException
+ * if an I/O Exception occurs
+ */
+ @Nonnull
+ public final Set
- toSet() throws IOException {
+ return Collections.unmodifiableSet(new LinkedHashSet<>(toList()));
+ }
+
+ /**
+ * Sets the pagination size.
+ *
+ *
+ * When set to non-zero, each API call will retrieve this many entries.
+ *
+ * @param size
+ * the size
+ * @return the paged iterable
+ */
+ public final PaginatedEndpoint withPageSize(int size) {
+ this.pageSize = size;
+ return this;
+ }
+
+ /**
+ * Eagerly walk {@link Iterable} and return the result in a {@link GitHubResponse} containing an array of {@code T}
+ * items.
+ *
+ * @return the last response with an array containing all the results from all pages.
+ * @throws IOException
+ * if an I/O exception occurs.
+ */
+ @Nonnull
+ final GitHubResponse- toResponse() throws IOException {
+ PaginatedEndpointPages iterator = pages();
+ Item[] items = toList(iterator, itemType).toArray((Item[]) Array.newInstance(itemType, 0));
+ GitHubResponse lastResponse = iterator.finalResponse();
+ return new GitHubResponse<>(lastResponse, items);
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/PaginatedEndpointItems.java b/src/main/java/org/kohsuke/github/PaginatedEndpointItems.java
new file mode 100644
index 0000000000..099213f601
--- /dev/null
+++ b/src/main/java/org/kohsuke/github/PaginatedEndpointItems.java
@@ -0,0 +1,130 @@
+package org.kohsuke.github;
+
+import java.util.*;
+
+/**
+ * This class is not thread-safe. Any one instance should only be called from a single thread.
+ */
+class PaginatedEndpointItems, Item> implements Iterator
- {
+
+ /**
+ * Current batch of items. Each time {@link #next()} is called the next item in this array will be returned. After
+ * the last item of the array is returned, when {@link #next()} is called again, a new page of items will be fetched
+ * and iterating will continue from the first item in the new page.
+ *
+ * @see #fetchNext() {@link #fetchNext()} for details on how this field is used.
+ */
+ private Page currentPage;
+
+ /**
+ * The index of the next item on the page, the item that will be returned when {@link #next()} is called.
+ *
+ * @see #fetchNext() {@link #fetchNext()} for details on how this field is used.
+ */
+ private int nextItemIndex;
+
+ private final PaginatedEndpointPages pageIterator;
+
+ PaginatedEndpointItems(PaginatedEndpointPages pageIterator) {
+ this.pageIterator = pageIterator;
+ }
+
+ /**
+ * Get the current page.
+ *
+ * If not previously fetched, will attempt fetch the first page. Will still return the last page even after
+ * hasNext() returns false.
+ */
+ public Page getCurrentPage() {
+ if (currentPage == null) {
+ peek();
+ }
+ return currentPage;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean hasNext() {
+ return peek() != null;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ public Item next() {
+ Item result = peek();
+ if (result == null)
+ throw new NoSuchElementException();
+ nextItemIndex++;
+ return result;
+ }
+
+ /**
+ * Gets the next page worth of data.
+ *
+ * @return the list
+ */
+ @Deprecated
+ public List
- nextPage() {
+ // if we have not fetched any pages yet, always fetch.
+ // If we have fetched at least one page, check hasNext()
+ if (currentPage == null) {
+ peek();
+ } else if (!hasNext()) {
+ throw new NoSuchElementException();
+ }
+
+ // Current should never be null after fetch
+ Objects.requireNonNull(currentPage);
+ Item[] r = currentPage.getItems();
+ if (nextItemIndex != 0) {
+ r = Arrays.copyOfRange(r, nextItemIndex, r.length);
+ }
+ nextItemIndex = currentPage.getItems().length;
+ return Arrays.asList(r);
+ }
+
+ /**
+ *
+ * @return
+ */
+ public Item peek() {
+ Item result = lookupItem();
+ if (result == null && pageIterator.hasNext()) {
+ result = fetchNext();
+ }
+ return result;
+ }
+
+ /**
+ * Fetch is called at the start of {@link #next()} or {@link #hasNext()} to fetch another page of data if it is
+ * needed and available.
+ *
+ * If there is no current page yet (at the start of iterating), a page is fetched. If {@link #nextItemIndex} points
+ * to an item in the current page array, the state is valid - no more work is needed. If {@link #nextItemIndex} is
+ * greater than the last index in the current page array, the method checks if there is another page of data
+ * available.
+ *
+ *
+ * If there is another page, get that page of data and reset the check {@link #nextItemIndex} to the start of the
+ * new page.
+ *
+ *
+ * If no more pages are available, leave the page and index unchanged. In this case, {@link #hasNext()} will return
+ * {@code false} and {@link #next()} will throw an exception.
+ *
+ */
+ private Item fetchNext() {
+ // On first call, always get next page (may be empty array)
+ currentPage = Objects.requireNonNull(pageIterator.next());
+ nextItemIndex = 0;
+ return lookupItem();
+ }
+
+ private Item lookupItem() {
+ return currentPage != null && currentPage.getItems().length > nextItemIndex
+ ? currentPage.getItems()[nextItemIndex]
+ : null;
+ }
+}
diff --git a/src/main/java/org/kohsuke/github/GitHubPageIterator.java b/src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
similarity index 57%
rename from src/main/java/org/kohsuke/github/GitHubPageIterator.java
rename to src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
index 4a831bf3f8..bb1e43f243 100644
--- a/src/main/java/org/kohsuke/github/GitHubPageIterator.java
+++ b/src/main/java/org/kohsuke/github/PaginatedEndpointPages.java
@@ -1,64 +1,40 @@
package org.kohsuke.github;
+import org.jetbrains.annotations.NotNull;
+
import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.NoSuchElementException;
+import java.util.function.Consumer;
import javax.annotation.Nonnull;
-// TODO: Auto-generated Javadoc
/**
- * May be used for any item that has pagination information. Iterates over paginated {@code T} objects (not the items
- * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse}{@code }
- * after iterating completes.
- *
+ * May be used for any item that has pagination information. Iterates over paginated {@code P} objects (not the items
+ * inside the page). Also exposes {@link #finalResponse()} to allow getting a full {@link GitHubResponse} after
+ * iterating completes.
+ *
* Works for array responses, also works for search results which are single instances with an array of items inside.
- *
+ *
* This class is not thread-safe. Any one instance should only be called from a single thread.
*
* @author Liam Newman
- * @param
+ * @param
* type of each page (not the items in the page).
*/
-class GitHubPageIterator implements Iterator {
-
- /**
- * Loads paginated resources.
- *
- * @param
- * type of each page (not the items in the page).
- * @param client
- * the {@link GitHubClient} from which to request responses
- * @param type
- * type of each page (not the items in the page).
- * @param request
- * the request
- * @param pageSize
- * the page size
- * @return iterator
- */
- static