diff --git a/pom.xml b/pom.xml index 992b1a32cf..fee634c580 100644 --- a/pom.xml +++ b/pom.xml @@ -145,6 +145,7 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + 2.20.0 com.github.spotbugs @@ -545,6 +546,14 @@ org.kohsuke.github.internal + + org.kohsuke.github.PagedIterable#_iterator(int) + org.kohsuke.github.PagedIterable#toArray(org.kohsuke.github.PagedIterator) + org.kohsuke.github.PagedIterable#PagedIterable() + org.kohsuke.github.PagedIterator#base + org.kohsuke.github.PagedIterator#wrapUp(java.lang.Object[]) + org.kohsuke.github.PagedSearchIterable#_iterator(int) + org.kohsuke.github.PagedSearchIterable#adapt(java.util.Iterator) diff --git a/src/main/java/org/kohsuke/github/GHAppInstallation.java b/src/main/java/org/kohsuke/github/GHAppInstallation.java index e92c744e99..3aad6326c5 100644 --- a/src/main/java/org/kohsuke/github/GHAppInstallation.java +++ b/src/main/java/org/kohsuke/github/GHAppInstallation.java @@ -263,10 +263,8 @@ public GHTargetType getTargetType() { */ @Deprecated public PagedSearchIterable listRepositories() { - GitHubRequest request; - - request = root().createRequest().withUrlPath("/installation/repositories").build(); - - return new PagedSearchIterable<>(root(), request, GHAppInstallationRepositoryResult.class); + return new PagedSearchIterable<>(root().createRequest() + .withUrlPath("/installation/repositories") + .toPaginatedEndpoint(GHAppInstallationRepositoryResult.class, GHRepository.class, null)); } } diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java b/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java index 44ace753a2..a358435008 100644 --- a/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java +++ b/src/main/java/org/kohsuke/github/GHAppInstallationRequest.java @@ -10,7 +10,6 @@ */ public class GHAppInstallationRequest extends GHObject { private GHOrganization account; - private GHUser requester; /** diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java b/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java deleted file mode 100644 index fc89d371ee..0000000000 --- a/src/main/java/org/kohsuke/github/GHAppInstallationsIterable.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for GHAppInstallation listing. - */ -class GHAppInstallationsIterable extends PagedIterable { - - /** The Constant APP_INSTALLATIONS_URL. */ - public static final String APP_INSTALLATIONS_URL = "/user/installations"; - private GHAppInstallationsPage result; - private final transient GitHub root; - - /** - * Instantiates a new GH app installations iterable. - * - * @param root - * the root - */ - public GHAppInstallationsIterable(GitHub root) { - this.root = root; - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - final GitHubRequest request = root.createRequest().withUrlPath(APP_INSTALLATIONS_URL).build(); - return new PagedIterator<>( - adapt(GitHubPageIterator.create(root.getClient(), GHAppInstallationsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHAppInstallation[] next() { - GHAppInstallationsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getInstallations(); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java b/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java index cd8f9a1f7e..0c593662d1 100644 --- a/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java +++ b/src/main/java/org/kohsuke/github/GHAppInstallationsPage.java @@ -4,10 +4,15 @@ /** * Represents the one page of GHAppInstallations. */ -class GHAppInstallationsPage { +class GHAppInstallationsPage implements GitHubPage { private GHAppInstallation[] installations; private int totalCount; + @Override + public GHAppInstallation[] getItems() { + return getInstallations(); + } + /** * Gets the total count. * diff --git a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java b/src/main/java/org/kohsuke/github/GHArtifactsIterable.java deleted file mode 100644 index 2a574150cc..0000000000 --- a/src/main/java/org/kohsuke/github/GHArtifactsIterable.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for artifacts listing. - */ -class GHArtifactsIterable extends PagedIterable { - private final transient GHRepository owner; - private final GitHubRequest request; - - private GHArtifactsPage result; - - /** - * Instantiates a new GH artifacts iterable. - * - * @param owner - * the owner - * @param requestBuilder - * the request builder - */ - public GHArtifactsIterable(GHRepository owner, GitHubRequest.Builder requestBuilder) { - this.owner = owner; - this.request = requestBuilder.build(); - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>( - adapt(GitHubPageIterator.create(owner.root().getClient(), GHArtifactsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHArtifact[] next() { - GHArtifactsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getArtifacts(owner); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHArtifactsPage.java b/src/main/java/org/kohsuke/github/GHArtifactsPage.java index 8b3675bb11..c4edc38515 100644 --- a/src/main/java/org/kohsuke/github/GHArtifactsPage.java +++ b/src/main/java/org/kohsuke/github/GHArtifactsPage.java @@ -8,10 +8,15 @@ */ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") -class GHArtifactsPage { +class GHArtifactsPage implements GitHubPage { private GHArtifact[] artifacts; private int totalCount; + @Override + public GHArtifact[] getItems() { + return artifacts; + } + /** * Gets the total count. * @@ -20,18 +25,4 @@ class GHArtifactsPage { public int getTotalCount() { return totalCount; } - - /** - * Gets the artifacts. - * - * @param owner - * the owner - * @return the artifacts - */ - GHArtifact[] getArtifacts(GHRepository owner) { - for (GHArtifact artifact : artifacts) { - artifact.wrapUp(owner); - } - return artifacts; - } } diff --git a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java index 73d55ba4c1..813b8f1416 100644 --- a/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java +++ b/src/main/java/org/kohsuke/github/GHAuthenticatedAppInstallation.java @@ -35,11 +35,9 @@ protected GHAuthenticatedAppInstallation(@Nonnull GitHub root) { * @return the paged iterable */ public PagedSearchIterable listRepositories() { - GitHubRequest request; - - request = root().createRequest().withUrlPath("/installation/repositories").build(); - - return new PagedSearchIterable<>(root(), request, GHAuthenticatedAppInstallationRepositoryResult.class); + return new PagedSearchIterable<>(root().createRequest() + .withUrlPath("/installation/repositories") + .toPaginatedEndpoint(GHAuthenticatedAppInstallationRepositoryResult.class, GHRepository.class, null)); } } diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java b/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java deleted file mode 100644 index 0866bd1f58..0000000000 --- a/src/main/java/org/kohsuke/github/GHCheckRunsIterable.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for check-runs listing. - */ -class GHCheckRunsIterable extends PagedIterable { - private final GHRepository owner; - private final GitHubRequest request; - - private GHCheckRunsPage result; - - /** - * Instantiates a new GH check runs iterable. - * - * @param owner - * the owner - * @param request - * the request - */ - public GHCheckRunsIterable(GHRepository owner, GitHubRequest request) { - this.owner = owner; - this.request = request; - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>( - adapt(GitHubPageIterator.create(owner.root().getClient(), GHCheckRunsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHCheckRun[] next() { - GHCheckRunsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getCheckRuns(owner); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHCheckRunsPage.java b/src/main/java/org/kohsuke/github/GHCheckRunsPage.java index d0b5d012f2..4f36d526c6 100644 --- a/src/main/java/org/kohsuke/github/GHCheckRunsPage.java +++ b/src/main/java/org/kohsuke/github/GHCheckRunsPage.java @@ -8,10 +8,15 @@ */ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") -class GHCheckRunsPage { +class GHCheckRunsPage implements GitHubPage { private GHCheckRun[] checkRuns; private int totalCount; + @Override + public GHCheckRun[] getItems() { + return checkRuns; + } + /** * Gets the total count. * @@ -20,18 +25,4 @@ class GHCheckRunsPage { public int getTotalCount() { return totalCount; } - - /** - * Gets the check runs. - * - * @param owner - * the owner - * @return the check runs - */ - GHCheckRun[] getCheckRuns(GHRepository owner) { - for (GHCheckRun checkRun : checkRuns) { - checkRun.wrap(owner); - } - return checkRuns; - } } diff --git a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java index 808f036017..6332323fbe 100644 --- a/src/main/java/org/kohsuke/github/GHCommitFileIterable.java +++ b/src/main/java/org/kohsuke/github/GHCommitFileIterable.java @@ -2,12 +2,8 @@ import org.kohsuke.github.GHCommit.File; -import java.util.Collections; -import java.util.Iterator; import java.util.List; -import javax.annotation.Nonnull; - /** * Iterable for commit listing. * @@ -21,9 +17,22 @@ class GHCommitFileIterable extends PagedIterable { */ private static final int GH_FILE_LIMIT_PER_COMMIT_PAGE = 300; - private final File[] files; - private final GHRepository owner; - private final String sha; + private static PaginatedEndpoint createEndpoint(GHRepository owner, + String sha, + List files) { + PaginatedEndpoint endpoint; + if (files != null && files.size() < GH_FILE_LIMIT_PER_COMMIT_PAGE) { + // create an endpoint that only reads one already loaded page + endpoint = PaginatedEndpoint.fromSinglePage(new GHCommitFilesPage(files.toArray(new File[0])), + GHCommit.File.class); + } else { + endpoint = owner.root() + .createRequest() + .withUrlPath(owner.getApiTailUrl("commits/" + sha)) + .toPaginatedEndpoint(GHCommitFilesPage.class, GHCommit.File.class, null); + } + return endpoint; + } /** * Instantiates a new GH commit iterable. @@ -35,62 +44,13 @@ class GHCommitFileIterable extends PagedIterable { * @param files * the list of files initially populated */ - public GHCommitFileIterable(GHRepository owner, String sha, List files) { - this.owner = owner; - this.sha = sha; - this.files = files != null ? files.toArray(new File[0]) : null; + GHCommitFileIterable(GHRepository owner, String sha, List files) { + super(createEndpoint(owner, sha, files)); } - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull @Override - public PagedIterator _iterator(int pageSize) { - - Iterator pageIterator; - - if (files != null && files.length < GH_FILE_LIMIT_PER_COMMIT_PAGE) { - // create a page iterator that only provides one page - pageIterator = Collections.singleton(files).iterator(); - } else { - // page size is controlled by the server for this iterator, do not allow it to be set by the caller - pageSize = 0; - - GitHubRequest request = owner.root() - .createRequest() - .withUrlPath(owner.getApiTailUrl("commits/" + sha)) - .build(); - - pageIterator = adapt( - GitHubPageIterator.create(owner.root().getClient(), GHCommitFilesPage.class, request, pageSize)); - } - - return new PagedIterator<>(pageIterator, null); - } - - /** - * Adapt. - * - * @param base - * the base commit page - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - - public boolean hasNext() { - return base.hasNext(); - } - - public GHCommit.File[] next() { - GHCommitFilesPage v = base.next(); - return v.getFiles(); - } - }; + public PagedIterable withPageSize(int i) { + // page size is controlled by the server for this iterable, do not allow it to be set by the caller + return this; } } diff --git a/src/main/java/org/kohsuke/github/GHCommitFilesPage.java b/src/main/java/org/kohsuke/github/GHCommitFilesPage.java index d2ab10dc98..c23761ae27 100644 --- a/src/main/java/org/kohsuke/github/GHCommitFilesPage.java +++ b/src/main/java/org/kohsuke/github/GHCommitFilesPage.java @@ -7,24 +7,23 @@ * * @author Stephen Horgan */ -class GHCommitFilesPage { +class GHCommitFilesPage implements GitHubPage { private File[] files; public GHCommitFilesPage() { } - public GHCommitFilesPage(File[] files) { + GHCommitFilesPage(File[] files) { this.files = files; } /** * Gets the files. * - * @param owner - * the owner * @return the files */ - File[] getFiles() { + @Override + public File[] getItems() { return files; } } diff --git a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java index 3a1ddbffce..a8ca46ef8a 100644 --- a/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHCommitSearchBuilder.java @@ -33,14 +33,6 @@ private static class CommitSearchResult extends SearchResult { @Override GHCommit[] getItems(GitHub root) { - for (GHCommit commit : items) { - String repoName = getRepoName(commit.url); - try { - GHRepository repo = root.getRepository(repoName); - commit.wrapUp(repo); - } catch (IOException ioe) { - } - } return items; } } @@ -66,7 +58,7 @@ private static String getRepoName(String commitUrl) { * the root */ GHCommitSearchBuilder(GitHub root) { - super(root, CommitSearchResult.class); + super(root, CommitSearchResult.class, GHCommit.class); } /** @@ -179,6 +171,11 @@ public GHCommitSearchBuilder is(String v) { return q("is:" + v); } + @Override + public PagedSearchIterable list() { + return list(this::lateBindGHCommit); + } + /** * Merge gh commit search builder. * @@ -290,4 +287,16 @@ public GHCommitSearchBuilder user(String v) { protected String getApiUrl() { return "/search/commits"; } + + /** + * Exposed internally for testing only. + */ + void lateBindGHCommit(GHCommit item) { + String repoName = getRepoName(item.getUrl().toString()); + try { + GHRepository repo = root().getRepository(repoName); + item.wrapUp(repo); + } catch (IOException ioe) { + } + } } diff --git a/src/main/java/org/kohsuke/github/GHCompare.java b/src/main/java/org/kohsuke/github/GHCompare.java index 48340fda36..dccb9a6019 100644 --- a/src/main/java/org/kohsuke/github/GHCompare.java +++ b/src/main/java/org/kohsuke/github/GHCompare.java @@ -5,10 +5,6 @@ import java.io.IOException; import java.net.URL; -import java.util.Collections; -import java.util.Iterator; - -import javax.annotation.Nonnull; // TODO: Auto-generated Javadoc /** @@ -16,7 +12,7 @@ * * @author Michael Clarke */ -public class GHCompare { +public class GHCompare implements GitHubPage { /** * Compare commits had a child commit element with additional details we want to capture. This extension of GHCommit @@ -113,6 +109,7 @@ public String getUrl() { return url; } } + /** * The enum Status. */ @@ -158,70 +155,9 @@ public String getUrl() { return url; } } - /** - * Iterable for commit listing. - */ - class GHCompareCommitsIterable extends PagedIterable { - - private GHCompare result; - - /** - * Instantiates a new GH compare commits iterable. - */ - public GHCompareCommitsIterable() { - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - GitHubRequest request = owner.root() - .createRequest() - .injectMappingValue("GHCompare_usePaginatedCommits", usePaginatedCommits) - .withUrlPath(owner.getApiTailUrl(url.substring(url.lastIndexOf("/compare/")))) - .build(); - - // page_size must be set for GHCompare commit pagination - if (pageSize == 0) { - pageSize = 10; - } - return new PagedIterator<>( - adapt(GitHubPageIterator.create(owner.root().getClient(), GHCompare.class, request, pageSize)), - item -> item.wrapUp(owner)); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public Commit[] next() { - GHCompare v = base.next(); - if (result == null) { - result = v; - } - return v.commits; - } - }; - } - } private int aheadBy, behindBy, totalCommits; - private Commit baseCommit, mergeBaseCommit; + private Commit baseCommit, mergeBaseCommit; private Commit[] commits; private GHCommit.File[] files; @@ -324,6 +260,12 @@ public URL getHtmlUrl() { return GitHubClient.parseURL(htmlUrl); } + @Override + @SuppressFBWarnings(value = { "EI_EXPOSE_REP" }, justification = "Expected behavior") + public GHCompare.Commit[] getItems() { + return commits; + } + /** * Gets merge base commit. * @@ -395,16 +337,16 @@ public URL getUrl() { */ public PagedIterable listCommits() { if (usePaginatedCommits) { - return new GHCompareCommitsIterable(); + final GHRepository owner = this.owner; + return owner.root() + .createRequest() + .injectMappingValue("GHCompare_usePaginatedCommits", usePaginatedCommits) + .withUrlPath(owner.getApiTailUrl(url.substring(url.lastIndexOf("/compare/")))) + .toIterable(GHCompare.class, Commit.class, item -> item.wrapUp(owner)) + .withPageSize(10); } else { // if not using paginated commits, adapt the returned commits array - return new PagedIterable() { - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>(Collections.singleton(commits).iterator(), null); - } - }; + return new PagedIterable<>(PaginatedEndpoint.fromSinglePage(this.commits, Commit.class)); } } diff --git a/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java b/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java index bdd16cea3e..7aa1243f38 100644 --- a/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHContentSearchBuilder.java @@ -36,7 +36,7 @@ GHContent[] getItems(GitHub root) { * the root */ GHContentSearchBuilder(GitHub root) { - super(root, ContentSearchResult.class); + super(root, ContentSearchResult.class, GHContent.class); } /** diff --git a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java index f921fdd920..850dbdff4e 100644 --- a/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java +++ b/src/main/java/org/kohsuke/github/GHExternalGroupIterable.java @@ -1,9 +1,6 @@ package org.kohsuke.github; -import java.util.Arrays; -import java.util.Iterator; - -import javax.annotation.Nonnull; +import org.jetbrains.annotations.NotNull; /** * Iterable for external group listing. @@ -12,12 +9,6 @@ */ class GHExternalGroupIterable extends PagedIterable { - private final GHOrganization owner; - - private final GitHubRequest request; - - private GHExternalGroupPage result; - /** * Instantiates a new GH external groups iterable. * @@ -26,52 +17,25 @@ class GHExternalGroupIterable extends PagedIterable { * @param requestBuilder * the request builder */ - GHExternalGroupIterable(final GHOrganization owner, final GitHubRequest.Builder requestBuilder) { - this.owner = owner; - this.request = requestBuilder.build(); - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>( - adapt(GitHubPageIterator - .create(owner.root().getClient(), GHExternalGroupPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - private Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - try { - return base.hasNext(); - } catch (final GHException e) { - throw EnterpriseManagedSupport.forOrganization(owner).filterException(e).orElse(e); - } - } - - public GHExternalGroup[] next() { - GHExternalGroupPage v = base.next(); - if (result == null) { - result = v; - } - Arrays.stream(v.getGroups()).forEach(g -> g.wrapUp(owner)); - return v.getGroups(); + GHExternalGroupIterable(final GHOrganization owner, GitHubRequest.Builder requestBuilder) { + super(new PaginatedEndpoint<>(owner.root().getClient(), + requestBuilder.build(), + GHExternalGroupPage.class, + GHExternalGroup.class, + item -> item.wrapUp(owner)) { + @NotNull @Override + public PaginatedEndpointPages pages() { + return new PaginatedEndpointPages<>(client, pageType, request, pageSize, itemInitializer) { + @Override + public boolean hasNext() { + try { + return super.hasNext(); + } catch (final GHException e) { + throw EnterpriseManagedSupport.forOrganization(owner).filterException(e).orElse(e); + } + } + }; } - }; + }); } } diff --git a/src/main/java/org/kohsuke/github/GHExternalGroupPage.java b/src/main/java/org/kohsuke/github/GHExternalGroupPage.java index d47b49678c..02441fcfff 100644 --- a/src/main/java/org/kohsuke/github/GHExternalGroupPage.java +++ b/src/main/java/org/kohsuke/github/GHExternalGroupPage.java @@ -7,7 +7,7 @@ * * @author Miguel Esteban GutiƩrrez */ -class GHExternalGroupPage { +class GHExternalGroupPage implements GitHubPage { private static final GHExternalGroup[] GH_EXTERNAL_GROUPS = new GHExternalGroup[0]; @@ -31,4 +31,8 @@ public GHExternalGroup[] getGroups() { return groups; } + @Override + public GHExternalGroup[] getItems() { + return getGroups(); + } } diff --git a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java index 3967691ac3..507a254864 100644 --- a/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHIssueSearchBuilder.java @@ -32,8 +32,6 @@ private static class IssueSearchResult extends SearchResult { @Override GHIssue[] getItems(GitHub root) { - for (GHIssue i : items) { - } return items; } } @@ -45,7 +43,7 @@ GHIssue[] getItems(GitHub root) { * the root */ GHIssueSearchBuilder(GitHub root) { - super(root, IssueSearchResult.class); + super(root, IssueSearchResult.class, GHIssue.class); } /** diff --git a/src/main/java/org/kohsuke/github/GHMyself.java b/src/main/java/org/kohsuke/github/GHMyself.java index 05e52cd5ac..3436fc8d30 100644 --- a/src/main/java/org/kohsuke/github/GHMyself.java +++ b/src/main/java/org/kohsuke/github/GHMyself.java @@ -38,6 +38,9 @@ public enum RepositoryListFilter { PUBLIC; } + /** The Constant APP_INSTALLATIONS_URL. */ + public static final String APP_INSTALLATIONS_URL = "/user/installations"; + /** * Create default GHMyself instance */ @@ -110,7 +113,9 @@ public synchronized Map getAllRepositories() { * app installations accessible to the user access token */ public PagedIterable getAppInstallations() { - return new GHAppInstallationsIterable(root()); + return root().createRequest() + .withUrlPath(APP_INSTALLATIONS_URL) + .toIterable(GHAppInstallationsPage.class, GHAppInstallation.class, null); } /** diff --git a/src/main/java/org/kohsuke/github/GHNotificationStream.java b/src/main/java/org/kohsuke/github/GHNotificationStream.java index 269ddf972f..28e075b812 100644 --- a/src/main/java/org/kohsuke/github/GHNotificationStream.java +++ b/src/main/java/org/kohsuke/github/GHNotificationStream.java @@ -143,8 +143,7 @@ GHThread fetch() { } Requester requester = req.withUrlPath(apiUrl); - GitHubResponse response = ((GitHubPageContentsIterable) requester - .toIterable(GHThread[].class, null)).toResponse(); + GitHubResponse response = requester.toIterable(GHThread[].class, null).toResponse(); threads = response.body(); if (threads == null) { diff --git a/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java b/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java index 143f6e6ae2..910b8c6163 100644 --- a/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHPullRequestSearchBuilder.java @@ -44,7 +44,7 @@ GHPullRequest[] getItems(GitHub root) { * the root */ GHPullRequestSearchBuilder(GitHub root) { - super(root, PullRequestSearchResult.class); + super(root, PullRequestSearchResult.class, GHPullRequest.class); } /** diff --git a/src/main/java/org/kohsuke/github/GHRepository.java b/src/main/java/org/kohsuke/github/GHRepository.java index 1ecd86f0bd..c544136980 100644 --- a/src/main/java/org/kohsuke/github/GHRepository.java +++ b/src/main/java/org/kohsuke/github/GHRepository.java @@ -1189,10 +1189,7 @@ public Map getBranches() throws IOException { * for a specific ref */ public PagedIterable getCheckRuns(String ref) { - GitHubRequest request = root().createRequest() - .withUrlPath(String.format("/repos/%s/%s/commits/%s/check-runs", getOwnerName(), name, ref)) - .build(); - return new GHCheckRunsIterable(this, request); + return getCheckRuns(ref, null); } /** @@ -1207,11 +1204,10 @@ public PagedIterable getCheckRuns(String ref) { * for a specific ref */ public PagedIterable getCheckRuns(String ref, Map params) { - GitHubRequest request = root().createRequest() + Requester requester = root().createRequest() .withUrlPath(String.format("/repos/%s/%s/commits/%s/check-runs", getOwnerName(), name, ref)) - .with(params) - .build(); - return new GHCheckRunsIterable(this, request); + .with(params); + return requester.toIterable(GHCheckRunsPage.class, GHCheckRun.class, item -> item.wrap(this)); } /** @@ -2552,7 +2548,9 @@ public boolean isVulnerabilityAlertsEnabled() throws IOException { * @return the paged iterable */ public PagedIterable listArtifacts() { - return new GHArtifactsIterable(this, root().createRequest().withUrlPath(getApiTailUrl("actions/artifacts"))); + return root().createRequest() + .withUrlPath(getApiTailUrl("actions/artifacts")) + .toIterable(GHArtifactsPage.class, GHArtifact.class, item -> item.wrapUp(this)); } /** @@ -2960,7 +2958,9 @@ public List listTopics() throws IOException { * @return the paged iterable */ public PagedIterable listWorkflows() { - return new GHWorkflowsIterable(this); + return root().createRequest() + .withUrlPath(getApiTailUrl("actions/workflows")) + .toIterable(GHWorkflowsPage.class, GHWorkflow.class, item -> item.wrapUp(this)); } /** diff --git a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java index 9e600ec927..c0d0896658 100644 --- a/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHRepositorySearchBuilder.java @@ -32,8 +32,6 @@ private static class RepositorySearchResult extends SearchResult { @Override GHRepository[] getItems(GitHub root) { - for (GHRepository item : items) { - } return items; } } @@ -45,7 +43,7 @@ GHRepository[] getItems(GitHub root) { * the root */ GHRepositorySearchBuilder(GitHub root) { - super(root, RepositorySearchResult.class); + super(root, RepositorySearchResult.class, GHRepository.class); } /** diff --git a/src/main/java/org/kohsuke/github/GHSearchBuilder.java b/src/main/java/org/kohsuke/github/GHSearchBuilder.java index ea6317426c..78a5bda076 100644 --- a/src/main/java/org/kohsuke/github/GHSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHSearchBuilder.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; @@ -18,11 +19,12 @@ */ public abstract class GHSearchBuilder extends GHQueryBuilder { + private final Class itemType; + /** * Data transfer object that receives the result of search. */ private final Class> receiverType; - /** The terms. */ protected final List terms = new ArrayList(); @@ -34,9 +36,10 @@ public abstract class GHSearchBuilder extends GHQueryBuilder { * @param receiverType * the receiver type */ - GHSearchBuilder(GitHub root, Class> receiverType) { + GHSearchBuilder(GitHub root, Class> receiverType, Class itemType) { super(root); this.receiverType = receiverType; + this.itemType = itemType; req.withUrlPath(getApiUrl()); req.rateLimit(RateLimitTarget.SEARCH); } @@ -48,9 +51,7 @@ public abstract class GHSearchBuilder extends GHQueryBuilder { */ @Override public PagedSearchIterable list() { - - req.set("q", StringUtils.join(terms, " ")); - return new PagedSearchIterable<>(root(), req.build(), receiverType); + return list(null); } /** @@ -72,6 +73,13 @@ public GHQueryBuilder q(String term) { */ protected abstract String getApiUrl(); + PagedSearchIterable list(Consumer itemInitializer) { + + req.set("q", StringUtils.join(terms, " ")); + return new PagedSearchIterable<>( + new PaginatedEndpoint<>(root().getClient(), req.build(), receiverType, itemType, itemInitializer)); + } + /** * Add a search term with qualifier. * diff --git a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java index 0193b2139e..6852114b26 100644 --- a/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java +++ b/src/main/java/org/kohsuke/github/GHUserSearchBuilder.java @@ -38,7 +38,7 @@ GHUser[] getItems(GitHub root) { * the root */ GHUserSearchBuilder(GitHub root) { - super(root, UserSearchResult.class); + super(root, UserSearchResult.class, GHUser.class); } /** diff --git a/src/main/java/org/kohsuke/github/GHWorkflow.java b/src/main/java/org/kohsuke/github/GHWorkflow.java index dff9ffdc3d..74e4b0a4b3 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflow.java +++ b/src/main/java/org/kohsuke/github/GHWorkflow.java @@ -153,7 +153,9 @@ public String getState() { * @return the paged iterable */ public PagedIterable listRuns() { - return new GHWorkflowRunsIterable(owner, root().createRequest().withUrlPath(getApiRoute(), "runs")); + return root().createRequest() + .withUrlPath(getApiRoute(), "runs") + .toIterable(GHWorkflowRunsPage.class, GHWorkflowRun.class, item -> item.wrapUp(owner)); } private String getApiRoute() { diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java b/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java index 9f011e9612..a7656f3962 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowJobQueryBuilder.java @@ -48,6 +48,6 @@ public GHWorkflowJobQueryBuilder latest() { */ @Override public PagedIterable list() { - return new GHWorkflowJobsIterable(repo, req.build()); + return req.toIterable(GHWorkflowJobsPage.class, GHWorkflowJob.class, item -> item.wrapUp(repo)); } } diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java deleted file mode 100644 index 6ab751850d..0000000000 --- a/src/main/java/org/kohsuke/github/GHWorkflowJobsIterable.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for workflow run jobs listing. - */ -class GHWorkflowJobsIterable extends PagedIterable { - private final GHRepository repo; - private final GitHubRequest request; - - private GHWorkflowJobsPage result; - - /** - * Instantiates a new GH workflow jobs iterable. - * - * @param repo - * the repo - * @param request - * the request - */ - public GHWorkflowJobsIterable(GHRepository repo, GitHubRequest request) { - this.repo = repo; - this.request = request; - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>( - adapt(GitHubPageIterator.create(repo.root().getClient(), GHWorkflowJobsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHWorkflowJob[] next() { - GHWorkflowJobsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getWorkflowJobs(repo); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java index 8d4a7ca772..02d45de338 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowJobsPage.java @@ -8,10 +8,15 @@ */ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") -class GHWorkflowJobsPage { +class GHWorkflowJobsPage implements GitHubPage { private GHWorkflowJob[] jobs; private int totalCount; + @Override + public GHWorkflowJob[] getItems() { + return jobs; + } + /** * Gets the total count. * @@ -20,18 +25,4 @@ class GHWorkflowJobsPage { public int getTotalCount() { return totalCount; } - - /** - * Gets the workflow jobs. - * - * @param repo - * the repo - * @return the workflow jobs - */ - GHWorkflowJob[] getWorkflowJobs(GHRepository repo) { - for (GHWorkflowJob job : jobs) { - job.wrapUp(repo); - } - return jobs; - } } diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRun.java b/src/main/java/org/kohsuke/github/GHWorkflowRun.java index e5a2456ec8..bfd20136d8 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowRun.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowRun.java @@ -579,7 +579,9 @@ public PagedIterable listAllJobs() { * @return the paged iterable */ public PagedIterable listArtifacts() { - return new GHArtifactsIterable(owner, root().createRequest().withUrlPath(getApiRoute(), "artifacts")); + return root().createRequest() + .withUrlPath(getApiRoute(), "artifacts") + .toIterable(GHArtifactsPage.class, GHArtifact.class, item -> item.wrapUp(owner)); } /** diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java b/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java index 105dd77a84..97981663a6 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowRunQueryBuilder.java @@ -133,7 +133,8 @@ public GHWorkflowRunQueryBuilder headSha(String headSha) { */ @Override public PagedIterable list() { - return new GHWorkflowRunsIterable(repo, req.withUrlPath(repo.getApiTailUrl("actions/runs"))); + return req.withUrlPath(repo.getApiTailUrl("actions/runs")) + .toIterable(GHWorkflowRunsPage.class, GHWorkflowRun.class, item -> item.wrapUp(repo)); } /** diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java deleted file mode 100644 index 4a525a83dc..0000000000 --- a/src/main/java/org/kohsuke/github/GHWorkflowRunsIterable.java +++ /dev/null @@ -1,67 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for workflow runs listing. - */ -class GHWorkflowRunsIterable extends PagedIterable { - private final GHRepository owner; - private final GitHubRequest request; - - private GHWorkflowRunsPage result; - - /** - * Instantiates a new GH workflow runs iterable. - * - * @param owner - * the owner - * @param requestBuilder - * the request builder - */ - public GHWorkflowRunsIterable(GHRepository owner, GitHubRequest.Builder requestBuilder) { - this.owner = owner; - this.request = requestBuilder.build(); - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - return new PagedIterator<>( - adapt(GitHubPageIterator.create(owner.root().getClient(), GHWorkflowRunsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHWorkflowRun[] next() { - GHWorkflowRunsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getWorkflowRuns(owner); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java index 8df067dead..5e0ebaca99 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowRunsPage.java @@ -8,10 +8,15 @@ */ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") -class GHWorkflowRunsPage { +class GHWorkflowRunsPage implements GitHubPage { private int totalCount; private GHWorkflowRun[] workflowRuns; + @Override + public GHWorkflowRun[] getItems() { + return workflowRuns; + } + /** * Gets the total count. * @@ -20,18 +25,4 @@ class GHWorkflowRunsPage { public int getTotalCount() { return totalCount; } - - /** - * Gets the workflow runs. - * - * @param owner - * the owner - * @return the workflow runs - */ - GHWorkflowRun[] getWorkflowRuns(GHRepository owner) { - for (GHWorkflowRun workflowRun : workflowRuns) { - workflowRun.wrapUp(owner); - } - return workflowRuns; - } } diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java b/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java deleted file mode 100644 index 66d3d9480f..0000000000 --- a/src/main/java/org/kohsuke/github/GHWorkflowsIterable.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.kohsuke.github; - -import java.util.Iterator; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * Iterable for workflows listing. - */ -class GHWorkflowsIterable extends PagedIterable { - private final transient GHRepository owner; - - private GHWorkflowsPage result; - - /** - * Instantiates a new GH workflows iterable. - * - * @param owner - * the owner - */ - public GHWorkflowsIterable(GHRepository owner) { - this.owner = owner; - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator - */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - GitHubRequest request = owner.root() - .createRequest() - .withUrlPath(owner.getApiTailUrl("actions/workflows")) - .build(); - - return new PagedIterator<>( - adapt(GitHubPageIterator.create(owner.root().getClient(), GHWorkflowsPage.class, request, pageSize)), - null); - } - - /** - * Adapt. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public GHWorkflow[] next() { - GHWorkflowsPage v = base.next(); - if (result == null) { - result = v; - } - return v.getWorkflows(owner); - } - }; - } -} diff --git a/src/main/java/org/kohsuke/github/GHWorkflowsPage.java b/src/main/java/org/kohsuke/github/GHWorkflowsPage.java index 1fb87a8147..134bdd6419 100644 --- a/src/main/java/org/kohsuke/github/GHWorkflowsPage.java +++ b/src/main/java/org/kohsuke/github/GHWorkflowsPage.java @@ -8,10 +8,15 @@ */ @SuppressFBWarnings(value = { "UWF_UNWRITTEN_PUBLIC_OR_PROTECTED_FIELD", "UWF_UNWRITTEN_FIELD", "NP_UNWRITTEN_FIELD" }, justification = "JSON API") -class GHWorkflowsPage { +class GHWorkflowsPage implements GitHubPage { private int totalCount; private GHWorkflow[] workflows; + @Override + public GHWorkflow[] getItems() { + return workflows; + } + /** * Gets the total count. * @@ -20,18 +25,4 @@ class GHWorkflowsPage { public int getTotalCount() { return totalCount; } - - /** - * Gets the workflows. - * - * @param owner - * the owner - * @return the workflows - */ - GHWorkflow[] getWorkflows(GHRepository owner) { - for (GHWorkflow workflow : workflows) { - workflow.wrapUp(owner); - } - return workflows; - } } diff --git a/src/main/java/org/kohsuke/github/GitHubPage.java b/src/main/java/org/kohsuke/github/GitHubPage.java new file mode 100644 index 0000000000..764b099788 --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubPage.java @@ -0,0 +1,23 @@ +package org.kohsuke.github; + +import java.util.Arrays; +import java.util.List; + +/** + * A page of results from GitHub. + * + * @param + * the type of items on the page. + */ +interface GitHubPage { + /** + * Wraps up the retrieved object and return them. Only called once. + * + * @return the items + */ + I[] getItems(); + + default List getItemsList() { + return Arrays.asList(this.getItems()); + } +} diff --git a/src/main/java/org/kohsuke/github/GitHubPageArrayAdapter.java b/src/main/java/org/kohsuke/github/GitHubPageArrayAdapter.java new file mode 100644 index 0000000000..50005f35fc --- /dev/null +++ b/src/main/java/org/kohsuke/github/GitHubPageArrayAdapter.java @@ -0,0 +1,21 @@ +package org.kohsuke.github; + +/** + * Represents a page of results + * + * @author Kohsuke Kawaguchi + * @param + * the generic type + */ +class GitHubPageArrayAdapter implements GitHubPage { + + private final I[] items; + + public GitHubPageArrayAdapter(I[] items) { + this.items = items; + } + + public I[] getItems() { + return items; + } +} diff --git a/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java b/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java deleted file mode 100644 index f8fc7e4907..0000000000 --- a/src/main/java/org/kohsuke/github/GitHubPageContentsIterable.java +++ /dev/null @@ -1,93 +0,0 @@ -package org.kohsuke.github; - -import java.io.IOException; -import java.util.function.Consumer; - -import javax.annotation.Nonnull; - -// TODO: Auto-generated Javadoc -/** - * {@link PagedIterable} implementation that take a {@link Consumer} that initializes all the items on each page as they - * are retrieved. - * - * {@link GitHubPageContentsIterable} is immutable and thread-safe, but the iterator returned from {@link #iterator()} - * is not. Any one instance of iterator should only be called from a single thread. - * - * @author Liam Newman - * @param - * the type of items on each page - */ -class GitHubPageContentsIterable extends PagedIterable { - - /** - * This class is not thread-safe. Any one instance should only be called from a single thread. - */ - private class GitHubPageContentsIterator extends PagedIterator { - - public GitHubPageContentsIterator(GitHubPageIterator iterator, Consumer itemInitializer) { - super(iterator, itemInitializer); - } - - /** - * Gets the {@link GitHubResponse} for the last page received. - * - * @return the {@link GitHubResponse} for the last page received. - */ - private GitHubResponse lastResponse() { - return ((GitHubPageIterator) base).finalResponse(); - } - } - - private final GitHubClient client; - private final Consumer itemInitializer; - private final Class receiverType; - private final GitHubRequest request; - - /** - * Instantiates a new git hub page contents iterable. - * - * @param client - * the client - * @param request - * the request - * @param receiverType - * the receiver type - * @param itemInitializer - * the item initializer - */ - GitHubPageContentsIterable(GitHubClient client, - GitHubRequest request, - Class receiverType, - Consumer itemInitializer) { - this.client = client; - this.request = request; - this.receiverType = receiverType; - this.itemInitializer = itemInitializer; - } - - /** - * {@inheritDoc} - */ - @Override - @Nonnull - public PagedIterator _iterator(int pageSize) { - final GitHubPageIterator iterator = GitHubPageIterator.create(client, receiverType, request, pageSize); - return new GitHubPageContentsIterator(iterator, itemInitializer); - } - - /** - * 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 - GitHubResponse toResponse() throws IOException { - GitHubPageContentsIterator iterator = (GitHubPageContentsIterator) iterator(); - T[] items = toArray(iterator); - GitHubResponse lastResponse = iterator.lastResponse(); - return new GitHubResponse<>(lastResponse, items); - } -} diff --git a/src/main/java/org/kohsuke/github/GitHubRequest.java b/src/main/java/org/kohsuke/github/GitHubRequest.java index 20cf1462a4..51eec456ec 100644 --- a/src/main/java/org/kohsuke/github/GitHubRequest.java +++ b/src/main/java/org/kohsuke/github/GitHubRequest.java @@ -323,10 +323,11 @@ public B with(@WillClose InputStream body) throws IOException { * @return the request builder */ public B with(Map map) { - for (Map.Entry entry : map.entrySet()) { - with(entry.getKey(), entry.getValue()); + if (map != null) { + for (Map.Entry entry : map.entrySet()) { + with(entry.getKey(), entry.getValue()); + } } - return (B) this; } diff --git a/src/main/java/org/kohsuke/github/GitHubResponse.java b/src/main/java/org/kohsuke/github/GitHubResponse.java index 8ac65391f7..180f04998d 100644 --- a/src/main/java/org/kohsuke/github/GitHubResponse.java +++ b/src/main/java/org/kohsuke/github/GitHubResponse.java @@ -162,7 +162,7 @@ static T parseBody(GitHubConnectorResponse connectorResponse, T instance) th * @param body * the body */ - GitHubResponse(GitHubResponse response, @CheckForNull T body) { + GitHubResponse(GitHubResponse response, @CheckForNull T body) { this.statusCode = response.statusCode(); this.headers = response.headers; this.body = body; diff --git a/src/main/java/org/kohsuke/github/PagedIterable.java b/src/main/java/org/kohsuke/github/PagedIterable.java index a916af8009..8eb3f44b21 100644 --- a/src/main/java/org/kohsuke/github/PagedIterable.java +++ b/src/main/java/org/kohsuke/github/PagedIterable.java @@ -1,17 +1,11 @@ package org.kohsuke.github; import java.io.IOException; -import java.lang.reflect.Array; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import javax.annotation.Nonnull; -// TODO: Auto-generated Javadoc /** * {@link Iterable} that returns {@link PagedIterator}. {@link PagedIterable} is thread-safe but {@link PagedIterator} * is not. Any one instance of {@link PagedIterator} should only be called from a single thread. @@ -20,72 +14,35 @@ * @param * the type of items on each page */ -public abstract class PagedIterable implements Iterable { - /** - * Page size. 0 is default. - */ - private int pageSize = 0; +public class PagedIterable implements Iterable { - /** - * Instantiate a PagedIterable. - */ - public PagedIterable() { - } + private final PaginatedEndpoint, T> paginatedEndpoint; /** - * Iterator over page items. - * - * @param pageSize - * the page size - * @return the paged iterator + * Instantiates a new git hub page contents iterable. */ - @Nonnull - public abstract PagedIterator _iterator(int pageSize); + > PagedIterable(PaginatedEndpoint paginatedEndpoint) { + this.paginatedEndpoint = paginatedEndpoint; + } - /** - * Returns an iterator over elements of type {@code T}. - * - * @return an Iterator. - */ @Nonnull public final PagedIterator iterator() { - return _iterator(pageSize); + return new PagedIterator<>(items()); } - /** - * Eagerly walk {@link Iterable} and return the result in an array. - * - * @return the list - * @throws IOException - * if an I/O exception occurs. - */ @Nonnull public T[] toArray() throws IOException { - return toArray(iterator()); + return paginatedEndpoint.toArray(); } - /** - * Eagerly walk {@link Iterable} and return the result in a list. - * - * @return the list - * @throws IOException - * if an I/O Exception occurs - */ @Nonnull public List toList() throws IOException { - return Collections.unmodifiableList(Arrays.asList(this.toArray())); + return paginatedEndpoint.toList(); } - /** - * Eagerly walk {@link Iterable} and return the result in a set. - * - * @return the set - * @throws IOException - * if an I/O Exception occurs - */ @Nonnull public Set toSet() throws IOException { - return Collections.unmodifiableSet(new LinkedHashSet<>(Arrays.asList(this.toArray()))); + return paginatedEndpoint.toSet(); } /** @@ -99,67 +56,19 @@ public Set toSet() throws IOException { * @return the paged iterable */ public PagedIterable withPageSize(int size) { - this.pageSize = size; + paginatedEndpoint.withPageSize(size); return this; } - /** - * Concatenates a list of arrays into a single array. - * - * @param type - * the type of array to be returned. - * @param pages - * the list of arrays to be concatenated. - * @param totalLength - * the total length of the returned array. - * @return an array containing all elements from all pages. - */ - @Nonnull - private T[] concatenatePages(Class type, List pages, int totalLength) { - - T[] result = type.cast(Array.newInstance(type.getComponentType(), totalLength)); - - int position = 0; - for (T[] page : pages) { - final int pageLength = Array.getLength(page); - System.arraycopy(page, 0, result, position, pageLength); - position += pageLength; - } - return result; + PaginatedEndpointItems, T> items() { + return paginatedEndpoint.items(); } - /** - * Eagerly walk {@link PagedIterator} and return the result in an array. - * - * @param iterator - * the {@link PagedIterator} to read - * @return an array of all elements from the {@link PagedIterator} - * @throws IOException - * if an I/O exception occurs. - */ - protected T[] toArray(final PagedIterator iterator) throws IOException { - try { - ArrayList pages = new ArrayList<>(); - int totalSize = 0; - T[] item; - do { - item = iterator.nextPageArray(); - totalSize += Array.getLength(item); - pages.add(item); - } while (iterator.hasNext()); - - Class type = (Class) item.getClass(); - - return concatenatePages(type, pages, totalSize); - } 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; - } - } + PaginatedEndpointPages, T> pages() { + return paginatedEndpoint.pages(); } + GitHubResponse toResponse() throws IOException { + return paginatedEndpoint.toResponse(); + } } diff --git a/src/main/java/org/kohsuke/github/PagedIterator.java b/src/main/java/org/kohsuke/github/PagedIterator.java index ac6e54e826..719bd2102e 100644 --- a/src/main/java/org/kohsuke/github/PagedIterator.java +++ b/src/main/java/org/kohsuke/github/PagedIterator.java @@ -1,14 +1,7 @@ package org.kohsuke.github; -import java.util.Arrays; import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; -import java.util.Objects; -import java.util.function.Consumer; - -import javax.annotation.CheckForNull; -import javax.annotation.Nonnull; // TODO: Auto-generated Javadoc /** @@ -24,135 +17,37 @@ * @param * the type parameter */ +@Deprecated public class PagedIterator 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 #fetch() {@link #fetch()} for details on how this field is used. - */ - private T[] currentPage; - - @CheckForNull - private final Consumer itemInitializer; - - /** - * The index of the next item on the page, the item that will be returned when {@link #next()} is called. - * - * @see #fetch() {@link #fetch()} for details on how this field is used. - */ - private int nextItemIndex; - - /** The base. */ - @Nonnull - protected final Iterator base; + private final PaginatedEndpointItems, T> endpointIterator; /** * Instantiates a new paged iterator. * - * @param base + * @param endpointIterator * the base - * @param itemInitializer - * the item initializer */ - PagedIterator(@Nonnull Iterator base, @CheckForNull Consumer itemInitializer) { - this.base = base; - this.itemInitializer = itemInitializer; + PagedIterator(PaginatedEndpointItems, T> endpointIterator) { + this.endpointIterator = endpointIterator; } - /** - * {@inheritDoc} - */ public boolean hasNext() { - fetch(); - return (currentPage != null && currentPage.length > nextItemIndex); + return endpointIterator.hasNext(); } - /** - * {@inheritDoc} - */ public T next() { - if (!hasNext()) - throw new NoSuchElementException(); - return currentPage[nextItemIndex++]; + return endpointIterator.next(); } /** - * Gets the next page worth of data. + * Get the next page of items. * - * @return the list + * @return a list of the next page of items. + * @deprecated use PagedIterable.pages(). */ + @Deprecated public List nextPage() { - return Arrays.asList(nextPageArray()); - } - - /** - * 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 void fetch() { - if ((currentPage == null || currentPage.length <= nextItemIndex) && base.hasNext()) { - // On first call, always get next page (may be empty array) - T[] result = Objects.requireNonNull(base.next()); - wrapUp(result); - currentPage = result; - nextItemIndex = 0; - } - } - - /** - * This poorly named method, initializes items with local data after they are fetched. It is up to the implementer - * to decide what local data to apply. - * - * @param page - * the page of items to be initialized - */ - protected void wrapUp(T[] page) { - if (itemInitializer != null) { - for (T item : page) { - itemInitializer.accept(item); - } - } - } - - /** - * Gets the next page worth of data. - * - * @return the list - */ - @Nonnull - T[] nextPageArray() { - // if we have not fetched any pages yet, always fetch. - // If we have fetched at least one page, check hasNext() - if (currentPage == null) { - fetch(); - } else if (!hasNext()) { - throw new NoSuchElementException(); - } - - // Current should never be null after fetch - Objects.requireNonNull(currentPage); - T[] r = currentPage; - if (nextItemIndex != 0) { - r = Arrays.copyOfRange(r, nextItemIndex, r.length); - } - nextItemIndex = currentPage.length; - return r; + return endpointIterator.nextPage(); } } diff --git a/src/main/java/org/kohsuke/github/PagedSearchIterable.java b/src/main/java/org/kohsuke/github/PagedSearchIterable.java index 8c6d00a26d..6531f449bc 100644 --- a/src/main/java/org/kohsuke/github/PagedSearchIterable.java +++ b/src/main/java/org/kohsuke/github/PagedSearchIterable.java @@ -2,10 +2,6 @@ import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; -import java.util.Iterator; - -import javax.annotation.Nonnull; - // TODO: Auto-generated Javadoc /** * {@link PagedIterable} enhanced to report search result specific information. @@ -19,46 +15,14 @@ "UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR" }, justification = "Constructed by JSON API") public class PagedSearchIterable extends PagedIterable { - private final Class> receiverType; - - private final GitHubRequest request; - - /** - * As soon as we have any result fetched, it's set here so that we can report the total count. - */ - private SearchResult result; - - private final transient GitHub root; + private final PaginatedEndpoint, T> searchPaginatedEndpoint; /** * Instantiates a new paged search iterable. - * - * @param root - * the root - * @param request - * the request - * @param receiverType - * the receiver type - */ - PagedSearchIterable(GitHub root, GitHubRequest request, Class> receiverType) { - this.root = root; - this.request = request; - this.receiverType = receiverType; - } - - /** - * Iterator. - * - * @param pageSize - * the page size - * @return the paged iterator */ - @Nonnull - @Override - public PagedIterator _iterator(int pageSize) { - final Iterator adapter = adapt( - GitHubPageIterator.create(root.getClient(), receiverType, request, pageSize)); - return new PagedIterator(adapter, null); + > PagedSearchIterable(PaginatedEndpoint paginatedEndpoint) { + super(paginatedEndpoint); + this.searchPaginatedEndpoint = paginatedEndpoint; } /** @@ -66,9 +30,10 @@ public PagedIterator _iterator(int pageSize) { * * @return the total count */ + @Deprecated public int getTotalCount() { - populate(); - return result.totalCount; + // populate(); + return searchPaginatedEndpoint.pages().peek().totalCount; } /** @@ -76,9 +41,10 @@ public int getTotalCount() { * * @return the boolean */ + @Deprecated public boolean isIncomplete() { - populate(); - return result.incompleteResults; + // populate(); + return searchPaginatedEndpoint.pages().peek().incompleteResults; } /** @@ -93,30 +59,13 @@ public PagedSearchIterable withPageSize(int size) { return (PagedSearchIterable) super.withPageSize(size); } - private void populate() { - if (result == null) - iterator().hasNext(); + @Override + PaginatedEndpointItems, T> items() { + return new PaginatedEndpointItems<>(pages()); } - /** - * Adapts {@link Iterator}. - * - * @param base - * the base - * @return the iterator - */ - protected Iterator adapt(final Iterator> base) { - return new Iterator() { - public boolean hasNext() { - return base.hasNext(); - } - - public T[] next() { - SearchResult v = base.next(); - if (result == null) - result = v; - return v.getItems(root); - } - }; + @Override + PaginatedEndpointPages, T> pages() { + return searchPaginatedEndpoint.pages(); } } diff --git a/src/main/java/org/kohsuke/github/PaginatedArrayEndpoint.java b/src/main/java/org/kohsuke/github/PaginatedArrayEndpoint.java new file mode 100644 index 0000000000..67c28efeee --- /dev/null +++ b/src/main/java/org/kohsuke/github/PaginatedArrayEndpoint.java @@ -0,0 +1,73 @@ +package org.kohsuke.github; + +import org.jetbrains.annotations.NotNull; + +import java.io.IOException; +import java.util.*; +import java.util.function.Consumer; + +import javax.annotation.Nonnull; + +/** + * {@link PaginatedEndpoint} implementation that take a {@link Consumer} that initializes all the items on each page as + * they are retrieved. + * + * {@link PaginatedEndpoint} is immutable and thread-safe, but the iterator returned from {@link #iterator()} is not. + * Any one instance of iterator should only be called from a single thread. + * + * @author Liam Newman + * @param + * the type of items on each page + */ +class PaginatedArrayEndpoint extends PaginatedEndpoint, Item> { + + private class ArrayPages extends PaginatedEndpointPages, Item> { + + ArrayPages(GitHubClient client, + Class> pageType, + GitHubRequest request, + int pageSize, + Consumer itemInitializer) { + super(client, pageType, request, pageSize, itemInitializer); + } + + @Override + @NotNull protected GitHubResponse> sendNextRequest() throws IOException { + GitHubResponse response = client.sendRequest(nextRequest, + (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, arrayReceiverType)); + return new GitHubResponse<>(response, new GitHubPageArrayAdapter<>(response.body())); + } + } + + /** + * Pretend to get a specific page class type for the sake of the compile time strong typing. + * + * This class never uses {@code pageType}, so it is safe to pass null as the actual class value. + * + * @param + * The type of items in the array page. + * @param itemType + * The class instance for items in the array page. + * @return Always null, but cast to the appropriate class for compile time strong typing. + */ + private static Class> getArrayPageClass(Class itemType) { + return (Class>) null; + } + + private final Class arrayReceiverType; + + PaginatedArrayEndpoint(GitHubClient client, + GitHubRequest request, + Class arrayReceiverType, + Class itemType, + Consumer itemInitializer) { + super(client, request, getArrayPageClass(itemType), itemType, itemInitializer); + this.arrayReceiverType = arrayReceiverType; + } + + @Nonnull + @Override + public PaginatedEndpointPages, Item> pages() { + return new ArrayPages(client, pageType, request, pageSize, itemInitializer); + } +} diff --git a/src/main/java/org/kohsuke/github/PaginatedEndpoint.java b/src/main/java/org/kohsuke/github/PaginatedEndpoint.java new file mode 100644 index 0000000000..ef9428c966 --- /dev/null +++ b/src/main/java/org/kohsuke/github/PaginatedEndpoint.java @@ -0,0 +1,200 @@ +package org.kohsuke.github; + +import java.io.IOException; +import java.lang.reflect.Array; +import java.util.*; +import java.util.function.Consumer; + +import javax.annotation.Nonnull; + +/** + * {@link PaginatedEndpoint} implementation that take a {@link Consumer} that initializes all the items on each page as + * they are retrieved. + * + * {@link PaginatedEndpoint} is immutable and thread-safe, but the iterator returned from {@link #iterator()} is not. + * Any one instance of iterator should only be called from a single thread. + * + * @author Liam Newman + * @param + * the type of items on each page + */ +class PaginatedEndpoint, 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> 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 GitHubPageIterator create(GitHubClient client, Class type, GitHubRequest request, int pageSize) { - - if (pageSize > 0) { - GitHubRequest.Builder builder = request.toBuilder().with("per_page", pageSize); - request = builder.build(); - } +class PaginatedEndpointPages, Item> implements Iterator { - if (!"GET".equals(request.method())) { - throw new IllegalArgumentException("Request method \"GET\" is required for page iterator."); - } - - return new GitHubPageIterator<>(client, type, request); + static

