diff --git a/src/main/java/org/gitlab4j/api/Pager.java b/src/main/java/org/gitlab4j/api/Pager.java
new file mode 100644
index 0000000000000000000000000000000000000000..30a06bdd0e58ab989e8d22261df19b5e467e2962
--- /dev/null
+++ b/src/main/java/org/gitlab4j/api/Pager.java
@@ -0,0 +1,267 @@
+package org.gitlab4j.api;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+
+import javax.ws.rs.core.MultivaluedMap;
+import javax.ws.rs.core.Response;
+
+import org.gitlab4j.api.utils.JacksonJson;
+
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * This class defines an Iterator implementation that is used as a paging iterator for all API methods that
+ * return a List of objects. It hides the details of interacting with the GitLab API when paging is involved
+ * simplifying accessing large lists of objects.
+ *
+ *
Example usage:
+ *
+ *
+ * // Get a Pager instance that will page through the projects with 10 projects per page
+ * Pager<Project> projectPager = gitlabApi.getProjectsApi().getProjectsPager(10);
+ *
+ * // Iterate through the pages and print out the name and description
+ * while (projectsPager.hasNext())) {
+ * List<Project> projects = projectsPager.next();
+ * for (Project project : projects) {
+ * System.out.println(project.getName() + " -: " + project.getDescription());
+ * }
+ * }
+ *
+ *
+ * @param the GitLab4J type contained in the List.
+ */
+public class Pager implements Iterator>, Constants {
+
+ private int itemsPerPage;
+ private int totalPages;
+ private int totalItems;
+ private int currentPage;
+
+ private List pageParam = new ArrayList<>(1);
+ private List currentItems;
+
+ private AbstractApi api;
+ private MultivaluedMap queryParams;
+ private Object[] pathArgs;
+
+ private static JacksonJson jacksonJson = new JacksonJson();
+ private static ObjectMapper mapper = jacksonJson.getObjectMapper();
+ private JavaType javaType;
+
+ /**
+ * Creates a Pager instance to access the API through the specified path and query parameters.
+ *
+ * @param api the AbstractApi implementation to communicate through
+ * @param type the GitLab4J type that will be contained in the List
+ * @param itemsPerPage items per page
+ * @param queryParams HTTP query params
+ * @param pathArgs HTTP path arguments
+ * @throws GitLabApiException if any error occurs
+ */
+ Pager(AbstractApi api, Class type, int itemsPerPage, MultivaluedMap queryParams, Object... pathArgs) throws GitLabApiException {
+
+ javaType = mapper.getTypeFactory().constructCollectionType(List.class, type);
+
+ // Make sure the per_page parameter is present
+ if (queryParams == null) {
+ queryParams = new GitLabApiForm().withParam(PER_PAGE_PARAM, itemsPerPage).asMap();
+ } else {
+ queryParams.remove(PER_PAGE_PARAM);
+ queryParams.add(PER_PAGE_PARAM, Integer.toString(itemsPerPage));
+ }
+
+ // Set the page param to 1
+ pageParam = new ArrayList<>();
+ pageParam.add("1");
+ queryParams.put(PAGE_PARAM, pageParam);
+ Response response = api.get(Response.Status.OK, queryParams, pathArgs);
+
+ try {
+ currentItems = mapper.readValue((InputStream) response.getEntity(), javaType);
+ } catch (IOException e) {
+ throw new GitLabApiException(e);
+ }
+
+ this.api = api;
+ this.queryParams = queryParams;
+ this.pathArgs = pathArgs;
+ this.itemsPerPage = getHeaderValue(response, PER_PAGE);
+ totalPages = getHeaderValue(response, TOTAL_PAGES_HEADER);
+ totalItems = getHeaderValue(response, TOTAL_HEADER);
+ }
+
+ /**
+ * Get the specified integer header value from the Response instance.
+ *
+ * @param response the Response instance to get the value from
+ * @param key the HTTP header key to get the value for
+ * @return the specified integer header value from the Response instance
+ * @throws GitLabApiException if any error occurs
+ */
+ private int getHeaderValue(Response response, String key) throws GitLabApiException {
+
+ String value = response.getHeaderString(key);
+ value = (value != null ? value.trim() : null);
+ if (value == null || value.length() == 0)
+ throw new GitLabApiException("Missing '" + key + "' header from server");
+
+ try {
+ return (Integer.parseInt(value));
+ } catch (NumberFormatException nfe) {
+ throw new GitLabApiException("Invalid '" + key + "' header value (" + value + ") from server");
+ }
+ }
+
+ /**
+ * Sets the "page" query parameter.
+ *
+ * @param page the value for the "page" query parameter
+ */
+ private void setPageParam(int page) {
+ pageParam.set(0, Integer.toString(page));
+ queryParams.put(PAGE_PARAM, pageParam);
+ }
+
+ /**
+ * Get the items per page value.
+ *
+ * @return the items per page value
+ */
+ public int getItemsPerPage() {
+ return (itemsPerPage);
+ }
+
+ /**
+ * Get the total number of pages returned by the GitLab API.
+ *
+ * @return the total number of pages returned by the GitLab API
+ */
+ public int getTotalPages() {
+ return (totalPages);
+ }
+
+ /**
+ * Get the total number of items (T instances) returned by the GitLab API.
+ *
+ * @return the total number of items (T instances) returned by the GitLab API
+ */
+ public int getTotalItems() {
+ return (totalItems);
+ }
+
+ /**
+ * Get the current page of the iteration.
+ *
+ * @return the current page of the iteration
+ */
+ public int getCurrentPage() {
+ return (currentPage);
+ }
+
+ /**
+ * Returns the true if there are additional pages to iterate over, otherwise returns false.
+ *
+ * @return true if there are additional pages to iterate over, otherwise returns false
+ */
+ @Override
+ public boolean hasNext() {
+ return (currentPage < totalPages);
+ }
+
+ /**
+ * Returns the next List in the iteration containing the next page of objects.
+ *
+ * @return the next List in the iteration
+ * @throws NoSuchElementException if the iteration has no more elements
+ * @throws RuntimeException if a GitLab API error occurs, will contain a wrapped GitLabApiException with the details of the error
+ */
+ @Override
+ public List next() {
+ return (page(currentPage + 1));
+ }
+
+ /**
+ * Returns the first page of List. Will rewind the iterator.
+ *
+ * @return the first page of List
+ * @throws GitLabApiException if any error occurs
+ */
+ public List first() throws GitLabApiException {
+ return (page(1));
+ }
+
+ /**
+ * Returns the last page of List. Will set the iterator to the end.
+ *
+ * @return the last page of List
+ * @throws GitLabApiException if any error occurs
+ */
+ public List last() throws GitLabApiException {
+ return (page(totalPages));
+ }
+
+ /**
+ * Returns the previous page of List. Will set the iterator to the previous page.
+ *
+ * @return the previous page of List
+ * @throws GitLabApiException if any error occurs
+ */
+ public List previous() throws GitLabApiException {
+ return (page(currentPage - 1));
+ }
+
+ /**
+ * Returns the current page of List.
+ *
+ * @return the current page of List
+ * @throws GitLabApiException if any error occurs
+ */
+ public List current() throws GitLabApiException {
+ return (page(currentPage));
+ }
+
+ /**
+ * Returns the specified page of List.
+ *
+ * @param pageNumber the page to get
+ * @return the specified page of List
+ * @throws NoSuchElementException if the iteration has no more elements
+ * @throws RuntimeException if a GitLab API error occurs, will contain a wrapped GitLabApiException with the details of the error
+ */
+ public List page(int pageNumber) {
+
+ if (pageNumber > totalPages) {
+ throw new NoSuchElementException();
+ } else if (pageNumber < 1) {
+ throw new NoSuchElementException();
+ }
+
+ if (currentPage == 0 && pageNumber == 1) {
+ currentPage = 1;
+ return (currentItems);
+ }
+
+ if (currentPage == pageNumber) {
+ return (currentItems);
+ }
+
+ try {
+
+ setPageParam(pageNumber);
+ Response response = api.get(Response.Status.OK, queryParams, pathArgs);
+ currentItems = mapper.readValue((InputStream) response.getEntity(), javaType);
+ currentPage = pageNumber;
+ return (currentItems);
+
+ } catch (GitLabApiException | IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+}
diff --git a/src/test/java/org/gitlab4j/api/TestPager.java b/src/test/java/org/gitlab4j/api/TestPager.java
new file mode 100644
index 0000000000000000000000000000000000000000..a995ce428285e13c9d36412d6803708a09f33b52
--- /dev/null
+++ b/src/test/java/org/gitlab4j/api/TestPager.java
@@ -0,0 +1,136 @@
+package org.gitlab4j.api;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assume.assumeTrue;
+
+import java.util.List;
+
+import org.gitlab4j.api.GitLabApi.ApiVersion;
+import org.gitlab4j.api.models.Project;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runners.MethodSorters;
+
+/**
+ * In order for these tests to run you must set the following systems properties:
+ *
+ * TEST_NAMESPACE
+ * TEST_HOST_URL
+ * TEST_PRIVATE_TOKEN
+ *
+ * If any of the above are NULL, all tests in this class will be skipped. If running from mvn simply
+ * use a command line similar to:
+ *
+ * mvn test -DTEST_PRIVATE_TOKEN=your_private_token -DTEST_HOST_URL=https://gitlab.com \
+ * -DTEST_NAMESPACE=your_namespace
+ *
+ * NOTE: &FixMethodOrder(MethodSorters.NAME_ASCENDING) is very important to insure that the tests are in the correct order
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class TestPager {
+
+ // The following needs to be set to your test repository
+
+ private static final String TEST_NAMESPACE;
+ private static final String TEST_HOST_URL;
+ private static final String TEST_PRIVATE_TOKEN;
+ static {
+ TEST_NAMESPACE = System.getProperty("TEST_NAMESPACE");
+ TEST_HOST_URL = System.getProperty("TEST_HOST_URL");
+ TEST_PRIVATE_TOKEN = System.getProperty("TEST_PRIVATE_TOKEN");
+ }
+
+ private static GitLabApi gitLabApi;
+
+ public TestPager() {
+ super();
+ }
+
+ @BeforeClass
+ public static void setup() {
+
+ String problems = "";
+ if (TEST_NAMESPACE == null || TEST_NAMESPACE.trim().length() == 0) {
+ problems += "TEST_NAMESPACE cannot be empty\n";
+ }
+
+ if (TEST_HOST_URL == null || TEST_HOST_URL.trim().length() == 0) {
+ problems += "TEST_HOST_URL cannot be empty\n";
+ }
+
+ if (TEST_PRIVATE_TOKEN == null || TEST_PRIVATE_TOKEN.trim().length() == 0) {
+ problems += "TEST_PRIVATE_TOKEN cannot be empty\n";
+ }
+
+ if (problems.isEmpty()) {
+ gitLabApi = new GitLabApi(ApiVersion.V4, TEST_HOST_URL, TEST_PRIVATE_TOKEN);
+ } else {
+ System.err.print(problems);
+ }
+ }
+
+ @Before
+ public void beforeMethod() {
+ assumeTrue(gitLabApi != null);
+ }
+
+ @Test
+ public void testProjectPager() throws GitLabApiException {
+
+ Pager pager = gitLabApi.getProjectApi().getProjects(10);
+ assertNotNull(pager);
+ assertEquals(pager.getItemsPerPage(), 10);
+ assertTrue(0 < pager.getTotalPages());
+ assertTrue(0 < pager.getTotalItems());
+
+ int itemNumber = 0;
+ int pageIndex = 0;
+ while (pager.hasNext() && pageIndex < 4) {
+
+ List projects = pager.next();
+
+ pageIndex++;
+ assertEquals(pageIndex, pager.getCurrentPage());
+
+ if (pageIndex < pager.getTotalPages())
+ assertEquals(10, projects.size());
+
+ for (Project project : projects) {
+ itemNumber++;
+ System.out.format("page=%d, item=%d, projectId=%d, projectName=%s%n", pageIndex, itemNumber, project.getId(), project.getName());
+ }
+ }
+ }
+
+ @Test
+ public void testMemberProjectPager() throws GitLabApiException {
+
+ Pager pager = gitLabApi.getProjectApi().getMemberProjects(2);
+ assertNotNull(pager);
+ assertEquals(pager.getItemsPerPage(), 2);
+ assertTrue(0 < pager.getTotalPages());
+ assertTrue(0 < pager.getTotalItems());
+
+ int itemNumber = 0;
+ int pageIndex = 0;
+ while (pager.hasNext() && pageIndex < 10) {
+
+ List projects = pager.next();
+
+ pageIndex++;
+ assertEquals(pageIndex, pager.getCurrentPage());
+
+ if (pageIndex < pager.getTotalPages())
+ assertEquals(2, projects.size());
+
+ for (Project project : projects) {
+ itemNumber++;
+ System.out.format("page=%d, item=%d, projectId=%d, projectName=%s%n", pageIndex, itemNumber, project.getId(), project.getName());
+ }
+ }
+ }
+}