diff --git a/src/main/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeader.java b/src/main/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeader.java new file mode 100644 index 0000000..b133e0d --- /dev/null +++ b/src/main/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeader.java @@ -0,0 +1,74 @@ +package org.jenkinsci.plugin.gitea.client.http; + +import java.net.HttpURLConnection; + +/** + * @see RFC8288 + */ +public class PageLinkHeader { + + public static final String HEADER = "Link"; + private static final String PARAMETER_SEPARATOR = ","; + private static final String ATTRIBUTE_SEPARATOR = ";"; + + private String first; + private String last; + private String next; + private String prev; + + private PageLinkHeader(HttpURLConnection connection) { + String headerField = connection.getHeaderField(HEADER); + if (headerField != null) { + for (String rawLink : headerField.split(PARAMETER_SEPARATOR)) { + String[] attributes = rawLink.split(ATTRIBUTE_SEPARATOR); + if (attributes.length < 2) { + continue; + } + + String link = attributes[0].trim(); + if (!link.startsWith("<") || !link.endsWith(">")) { + continue; + } + + String parsedLink = link.substring(1, link.length() - 1); + for (int i = 1; i < attributes.length; i++) { + String[] parameterAttributes = attributes[i].split("="); + if (parameterAttributes.length < 2 || !parameterAttributes[0].trim().equals("rel")) { + continue; + } + + String parameterAttributeValue = parameterAttributes[1].replace("\"", "").toLowerCase(); + if ("first".equals(parameterAttributeValue)) { + first = parsedLink; + } else if ("last".equals(parameterAttributeValue)) { + last = parsedLink; + } else if ("next".equals(parameterAttributeValue)) { + next = parsedLink; + } else if ("prev".equals(parameterAttributeValue)) { + prev = parsedLink; + } + } + } + } + } + + public static PageLinkHeader from(HttpURLConnection connection) { + return new PageLinkHeader(connection); + } + + public String getFirst() { + return first; + } + + public String getLast() { + return last; + } + + public String getNext() { + return next; + } + + public String getPrev() { + return prev; + } +} diff --git a/src/main/java/org/jenkinsci/plugin/gitea/client/impl/DefaultGiteaConnection.java b/src/main/java/org/jenkinsci/plugin/gitea/client/impl/DefaultGiteaConnection.java index 07aeeac..d5f1518 100644 --- a/src/main/java/org/jenkinsci/plugin/gitea/client/impl/DefaultGiteaConnection.java +++ b/src/main/java/org/jenkinsci/plugin/gitea/client/impl/DefaultGiteaConnection.java @@ -41,10 +41,11 @@ import java.security.AccessController; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; +import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; -import java.util.Iterator; import java.util.List; +import java.util.Objects; import java.util.Set; import javax.net.ssl.HttpsURLConnection; import jenkins.model.Jenkins; @@ -70,6 +71,7 @@ import org.jenkinsci.plugin.gitea.client.api.GiteaTag; import org.jenkinsci.plugin.gitea.client.api.GiteaUser; import org.jenkinsci.plugin.gitea.client.api.GiteaVersion; +import org.jenkinsci.plugin.gitea.client.http.PageLinkHeader; import org.kohsuke.accmod.Restricted; import org.kohsuke.accmod.restrictions.NoExternalUse; @@ -237,7 +239,7 @@ public List fetchRepositories(String username) throws IOExcepti @Override public List fetchRepositories(GiteaOwner owner) throws IOException, InterruptedException { - if(owner instanceof GiteaOrganization) { + if (owner instanceof GiteaOrganization) { return fetchOrganizationRepositories(owner); } return fetchRepositories(owner.getUsername()); @@ -245,7 +247,8 @@ public List fetchRepositories(GiteaOwner owner) throws IOExcept } @Override - public List fetchOrganizationRepositories(GiteaOwner owner) throws IOException, InterruptedException { + public List fetchOrganizationRepositories(GiteaOwner owner) + throws IOException, InterruptedException { return getList( api() .literal("/orgs") @@ -327,7 +330,8 @@ public GiteaAnnotatedTag fetchAnnotatedTag(String username, String repository, S } @Override - public GiteaAnnotatedTag fetchAnnotatedTag(GiteaRepository repository, GiteaTag tag) throws IOException, InterruptedException { + public GiteaAnnotatedTag fetchAnnotatedTag(GiteaRepository repository, GiteaTag tag) + throws IOException, InterruptedException { return fetchAnnotatedTag(repository.getOwner().getUsername(), repository.getName(), tag.getId()); } @@ -961,34 +965,45 @@ private T patch(UriTemplate template, Object body, final Class modelClass private List getList(UriTemplate template, final Class modelClass) throws IOException, InterruptedException { - HttpURLConnection connection = openConnection(template); - withAuthentication(connection); - try { - connection.connect(); - int status = connection.getResponseCode(); - if (status / 100 == 2) { - try (InputStream is = connection.getInputStream()) { - List list = mapper.readerFor(mapper.getTypeFactory() - .constructCollectionType(List.class, modelClass)) - .readValue(is); - // strip null values from the list - for (Iterator iterator = list.iterator(); iterator.hasNext(); ) { - if (iterator.next() == null) { - iterator.remove(); - } + + String uri = template.expand(); + + List result = new ArrayList<>(); + while (uri != null) { + HttpURLConnection connection = openConnection(uri); + withAuthentication(connection); + try { + connection.connect(); + int status = connection.getResponseCode(); + if (status / 100 == 2) { + uri = PageLinkHeader.from(connection).getNext(); + try (InputStream is = connection.getInputStream()) { + result.addAll(mapper.readerFor(mapper.getTypeFactory() + .constructCollectionType(List.class, modelClass)) + .readValue(is)); + + // strip null values from the list + result.removeIf(Objects::isNull); } - return list; + } else { + throw new GiteaHttpStatusException(status, connection.getResponseMessage()); } + } finally { + connection.disconnect(); } - throw new GiteaHttpStatusException(status, connection.getResponseMessage()); - } finally { - connection.disconnect(); } + + return result; } @Restricted(NoExternalUse.class) protected HttpURLConnection openConnection(UriTemplate template) throws IOException { - URL url = new URL(template.expand()); + return openConnection(template.expand()); + } + + @Restricted(NoExternalUse.class) + protected HttpURLConnection openConnection(String uri) throws IOException { + URL url = new URL(uri); Jenkins jenkins = Jenkins.get(); if (jenkins.proxy == null) { return (HttpURLConnection) url.openConnection(); diff --git a/src/test/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeaderTest.java b/src/test/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeaderTest.java new file mode 100644 index 0000000..6bf28aa --- /dev/null +++ b/src/test/java/org/jenkinsci/plugin/gitea/client/http/PageLinkHeaderTest.java @@ -0,0 +1,56 @@ +package org.jenkinsci.plugin.gitea.client.http; + +import java.net.HttpURLConnection; +import org.junit.Assert; +import org.junit.Test; +import org.mockito.Mockito; + +public class PageLinkHeaderTest { + + @Test + public void given__pageable_connection__when__fetch__then__parse() { + StringBuilder headerBuilder = new StringBuilder(); + headerBuilder.append("; rel=\"next\","); + headerBuilder.append("; rel=\"last\","); + headerBuilder.append("; rel=\"first\","); + headerBuilder.append("; rel=\"prev\""); + + HttpURLConnection connect = Mockito.mock(HttpURLConnection.class); + Mockito.when(connect.getHeaderField(PageLinkHeader.HEADER)).thenReturn(headerBuilder.toString()); + + + PageLinkHeader linkHeader = PageLinkHeader.from(connect); + + Assert.assertNotNull(linkHeader.getFirst()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=1", linkHeader.getFirst()); + + Assert.assertNotNull(linkHeader.getLast()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=3", linkHeader.getLast()); + + Assert.assertNotNull(linkHeader.getPrev()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=1", linkHeader.getPrev()); + + Assert.assertNotNull(linkHeader.getNext()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=3", linkHeader.getNext()); + } + + @Test + public void given__pageable_rel_not_first__when__fetch__then__parse() { + StringBuilder headerBuilder = new StringBuilder(); + headerBuilder.append("; attr=value; rel=\"next\","); + headerBuilder.append("; rel=\"last\","); + + HttpURLConnection connect = Mockito.mock(HttpURLConnection.class); + Mockito.when(connect.getHeaderField(PageLinkHeader.HEADER)).thenReturn(headerBuilder.toString()); + + + PageLinkHeader linkHeader = PageLinkHeader.from(connect); + + Assert.assertNotNull(linkHeader.getNext()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=2", linkHeader.getNext()); + + Assert.assertNotNull(linkHeader.getLast()); + Assert.assertEquals("http://try.gitea.io/api/v1/orgs/test_org/repos?page=2", linkHeader.getLast()); + } + +} diff --git a/src/test/java/org/jenkinsci/plugin/gitea/client/impl/GiteaConnection_DisabledPR_Issues.java b/src/test/java/org/jenkinsci/plugin/gitea/client/impl/GiteaConnection_DisabledPR_Issues.java index f0cfbfc..642d8a5 100644 --- a/src/test/java/org/jenkinsci/plugin/gitea/client/impl/GiteaConnection_DisabledPR_Issues.java +++ b/src/test/java/org/jenkinsci/plugin/gitea/client/impl/GiteaConnection_DisabledPR_Issues.java @@ -14,7 +14,7 @@ public class GiteaConnection_DisabledPR_Issues extends DefaultGiteaConnection { } @Override - protected HttpURLConnection openConnection(UriTemplate template) throws IOException { + protected HttpURLConnection openConnection(String uri) throws IOException { throw new GiteaHttpStatusException(404, "TEST Case"); } }