, I> PaginatedEndpointPages fromSinglePage(Class

pageType, final P page) { + return new PaginatedEndpointPages<>(pageType, page); } - private final GitHubClient client; /** * When done iterating over pages, it is on rare occasions useful to be able to get information from the final * response that was retrieved. */ - private GitHubResponse finalResponse = null; - + private GitHubResponse finalResponse = null; + private final Consumer itemInitializer; /** * The page that will be returned when {@link #next()} is called. * @@ -66,22 +42,43 @@ static GitHubPageIterator create(GitHubClient client, Class type, GitH * Will be {@code null} after {@link #next()} is called. *

*

- * Will not be {@code null} after {@link #fetch()} is called if a new page was fetched. + * Will not be {@code null} after {@link #fetchNext()} is called if a new page was fetched. *

*/ - private T next; + private Page next; + + protected final GitHubClient client; /** * The request that will be sent when to get a new response page if {@link #next} is {@code null}. Will be * {@code null} when there are no more pages to fetch. */ - private GitHubRequest nextRequest; + protected GitHubRequest nextRequest; + + protected final Class pageType; - private final Class type; + /* + * Constructor for PaginatedEndpointPages for single page + */ + PaginatedEndpointPages(Class pageType, Page page) { + this(null, pageType, null, 0, null); + this.next = page; + } - private GitHubPageIterator(GitHubClient client, Class type, GitHubRequest request) { + PaginatedEndpointPages(GitHubClient client, + Class pageType, + GitHubRequest request, + int pageSize, + Consumer itemInitializer) { + this.pageType = pageType; + this.itemInitializer = itemInitializer; this.client = client; - this.type = type; + + if (pageSize > 0) { + GitHubRequest.Builder builder = request.toBuilder().with("per_page", pageSize); + request = builder.build(); + } + this.nextRequest = request; } @@ -90,7 +87,7 @@ private GitHubPageIterator(GitHubClient client, Class type, GitHubRequest req * * @return the final response of the iterator. */ - public GitHubResponse finalResponse() { + public GitHubResponse finalResponse() { if (hasNext()) { throw new GHException("Final response is not available until after iterator is done."); } @@ -101,26 +98,36 @@ public GitHubResponse finalResponse() { * {@inheritDoc} */ public boolean hasNext() { - fetch(); - return next != null; + return peek() != null; } /** - * Gets the next page. - * - * @return the next page. + * {@inheritDoc} */ @Nonnull - public T next() { - fetch(); - T result = next; + public Page next() { + Page result = peek(); if (result == null) throw new NoSuchElementException(); - // If this is the last page, keep the response next = null; return result; } + /** + * + * @return + */ + public Page peek() { + if (next == null) { + Page result = fetchNext(); + if (result != null) { + next = result; + initializeItems(); + } + } + return next; + } + /** * Fetch is called at the start of {@link #hasNext()} or {@link #next()} to fetch another page of data if it is * needed. @@ -134,33 +141,43 @@ public T next() { * after the current response, {@link #nextRequest} is set to {@code null}. *

*/ - private void fetch() { - if (next != null) - return; // already fetched + private Page fetchNext() { if (nextRequest == null) - return; // no more data to fetch + return null; // no more data to fetch + + Page result; URL url = nextRequest.url(); try { - GitHubResponse nextResponse = client.sendRequest(nextRequest, - (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, type)); + GitHubResponse nextResponse = sendNextRequest(); assert nextResponse.body() != null; - next = nextResponse.body(); - nextRequest = findNextURL(nextRequest, nextResponse); - if (nextRequest == null) { - finalResponse = nextResponse; - } + result = nextResponse.body(); + updateNextRequest(nextResponse); } catch (IOException e) { // Iterators do not throw IOExceptions, so we wrap any IOException // in a runtime GHException to bubble out if needed. throw new GHException("Failed to retrieve " + url, e); } + return result; + } + + /** + * This method initializes items with local data after they are fetched. It is up to the implementer to decide what + * local data to apply. + * + */ + private void initializeItems() { + if (itemInitializer != null) { + for (Item item : next.getItems()) { + itemInitializer.accept(item); + } + } } /** * Locate the next page from the pagination "Link" tag. */ - private GitHubRequest findNextURL(GitHubRequest nextRequest, GitHubResponse nextResponse) { + private void updateNextRequest(GitHubResponse nextResponse) { GitHubRequest result = null; String link = nextResponse.header("Link"); if (link != null) { @@ -174,7 +191,15 @@ private GitHubRequest findNextURL(GitHubRequest nextRequest, GitHubResponse n } } } - return result; + nextRequest = result; + if (nextRequest == null) { + // If this is the last page, keep the response + finalResponse = nextResponse; + } } + @NotNull protected GitHubResponse sendNextRequest() throws IOException { + return client.sendRequest(nextRequest, + (connectorResponse) -> GitHubResponse.parseBody(connectorResponse, pageType)); + } } diff --git a/src/main/java/org/kohsuke/github/Requester.java b/src/main/java/org/kohsuke/github/Requester.java index 95f0366ebd..5228ab50b3 100644 --- a/src/main/java/org/kohsuke/github/Requester.java +++ b/src/main/java/org/kohsuke/github/Requester.java @@ -189,16 +189,47 @@ public void sendGraphQL() throws IOException { * or {@link Iterator#hasNext()} are called. *

* + * @param

+ * the page type for the pages returned from * @param * the element type for the pages returned from - * @param type + * @param pageType * the type of the pages to retrieve. * @param itemInitializer * the consumer to execute on each paged item retrieved. * @return the {@link PagedIterable} for this builder. */ - public PagedIterable toIterable(Class type, Consumer itemInitializer) { - return new GitHubPageContentsIterable<>(client, build(), type, itemInitializer); + public

, R> PagedIterable toIterable(Class

pageType, + Class itemType, + Consumer itemInitializer) { + return new PagedIterable<>(toPaginatedEndpoint(pageType, itemType, itemInitializer)); + } + + /** + * Creates {@link PagedIterable } from this builder using the provided {@link Consumer}{@code }. + *

+ * This method and the {@link PagedIterable } do not actually begin fetching data until {@link Iterator#next()} + * or {@link Iterator#hasNext()} are called. + *

+ * + * @param + * the element type for the pages returned from + * @param receiverType + * the type of the array to retrieve. + * @param itemInitializer + * the consumer to execute on each paged item retrieved. + * @return the {@link PagedIterable} for this builder. + */ + public PagedIterable toIterable(Class receiverType, Consumer itemInitializer) { + GitHubRequest request = build(); + return new PagedIterable<>(PaginatedEndpoint.ofArrayEndpoint(client, request, receiverType, itemInitializer)); + } +

, R> PaginatedEndpoint toPaginatedEndpoint(Class

pageType, + Class itemType, + Consumer itemInitializer) { + GitHubRequest request = build(); + return new PaginatedEndpoint<>(client, request, pageType, itemType, itemInitializer); } + } diff --git a/src/main/java/org/kohsuke/github/SearchResult.java b/src/main/java/org/kohsuke/github/SearchResult.java index fe7e350439..2707b86753 100644 --- a/src/main/java/org/kohsuke/github/SearchResult.java +++ b/src/main/java/org/kohsuke/github/SearchResult.java @@ -8,7 +8,7 @@ * @param * the generic type */ -abstract class SearchResult { +abstract class SearchResult implements GitHubPage { /** The incomplete results. */ boolean incompleteResults; @@ -16,6 +16,10 @@ abstract class SearchResult { /** The total count. */ int totalCount; + public T[] getItems() { + return getItems(null); + } + /** * Wraps up the retrieved object and return them. Only called once. * diff --git a/src/main/resources/META-INF/native-image/org.kohsuke/github-api/reflect-config.json b/src/main/resources/META-INF/native-image/org.kohsuke/github-api/reflect-config.json index 30be262b74..951bbc434e 100644 --- a/src/main/resources/META-INF/native-image/org.kohsuke/github-api/reflect-config.json +++ b/src/main/resources/META-INF/native-image/org.kohsuke/github-api/reflect-config.json @@ -6823,6 +6823,140 @@ "allDeclaredMethods": true, "allPublicClasses": true, "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.GHExternalGroupIterable$1$1", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.GitHubPage", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedEndpoint", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.GitHubPageArrayAdapter", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedEndpointItems", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedEndpointPages", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedEndpoint$SinglePageEndpoint", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedArrayEndpoint", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true + }, + { + "name": "org.kohsuke.github.PaginatedArrayEndpoint$ArrayPages", + "allPublicFields": true, + "allDeclaredFields": true, + "queryAllPublicConstructors": true, + "queryAllDeclaredConstructors": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "queryAllPublicMethods": true, + "queryAllDeclaredMethods": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "allPublicClasses": true, + "allDeclaredClasses": true } - ] diff --git a/src/main/resources/META-INF/native-image/org.kohsuke/github-api/serialization-config.json b/src/main/resources/META-INF/native-image/org.kohsuke/github-api/serialization-config.json index 412aa47e18..338f148956 100644 --- a/src/main/resources/META-INF/native-image/org.kohsuke/github-api/serialization-config.json +++ b/src/main/resources/META-INF/native-image/org.kohsuke/github-api/serialization-config.json @@ -1366,5 +1366,32 @@ }, { "name": "org.kohsuke.github.GitHubBridgeAdapterObject" + }, + { + "name": "org.kohsuke.github.GHExternalGroupIterable$1$1" + }, + { + "name": "org.kohsuke.github.GitHubPage" + }, + { + "name": "org.kohsuke.github.PaginatedEndpoint" + }, + { + "name": "org.kohsuke.github.GitHubPageArrayAdapter" + }, + { + "name": "org.kohsuke.github.PaginatedEndpointItems" + }, + { + "name": "org.kohsuke.github.PaginatedEndpointPages" + }, + { + "name": "org.kohsuke.github.PaginatedEndpoint$SinglePageEndpoint" + }, + { + "name": "org.kohsuke.github.PaginatedArrayEndpoint" + }, + { + "name": "org.kohsuke.github.PaginatedArrayEndpoint$ArrayPagest" } ] diff --git a/src/test/java/org/kohsuke/github/AotIntegrationTest.java b/src/test/java/org/kohsuke/github/AotIntegrationTest.java index a8b458f792..ed4ba33605 100644 --- a/src/test/java/org/kohsuke/github/AotIntegrationTest.java +++ b/src/test/java/org/kohsuke/github/AotIntegrationTest.java @@ -41,41 +41,62 @@ public AotIntegrationTest() { public void testIfAllRequiredClassesAreRegisteredForAot() throws IOException { String artifactId = System.getProperty("test.projectArtifactId", "default"); - Stream providedReflectionConfigStreamOfNames = readAotConfigToStreamOfClassNames( - "./target/classes/META-INF/native-image/org.kohsuke/" + artifactId + "/reflect-config.json"); - Stream providedNoReflectStreamOfNames = Files - .lines(Path.of("./target/test-classes/no-reflect-and-serialization-list")); - Stream providedSerializationStreamOfNames = readAotConfigToStreamOfClassNames( - "./target/classes/META-INF/native-image/org.kohsuke/" + artifactId + "/serialization-config.json"); - Stream providedAotConfigClassNamesPart = Stream - .concat(providedSerializationStreamOfNames, - Stream.concat(providedReflectionConfigStreamOfNames, providedNoReflectStreamOfNames)) - .distinct(); - List providedReflectionAndNoReflectionConfigNames = providedAotConfigClassNamesPart + String reflectConfigPath = "./target/classes/META-INF/native-image/org.kohsuke/" + artifactId + + "/reflect-config.json"; + String noReflectPath = "./target/test-classes/no-reflect-and-serialization-list"; + String serializationConfigPath = "./target/classes/META-INF/native-image/org.kohsuke/" + artifactId + + "/serialization-config.json"; + String generatedReflectConfigPath = "./target/spring-aot/test/resources/META-INF/native-image/org.kohsuke/" + + artifactId + "/reflect-config.json"; + String generatedSerializationConfigPath = "./target/spring-aot/test/resources/META-INF/native-image/org.kohsuke/" + + artifactId + "/serialization-config.json"; + + Stream reflectConfigNames = readAotConfigToStreamOfClassNames(reflectConfigPath); + Stream noReflectNames = Files.lines(Path.of(noReflectPath)); + Stream serializationNames = readAotConfigToStreamOfClassNames(serializationConfigPath); + List allConfigClassNames = Stream + .concat(serializationNames, Stream.concat(reflectConfigNames, noReflectNames)) + .distinct() + .sorted() + .collect(Collectors.toList()); + + Stream generatedReflectConfigNames = readAotConfigToStreamOfClassNames(generatedReflectConfigPath); + Stream generatedSerializationNames = readAotConfigToStreamOfClassNames( + generatedSerializationConfigPath); + List allGeneratedConfigClassNames = Stream + .concat(generatedReflectConfigNames, generatedSerializationNames) + .distinct() + .sorted() .collect(Collectors.toList()); - Stream generatedReflectConfigStreamOfClassNames = readAotConfigToStreamOfClassNames( - "./target/spring-aot/test/resources/META-INF/native-image/org.kohsuke/" + artifactId - + "/reflect-config.json"); - Stream generatedSerializationStreamOfNames = readAotConfigToStreamOfClassNames( - "./target/spring-aot/test/resources/META-INF/native-image/org.kohsuke/" + artifactId - + "/serialization-config.json"); - Stream generatedAotConfigClassNames = Stream.concat(generatedReflectConfigStreamOfClassNames, - generatedSerializationStreamOfNames); + StringBuilder failures = new StringBuilder(); - generatedAotConfigClassNames.forEach(generatedReflectionConfigClassName -> { + allGeneratedConfigClassNames.forEach(generatedReflectionConfigClassName -> { try { - if (!providedReflectionAndNoReflectionConfigNames.contains(generatedReflectionConfigClassName)) { - fail(String.format( + if (!allConfigClassNames.contains(generatedReflectionConfigClassName)) { + failures.append(String.format( Files.readString( Path.of("./target/test-classes/reflection-and-serialization-test-error-message")), - generatedReflectionConfigClassName)); + generatedReflectionConfigClassName)).append('\n'); } } catch (IOException e) { throw new RuntimeException(e); } }); + // Future cleanup + // allConfigClassNames.forEach(reflectionConfigClassName -> { + // if (!allGeneratedConfigClassNames.contains(reflectionConfigClassName)) { + // failures.append( + // String.format("Extra class name found in config files: %1$s\n", reflectionConfigClassName)); + // } + // }); + + // Report all failures at once rather than one at a time + String failureString = failures.toString(); + if (!failureString.isEmpty()) { + fail("\n" + failureString); + } } private Stream readAotConfigToStreamOfClassNames(String reflectionConfig) throws IOException { diff --git a/src/test/java/org/kohsuke/github/AppTest.java b/src/test/java/org/kohsuke/github/AppTest.java index cb59f2af62..0668c226d7 100755 --- a/src/test/java/org/kohsuke/github/AppTest.java +++ b/src/test/java/org/kohsuke/github/AppTest.java @@ -362,7 +362,7 @@ public void testCommit() throws Exception { .getRepository("jenkins") .getCommit("08c1c9970af4d609ae754fbe803e06186e3206f7"); assertThat(commit.getParents().size(), equalTo(1)); - assertThat(commit.listFiles().toList().size(), equalTo(1)); + assertThat(commit.listFiles().withPageSize(50).toList().size(), equalTo(1)); assertThat(commit.getHtmlUrl().toString(), equalTo("https://github.com/jenkinsci/jenkins/commit/08c1c9970af4d609ae754fbe803e06186e3206f7")); assertThat(commit.getLinesAdded(), equalTo(40)); @@ -407,7 +407,7 @@ public void testCommit() throws Exception { public void testCommitComment() throws Exception { GHRepository r = gitHub.getUser("jenkinsci").getRepository("jenkins"); PagedIterable comments = r.listCommitComments(); - List batch = comments.iterator().nextPage(); + List batch = comments.pages().next().getItemsList(); for (GHCommitComment comment : batch) { // System.out.println(comment.getBody()); assertThat(r, sameInstance(comment.getOwner())); @@ -432,6 +432,18 @@ public void testCommitSearch() throws IOException { GHCommit firstCommit = r.iterator().next(); assertThat(firstCommit.listFiles().toList(), is(not(empty()))); + + GHCommitSearchBuilder builder = new GHCommitSearchBuilder(GitHub.offline()); + GHException e = Assert.assertThrows(GHException.class, () -> builder.list().iterator().next()); + assertThat(e.getMessage(), equalTo("Failed to retrieve https://api.github.invalid/search/commits?q=")); + + // Verify that this item initalizer does not throw when it otherwise would + builder.lateBindGHCommit(new GHCommit() { + @Override + public URL getUrl() { + return firstCommit.getUrl(); + } + }); } /** diff --git a/src/test/java/org/kohsuke/github/GHObjectTest.java b/src/test/java/org/kohsuke/github/GHObjectTest.java index 04cb3ac365..5e06a2a08e 100644 --- a/src/test/java/org/kohsuke/github/GHObjectTest.java +++ b/src/test/java/org/kohsuke/github/GHObjectTest.java @@ -25,16 +25,17 @@ public GHObjectTest() { @Test public void test_toString() throws Exception { GHOrganization org = gitHub.getOrganization(GITHUB_API_TEST_ORG); - assertThat(org.toString(), containsString("login=hub4j-test-org")); - assertThat(org.toString(), containsString("location=")); - assertThat(org.toString(), containsString("blog=")); - assertThat(org.toString(), containsString("email=")); - assertThat(org.toString(), containsString("bio=")); - assertThat(org.toString(), containsString("name=")); - assertThat(org.toString(), containsString("company=")); - assertThat(org.toString(), containsString("type=Organization")); - assertThat(org.toString(), containsString("followers=0")); - assertThat(org.toString(), containsString("hireable=false")); + String orgString = org.toString(); + assertThat(orgString, containsString("login=hub4j-test-org")); + assertThat(orgString, containsString("location=")); + assertThat(orgString, containsString("blog=")); + assertThat(orgString, containsString("email=")); + assertThat(orgString, containsString("bio=")); + assertThat(orgString, containsString("name=")); + assertThat(orgString, containsString("company=")); + assertThat(orgString, containsString("type=Organization")); + assertThat(orgString, containsString("followers=0")); + assertThat(orgString, containsString("hireable=false")); // getResponseHeaderFields is deprecated but we should not break it. assertThat(org.getResponseHeaderFields(), notNullValue()); diff --git a/src/test/java/org/kohsuke/github/GHRepositoryTest.java b/src/test/java/org/kohsuke/github/GHRepositoryTest.java index db5d892f85..876f49af23 100644 --- a/src/test/java/org/kohsuke/github/GHRepositoryTest.java +++ b/src/test/java/org/kohsuke/github/GHRepositoryTest.java @@ -4,6 +4,7 @@ import com.google.common.collect.Sets; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; import org.apache.commons.io.IOUtils; +import org.assertj.core.util.Objects; import org.junit.Assert; import org.junit.Test; import org.kohsuke.github.GHCheckRun.Conclusion; @@ -321,8 +322,14 @@ public void getCheckRuns() throws Exception { PagedIterable checkRuns = gitHub.getOrganization("hub4j") .getRepository("github-api") .getCheckRuns("78b9ff49d47daaa158eb373c4e2e040f739df8b9"); + + var endpointItems = checkRuns.withPageSize(2).items(); + var currentPage = endpointItems.getCurrentPage(); + var firstPage = Objects.castIfBelongsToType(currentPage, GHCheckRunsPage.class); + assertThat(firstPage.getTotalCount(), equalTo(8)); + // Check if the paging works correctly - assertThat(checkRuns.withPageSize(2).iterator().nextPage(), hasSize(2)); + assertThat(checkRuns.withPageSize(2).items().nextPage(), hasSize(2)); // Check if the checkruns are all succeeded and if we got all of them int checkRunsCount = 0; diff --git a/src/test/java/org/kohsuke/github/GHUserTest.java b/src/test/java/org/kohsuke/github/GHUserTest.java index adeaa294e8..e695ecae6d 100644 --- a/src/test/java/org/kohsuke/github/GHUserTest.java +++ b/src/test/java/org/kohsuke/github/GHUserTest.java @@ -245,7 +245,7 @@ public void verifySuspendedAt() throws IOException { private Set count30(PagedIterable l) { Set users = new HashSet(); - PagedIterator itr = l.iterator(); + Iterator itr = l.iterator(); for (int i = 0; i < 30 && itr.hasNext(); i++) { users.add(itr.next()); } diff --git a/src/test/java/org/kohsuke/github/GHWorkflowRunTest.java b/src/test/java/org/kohsuke/github/GHWorkflowRunTest.java index f2eda74276..7b5cda128c 100644 --- a/src/test/java/org/kohsuke/github/GHWorkflowRunTest.java +++ b/src/test/java/org/kohsuke/github/GHWorkflowRunTest.java @@ -1,5 +1,6 @@ package org.kohsuke.github; +import org.assertj.core.util.Objects; import org.awaitility.Awaitility; import org.junit.Before; import org.junit.Test; @@ -17,10 +18,13 @@ import java.util.List; import java.util.Optional; import java.util.Scanner; +import java.util.Spliterator; +import java.util.Spliterators; import java.util.function.Function; import java.util.logging.Level; import java.util.logging.Logger; import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; @@ -154,7 +158,7 @@ private static Optional getWorkflowRun(GHRepository repository, .event(GHEvent.PULL_REQUEST) .list() .withPageSize(20) - .iterator() + .items() .nextPage(); for (GHWorkflowRun workflowRun : workflowRuns) { @@ -176,7 +180,7 @@ private static Optional getWorkflowRun(GHRepository repository, .event(GHEvent.WORKFLOW_DISPATCH) .list() .withPageSize(20) - .iterator() + .items() .nextPage(); for (GHWorkflowRun workflowRun : workflowRuns) { @@ -347,8 +351,12 @@ public void testArtifacts() throws IOException { checkArtifactProperties(artifactById, "artifact2"); // Test GHRepository#listArtifacts() as we are sure we have artifacts around - List artifactsFromRepo = new ArrayList<>( - repo.listArtifacts().withPageSize(2).iterator().nextPage()); + var endpointItems = repo.listArtifacts().withPageSize(2).items(); + var currentPage = endpointItems.getCurrentPage(); + var firstPage = Objects.castIfBelongsToType(currentPage, GHArtifactsPage.class); + assertThat(firstPage.getTotalCount(), equalTo(69)); + + List artifactsFromRepo = new ArrayList<>(endpointItems.nextPage()); artifactsFromRepo.sort((a1, a2) -> a1.getName().compareTo(a2.getName())); // We have at least the two artifacts we just added @@ -524,9 +532,13 @@ public void testJobs() throws IOException { latestPreexistingWorkflowRunId) .orElseThrow(() -> new IllegalStateException("We must have a valid workflow run starting from here")); - List jobs = workflowRun.listJobs() - .toList() - .stream() + var endpointItems = workflowRun.listJobs().items(); + var currentPage = endpointItems.getCurrentPage(); + var firstPage = Objects.castIfBelongsToType(currentPage, GHWorkflowJobsPage.class); + assertThat(firstPage.getTotalCount(), equalTo(2)); + + List jobs = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(endpointItems, Spliterator.ORDERED), false) .sorted((j1, j2) -> j1.getName().compareTo(j2.getName())) .collect(Collectors.toList()); @@ -547,7 +559,7 @@ public void testJobs() throws IOException { checkJobProperties(workflowRun.getId(), job1ById, "job1"); // Also test listAllJobs() works correctly - List allJobs = workflowRun.listAllJobs().withPageSize(10).iterator().nextPage(); + List allJobs = workflowRun.listAllJobs().withPageSize(10).items().nextPage(); assertThat(allJobs.size(), greaterThanOrEqualTo(2)); } @@ -763,9 +775,13 @@ public void testStartupFailureConclusion() throws IOException { GHWorkflow ghWorkflow = repo.getWorkflow("startup-failure-workflow.yml"); - List ghWorkflowRunList = ghWorkflow.listRuns().toList(); + var endpointItems = ghWorkflow.listRuns().items(); + var currentPage = endpointItems.getCurrentPage(); + var firstPage = Objects.castIfBelongsToType(currentPage, GHWorkflowRunsPage.class); + assertThat(firstPage.getTotalCount(), equalTo(4)); - List list = ghWorkflowRunList.stream() + List list = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(endpointItems, Spliterator.ORDERED), false) .filter(ghWorkflowRun -> ghWorkflowRun.getConclusion().equals(Conclusion.STARTUP_FAILURE)) .collect(Collectors.toList()); diff --git a/src/test/java/org/kohsuke/github/GHWorkflowTest.java b/src/test/java/org/kohsuke/github/GHWorkflowTest.java index 836907ae91..5720457b0b 100644 --- a/src/test/java/org/kohsuke/github/GHWorkflowTest.java +++ b/src/test/java/org/kohsuke/github/GHWorkflowTest.java @@ -1,5 +1,6 @@ package org.kohsuke.github; +import org.assertj.core.util.Objects; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -7,13 +8,16 @@ import java.io.IOException; import java.util.Collections; import java.util.List; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import static com.github.tomakehurst.wiremock.client.WireMock.containing; import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.github.tomakehurst.wiremock.client.WireMock.verify; import static org.hamcrest.Matchers.*; -import static org.hamcrest.Matchers.notNullValue; // TODO: Auto-generated Javadoc /** @@ -184,7 +188,15 @@ public void testListWorkflowRuns() throws IOException { */ @Test public void testListWorkflows() throws IOException { - List workflows = repo.listWorkflows().toList(); + + var endpointItems = repo.listWorkflows().items(); + var currentPage = endpointItems.getCurrentPage(); + var firstPage = Objects.castIfBelongsToType(currentPage, GHWorkflowsPage.class); + assertThat(firstPage.getTotalCount(), equalTo(1)); + + List workflows = StreamSupport + .stream(Spliterators.spliteratorUnknownSize(endpointItems, Spliterator.ORDERED), false) + .collect(Collectors.toList()); GHWorkflow workflow = workflows.get(0); assertThat(workflow.getId(), equalTo(6817859L)); diff --git a/src/test/java/org/kohsuke/github/GitHubTest.java b/src/test/java/org/kohsuke/github/GitHubTest.java index 794eff5297..4cef99ba6a 100644 --- a/src/test/java/org/kohsuke/github/GitHubTest.java +++ b/src/test/java/org/kohsuke/github/GitHubTest.java @@ -294,10 +294,11 @@ public void searchContent() throws Exception { PagedSearchIterable r4 = searchBuilder.list(); - GHContent c4 = r4.iterator().next(); + GHContent c4 = r4.withPageSize(25).iterator().next(); assertThat(c4.getPath(), not(equalTo(c2.getPath()))); assertThat(c4.getPath(), not(equalTo(c3.getPath()))); assertThat(r4.getTotalCount(), equalTo(r2.getTotalCount())); + assertThat(r4.isIncomplete(), equalTo(false)); // Verify qualifier not allowed to be empty IllegalArgumentException e = Assert.assertThrows(IllegalArgumentException.class, diff --git a/src/test/java/org/kohsuke/github/PaginatedEndpointTest.java b/src/test/java/org/kohsuke/github/PaginatedEndpointTest.java new file mode 100644 index 0000000000..2aaa189b4f --- /dev/null +++ b/src/test/java/org/kohsuke/github/PaginatedEndpointTest.java @@ -0,0 +1,132 @@ +package org.kohsuke.github; + +import org.junit.Test; + +import java.io.*; +import java.util.NoSuchElementException; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.assertThrows; + +/** + * The Class PaginatedEndpointTest. + * + * @author Liam Newman + */ +public class PaginatedEndpointTest extends AbstractGitHubWireMockTest { + + /** + * Test + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void peekNullShouldThrow() throws IOException { + var page = new GitHubPageArrayAdapter<>(new Object[]{}); + var pageType = (Class>) page.getClass(); + + // calling next + var emptyPages = new PaginatedEndpointPages<>(pageType, page); + var emptyPageItems = emptyPages.next().getItems(); + assertThat(emptyPageItems.length, equalTo(0)); + assertThat(emptyPages.hasNext(), equalTo(false)); + // Calling next when hasNext() is false, throws. + assertThrows(NoSuchElementException.class, () -> emptyPages.next()); + + // Calling items.next() on an empty result should throw. + var items = new PaginatedEndpointItems<>(new PaginatedEndpointPages<>(pageType, page)); + assertThat(items.peek(), equalTo(null)); + assertThat(items.hasNext(), equalTo(false)); + assertThrows(NoSuchElementException.class, () -> items.next()); + } + + /** + * Test + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void testDeprecatedNextPage() throws IOException { + var page = new GitHubPageArrayAdapter<>(new Object[]{ 1, 2, 3 }); + var pageType = (Class>) page.getClass(); + + var items = new PaginatedEndpointItems<>(new PaginatedEndpointPages<>(pageType, page)); + assertThat(items.next(), equalTo(1)); + // Current Page on a partially read page should return full page + var itemsPage = items.getCurrentPage().getItems(); + assertThat(itemsPage.length, equalTo(3)); + + var partialPage = items.nextPage(); + // Next Page on a partially read page should return + assertThat(partialPage.size(), equalTo(2)); + assertThrows(NoSuchElementException.class, () -> items.next()); + assertThrows(NoSuchElementException.class, () -> items.nextPage()); + } + + /** + * Test + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void testIterators() throws IOException { + var page = new GitHubPageArrayAdapter<>(new Object[]{ 1, 2, 3 }); + + var endpoint = PaginatedEndpoint.fromSinglePage(page, Object.class); + + // Removed Iterable from PaginatedEndpoint for simplicity. + // Can be added later if desired, swap in this line to cover iterator() method. + // var iterator = endpoint.iterator(); + var iterator = endpoint.items(); + + assertThat(iterator.next(), equalTo(1)); + assertThat(iterator.next(), equalTo(2)); + assertThat(iterator.next(), equalTo(3)); + assertThat(iterator.hasNext(), equalTo(false)); + + var pagedIterator = new PagedIterator<>(endpoint.items()); + assertThat(pagedIterator.next(), equalTo(1)); + var nextPage = pagedIterator.nextPage(); + assertThat(nextPage.size(), equalTo(2)); + assertThat(iterator.hasNext(), equalTo(false)); + } + + /** + * Test + * + * @throws IOException + * Signals that an I/O exception has occurred. + */ + @Test + public void testNextPageError() throws IOException { + var page = new GitHubPageArrayAdapter<>(new Object[]{}) { + @Override + public Object[] getItems() { + throw new GHException("outer", new Exception("inner")); + } + }; + + var pageIo = new GitHubPageArrayAdapter<>(new Object[]{}) { + @Override + public Object[] getItems() { + throw new GHException("outer", new IOException("inner")); + } + }; + + final var pages = PaginatedEndpoint.fromSinglePage(page, Object.class).pages(); + + var e = assertThrows(GHException.class, () -> pages.finalResponse()); + assertThat(e.getMessage(), equalTo("Final response is not available until after iterator is done.")); + + var ex = assertThrows(GHException.class, () -> PaginatedEndpoint.toList(pages, Object.class)); + assertThat(ex.getMessage(), equalTo("outer")); + + final var pagesIo = PaginatedEndpoint.fromSinglePage(pageIo, Object.class).pages(); + var exIo = assertThrows(IOException.class, () -> PaginatedEndpoint.toList(pagesIo, Object.class)); + assertThat(exIo.getMessage(), equalTo("inner")); + } + +} diff --git a/src/test/resources/no-reflect-and-serialization-list b/src/test/resources/no-reflect-and-serialization-list index 4ad893272c..86aae594b3 100644 --- a/src/test/resources/no-reflect-and-serialization-list +++ b/src/test/resources/no-reflect-and-serialization-list @@ -25,7 +25,7 @@ org.kohsuke.github.GitHubClient$BodyHandler org.kohsuke.github.GitHubClient$GHApiInfo org.kohsuke.github.GitHubClient$RetryRequestException org.kohsuke.github.GitHubConnectorResponseErrorHandler -org.kohsuke.github.GitHubPageIterator +org.kohsuke.github.GitHubEndpointPageIterator org.kohsuke.github.GitHubRateLimitChecker org.kohsuke.github.GitHubRateLimitHandler org.kohsuke.github.GitHubRateLimitHandler$1 diff --git a/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/searchContent/mappings/6-search_code.json b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/searchContent/mappings/6-search_code.json index 103b4ae0ea..942a07a309 100644 --- a/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/searchContent/mappings/6-search_code.json +++ b/src/test/resources/org/kohsuke/github/GitHubTest/wiremock/searchContent/mappings/6-search_code.json @@ -2,7 +2,7 @@ "id": "a9b8870b-33d7-4164-a407-310342d68536", "name": "search_code", "request": { - "url": "/search/code?sort=indexed&order=desc&q=addClass+in%3Afile+language%3Ajs+repo%3Ajquery%2Fjquery", + "url": "/search/code?sort=indexed&order=desc&q=addClass+in%3Afile+language%3Ajs+repo%3Ajquery%2Fjquery&per_page=25", "method": "GET", "headers": { "Accept": {