diff --git a/sdk/clientcore/core/CHANGELOG.md b/sdk/clientcore/core/CHANGELOG.md index c26a04404640c..11aa57a5656af 100644 --- a/sdk/clientcore/core/CHANGELOG.md +++ b/sdk/clientcore/core/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added `PagedResponse`, `PagedOptions`, and `PagedIterable`, for supporting pagination. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java new file mode 100644 index 0000000000000..8e0b8d29f4eb6 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedIterable.java @@ -0,0 +1,262 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +import io.clientcore.core.util.ClientLogger; + +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * This class provides utility to iterate over {@link PagedResponse} using {@link Stream} and {@link Iterable} + * interfaces. + * + * @param The type of items in the page. + */ +public final class PagedIterable implements Iterable { + + private final Function> pageRetriever; + + /** + * Creates an instance of {@link PagedIterable} that consists of only a single page. This constructor takes a {@code + * Supplier} that return the single page of {@code T}. + * + * @param firstPageRetriever Function that retrieves the first page, given paging options. + */ + public PagedIterable(Function> firstPageRetriever) { + this(firstPageRetriever, ((pagingOptions, nextLink) -> null)); + } + + /** + * Creates an instance of {@link PagedIterable}. The constructor takes a {@code Supplier} and {@code Function}. The + * {@code Supplier} returns the first page of {@code T}, the {@code Function} retrieves subsequent pages of {@code + * T}. + * + * @param firstPageRetriever Function that retrieves the first page, given paging options. + * @param nextPageRetriever Function that retrieves the next page, given paging options and next link. + */ + public PagedIterable(Function> firstPageRetriever, + BiFunction> nextPageRetriever) { + this.pageRetriever = context -> (context.getNextLink() == null) + ? firstPageRetriever.apply(context.getPagingOptions()) + : nextPageRetriever.apply(context.getPagingOptions(), context.getNextLink()); + } + + /** + * {@inheritDoc} + */ + @Override + public Iterator iterator() { + return iterableByItemInternal(null).iterator(); + } + + /** + * Retrieve the {@link Iterable}, one page at a time. It will provide same {@link Iterable} of T values from + * starting if called multiple times. + * + * @return {@link Iterable} of a pages + */ + public Iterable> iterableByPage() { + return iterableByPageInternal(null); + } + + /** + * Retrieve the {@link Iterable}, one page at a time. It will provide same {@link Iterable} of pages from + * starting if called multiple times. + * + * @param pagingOptions the paging options + * @return {@link Iterable} of a pages + */ + public Iterable> iterableByPage(PagingOptions pagingOptions) { + return iterableByPageInternal(pagingOptions); + } + + /** + * Retrieve the {@link Stream} of value {@code T}. It will provide same {@link Stream} of T values from + * starting if called multiple times. + * + * @return {@link Stream} of value {@code T} + */ + public Stream stream() { + return StreamSupport.stream(iterableByItemInternal(null).spliterator(), false); + } + + /** + * Retrieve the {@link Stream}, one page at a time. It will provide same {@link Stream} of pages from starting if + * called multiple times. + * + * @return {@link Stream} of a pages + */ + public Stream> streamByPage() { + return StreamSupport.stream(iterableByPage().spliterator(), false); + } + + /** + * Retrieve the {@link Stream}, one page at a time. It will provide same {@link Stream} of T values from starting if + * called multiple times. + * + * @param pagingOptions the paging options + * @return {@link Stream} of a pages + */ + public Stream> streamByPage(PagingOptions pagingOptions) { + return StreamSupport.stream(iterableByPage(pagingOptions).spliterator(), false); + } + + private static final class PagingContext { + private final PagingOptions pagingOptions; + private final String nextLink; + + private PagingContext(PagingOptions pagingOptions, String nextLink) { + this.pagingOptions = pagingOptions; + this.nextLink = nextLink; + } + + private PagingOptions getPagingOptions() { + return pagingOptions; + } + + private String getNextLink() { + return nextLink; + } + } + + private Iterable iterableByItemInternal(PagingOptions pagingOptions) { + return () -> new PagedIterator<>(pageRetriever, pagingOptions) { + + private Iterator nextPage; + private Iterator currentPage; + + @Override + boolean needToRequestPage() { + return (currentPage == null || !currentPage.hasNext()) && nextPage == null; + } + + @Override + boolean isNextAvailable() { + return (currentPage != null && currentPage.hasNext()) || nextPage != null; + } + + @Override + T getNext() { + if ((currentPage == null || !currentPage.hasNext()) && nextPage != null) { + currentPage = nextPage; + nextPage = null; + } + + return currentPage.next(); + } + + @Override + void addPage(PagedResponse page) { + Iterator pageValues = page.getValue().iterator(); + if (pageValues.hasNext()) { + nextPage = pageValues; + } + } + }; + } + + private Iterable> iterableByPageInternal(PagingOptions pagingOptions) { + return () -> new PagedIterator>(pageRetriever, pagingOptions) { + + private PagedResponse nextPage; + + @Override + boolean needToRequestPage() { + return nextPage == null; + } + + @Override + boolean isNextAvailable() { + return nextPage != null; + } + + @Override + PagedResponse getNext() { + PagedResponse currentPage = nextPage; + nextPage = null; + return currentPage; + } + + @Override + void addPage(PagedResponse page) { + nextPage = page; + } + }; + } + + private abstract static class PagedIterator implements Iterator { + private static final ClientLogger LOGGER = new ClientLogger(PagedIterator.class); + + private final Function> pageRetriever; + private final Long pageSize; + private String continuationToken; + private String nextLink; + private boolean done; + + PagedIterator(Function> pageRetriever, PagingOptions pagingOptions) { + this.pageRetriever = pageRetriever; + this.pageSize = pagingOptions == null ? null : pagingOptions.getPageSize(); + this.continuationToken = pagingOptions == null ? null : pagingOptions.getContinuationToken(); + } + + @Override + public E next() { + if (!hasNext()) { + throw LOGGER.logThrowableAsError(new NoSuchElementException("Iterator contains no more elements.")); + } + + return getNext(); + } + + @Override + public boolean hasNext() { + // Request next pages in a loop in case we are returned empty pages for the by item implementation. + while (!done && needToRequestPage()) { + requestPage(); + } + + return isNextAvailable(); + } + + abstract boolean needToRequestPage(); + + abstract boolean isNextAvailable(); + + abstract E getNext(); + + void requestPage() { + boolean receivedPages = false; + PagingOptions pagingOptions = new PagingOptions(); + pagingOptions.setPageSize(pageSize); + pagingOptions.setContinuationToken(continuationToken); + PagedResponse page = pageRetriever.apply(new PagingContext(pagingOptions, nextLink)); + if (page != null) { + receivePage(page); + receivedPages = true; + } + + /* + * In the scenario when the subscription completes without emitting an element indicate we are done by checking + * if we have any additional elements to return. + */ + this.done = done || (!receivedPages && !isNextAvailable()); + } + + abstract void addPage(PagedResponse page); + + private void receivePage(PagedResponse page) { + addPage(page); + + nextLink = page.getNextLink(); + continuationToken = page.getContinuationToken(); + this.done = (nextLink == null || nextLink.isEmpty()) + && (continuationToken == null || continuationToken.isEmpty()); + } + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedResponse.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedResponse.java new file mode 100644 index 0000000000000..80488a0c34fa5 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagedResponse.java @@ -0,0 +1,168 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +import io.clientcore.core.util.binarydata.BinaryData; + +import java.io.IOException; +import java.util.List; + +/** + * Response of a REST API that returns page. + * + * @see Response + * + * @param The type of items in the page. + */ +public final class PagedResponse implements Response> { + + private final HttpRequest request; + private final int statusCode; + private final HttpHeaders headers; + private final BinaryData body; + + private final List items; + private final String continuationToken; + private final String nextLink; + private final String previousLink; + private final String firstLink; + private final String lastLink; + + /** + * Creates a new instance of the PagedResponse type. + * + * @param request The HttpRequest that was sent to the service whose response resulted in this response. + * @param statusCode The status code from the response. + * @param headers The headers from the response. + * @param body The body from the response. + * @param items The items returned from the service within the response. + */ + public PagedResponse(HttpRequest request, int statusCode, HttpHeaders headers, BinaryData body, List items) { + this(request, statusCode, headers, body, items, null, null, null, null, null); + } + + /** + * Creates a new instance of the PagedResponse type. + * + * @param request The HttpRequest that was sent to the service whose response resulted in this response. + * @param statusCode The status code from the response. + * @param headers The headers from the response. + * @param body The body from the response. + * @param items The items returned from the service within the response. + * @param continuationToken The continuation token returned from the service, to enable future requests to pick up + * from the same place in the paged iteration. + * @param nextLink The next page link returned from the service. + * @param previousLink The previous page link returned from the service. + * @param firstLink The first page link returned from the service. + * @param lastLink The last page link returned from the service. + */ + public PagedResponse(HttpRequest request, int statusCode, HttpHeaders headers, BinaryData body, List items, + String continuationToken, String nextLink, String previousLink, String firstLink, String lastLink) { + this.request = request; + this.statusCode = statusCode; + this.headers = headers; + this.body = body; + this.items = items; + this.continuationToken = continuationToken; + this.nextLink = nextLink; + this.previousLink = previousLink; + this.firstLink = firstLink; + this.lastLink = lastLink; + } + + /** + * Gets the continuation token. + * + * @return The continuation token, or null if there isn't a next page. + */ + public String getContinuationToken() { + return continuationToken; + } + + /** + * Gets the link to the next page. + * + * @return The next page link, or null if there isn't a next page. + */ + public String getNextLink() { + return nextLink; + } + + /** + * Gets the link to the previous page. + * + * @return The previous page link, or null if there isn't a previous page. + */ + public String getPreviousLink() { + return previousLink; + } + + /** + * Gets the link to the first page. + * + * @return The first page link + */ + public String getFirstLink() { + return firstLink; + } + + /** + * Gets the link to the last page. + * + * @return The last page link + */ + public String getLastLink() { + return lastLink; + } + + /** + * {@inheritDoc} + */ + @Override + public int getStatusCode() { + return statusCode; + } + + /** + * {@inheritDoc} + */ + @Override + public HttpHeaders getHeaders() { + return headers; + } + + /** + * {@inheritDoc} + */ + @Override + public HttpRequest getRequest() { + return request; + } + + /** + * {@inheritDoc} + */ + @Override + public List getValue() { + return items; + } + + /** + * {@inheritDoc} + */ + @Override + public BinaryData getBody() { + return body; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + if (body != null) { + body.close(); + } + } +} diff --git a/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagingOptions.java b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagingOptions.java new file mode 100644 index 0000000000000..41e64c3099024 --- /dev/null +++ b/sdk/clientcore/core/src/main/java/io/clientcore/core/http/models/PagingOptions.java @@ -0,0 +1,102 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +/** + * The paging options for the pageable operation. + */ +public final class PagingOptions { + + private Long offset; + private Long pageSize; + private Long pageIndex; + + private String continuationToken; + + /** + * Creates an instance of {@link PagingOptions}. + */ + public PagingOptions() { + } + + /** + * Gets the offset on items. + * + * @return the offset on items + */ + public Long getOffset() { + return offset; + } + + /** + * Sets the offset on items. + * + * @param offset the offset on items + * @return the PagingOptions object itself + */ + public PagingOptions setOffset(Long offset) { + this.offset = offset; + return this; + } + + /** + * Gets the size of a page. + * + * @return the size of a page + */ + public Long getPageSize() { + return pageSize; + } + + /** + * Sets the size of a page. + * + * @param pageSize the size of a page + * @return the PagingOptions object itself + */ + public PagingOptions setPageSize(Long pageSize) { + this.pageSize = pageSize; + return this; + } + + /** + * Gets the page index. + * + * @return the page index + */ + public Long getPageIndex() { + return pageIndex; + } + + /** + * Sets the page index. + * + * @param pageIndex the page index + * @return the PagingOptions object itself + */ + public PagingOptions setPageIndex(Long pageIndex) { + this.pageIndex = pageIndex; + return this; + } + + /** + * Gets the continuation token. + * + * @return the continuation token + */ + public String getContinuationToken() { + return continuationToken; + } + + /** + * Sets the continuation token. + * + * @param continuationToken the continuation token + * @return the PagingOptions object itself + */ + public PagingOptions setContinuationToken(String continuationToken) { + this.continuationToken = continuationToken; + return this; + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedIterableTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedIterableTests.java new file mode 100644 index 0000000000000..6b801a1e6c920 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedIterableTests.java @@ -0,0 +1,330 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +import io.clientcore.core.util.binarydata.BinaryData; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; +import org.opentest4j.AssertionFailedError; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PagedIterableTests { + + private final HttpHeaders httpHeaders = new HttpHeaders(); + private final HttpRequest httpRequest = new HttpRequest(HttpMethod.GET, "http://localhost"); + private final BinaryData responseBody = BinaryData.empty(); + + // tests with mocked PagedResponse + private List> pagedResponses; + + @ParameterizedTest + @ValueSource(ints = { 0, 5 }) + public void streamByPage(int numberOfPages) { + PagedIterable pagedIterable = getIntegerPagedIterable(numberOfPages); + List> pages = pagedIterable.streamByPage().collect(Collectors.toList()); + + assertEquals(numberOfPages, pages.size()); + assertEquals(pagedResponses, pages); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 5 }) + public void iterateByPage(int numberOfPages) { + PagedIterable pagedIterable = getIntegerPagedIterable(numberOfPages); + List> pages = new ArrayList<>(); + pagedIterable.iterableByPage().iterator().forEachRemaining(pages::add); + + assertEquals(numberOfPages, pages.size()); + assertEquals(pagedResponses, pages); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 5 }) + public void streamByT(int numberOfPages) { + PagedIterable pagedIterable = getIntegerPagedIterable(numberOfPages); + List values = pagedIterable.stream().collect(Collectors.toList()); + + assertEquals(numberOfPages * 3, values.size()); + assertEquals(Stream.iterate(0, i -> i + 1).limit(numberOfPages * 3L).collect(Collectors.toList()), values); + } + + @ParameterizedTest + @ValueSource(ints = { 0, 5 }) + public void iterateByT(int numberOfPages) { + PagedIterable pagedIterable = getIntegerPagedIterable(numberOfPages); + List values = new ArrayList<>(); + pagedIterable.iterator().forEachRemaining(values::add); + + assertEquals(numberOfPages * 3, values.size()); + assertEquals(Stream.iterate(0, i -> i + 1).limit(numberOfPages * 3L).collect(Collectors.toList()), values); + } + + @Test + public void iterateResponseContainsEmptyArray() { + pagedResponses = new ArrayList<>(3); + // second page is empty but has nextLink + pagedResponses.add(new PagedResponse<>(httpRequest, 200, httpHeaders, responseBody, List.of(0, 1, 2), null, "1", + null, null, null)); + pagedResponses.add(new PagedResponse<>(httpRequest, 200, httpHeaders, responseBody, Collections.emptyList(), + null, "2", null, null, null)); + pagedResponses.add(new PagedResponse<>(httpRequest, 200, httpHeaders, responseBody, List.of(3, 4), null, null, + null, null, null)); + + PagedIterable pagedIterable + = new PagedIterable<>(pagingOptions -> pagedResponses.isEmpty() ? null : pagedResponses.get(0), + (pagingOptions, nextLink) -> getNextPageSync(nextLink, pagedResponses)); + + verifyIteratorSize(pagedIterable.iterableByPage().iterator(), 3); + verifyIteratorSize(pagedIterable.iterator(), 5); + } + + private PagedIterable getIntegerPagedIterable(int numberOfPages) { + createPagedResponse(numberOfPages); + + return new PagedIterable<>(pagingOptions -> pagedResponses.isEmpty() ? null : pagedResponses.get(0), + (pagingOptions, nextLink) -> getNextPageSync(nextLink, pagedResponses)); + } + + private void createPagedResponse(int numberOfPages) { + pagedResponses = IntStream.range(0, numberOfPages) + .boxed() + .map(i -> createPagedResponse(httpRequest, httpHeaders, numberOfPages, this::getItems, i)) + .collect(Collectors.toList()); + } + + private PagedResponse createPagedResponse(HttpRequest httpRequest, HttpHeaders headers, int numberOfPages, + Function> valueSupplier, int i) { + return new PagedResponse<>(httpRequest, 200, headers, responseBody, valueSupplier.apply(i), null, + (i < numberOfPages - 1) ? String.valueOf(i + 1) : null, null, null, null); + } + + private PagedResponse getNextPageSync(String nextLink, List> pagedResponses) { + + if (nextLink == null || nextLink.isEmpty()) { + return null; + } + + int parsedToken = Integer.parseInt(nextLink); + if (parsedToken >= pagedResponses.size()) { + return null; + } + + return pagedResponses.get(parsedToken); + } + + private List getItems(int i) { + return IntStream.range(i * 3, i * 3 + 3).boxed().collect(Collectors.toList()); + } + + // tests with mocked HttpResponse + @ParameterizedTest + @ValueSource(ints = { 0, 10000, 100000 }) + public void streamParallelDoesNotRetrieveMorePagesThanExpected(int numberOfPages) { + /* + * The test doesn't make any service calls so use a high page count to give the test more opportunities for + * failure. + */ + + // there is still 1 request (1 page with empty array), when no items + int expectedNumberOfRetrievals = numberOfPages == 0 ? 1 : numberOfPages; + + nextPageMode = NextPageMode.CONTINUATION_TOKEN; + pagingStatistics.resetAll(); + pagingStatistics.totalPages = numberOfPages; + + PagedIterable pagedIterable = list(); + + long count = pagedIterable.stream().parallel().count(); + assertEquals((long) numberOfPages * pagingStatistics.pageSize, count); + assertEquals(expectedNumberOfRetrievals, pagingStatistics.numberOfPageRetrievals); + } + + @ParameterizedTest + @EnumSource(NextPageMode.class) + public void testPagedIterable(NextPageMode nextPageMode) { + this.nextPageMode = nextPageMode; + pagingStatistics.resetAll(); + + PagedIterable pagedIterable = this.list(); + + verifyIteratorSize(pagedIterable.iterableByPage().iterator(), pagingStatistics.totalPages); + verifyIteratorSize(pagedIterable.iterator(), (long) pagingStatistics.totalPages * pagingStatistics.pageSize); + + // case when pagingOptions == null + verifyIteratorSize(pagedIterable.iterableByPage(null).iterator(), pagingStatistics.totalPages); + verifyIteratorSize(pagedIterable.streamByPage(null).iterator(), pagingStatistics.totalPages); + } + + @Test + public void testPagedIterableContinuationToken() { + nextPageMode = NextPageMode.CONTINUATION_TOKEN; + pagingStatistics.resetAll(); + pagingStatistics.totalPages = 5; + + int startPage = 2; + + PagedIterable pagedIterable = this.list(); + + // continuationToken provided, start from startPage + PagingOptions pagingOptions = new PagingOptions().setContinuationToken(String.valueOf(startPage)); + + verifyIteratorSize(pagedIterable.iterableByPage(pagingOptions).iterator(), + pagingStatistics.totalPages - startPage); + verifyIteratorSize(pagedIterable.streamByPage(pagingOptions).iterator(), + pagingStatistics.totalPages - startPage); + } + + private static void verifyIteratorSize(Iterator iterator, long size) { + long iteratorSize = 0; + while (iterator.hasNext()) { + ++iteratorSize; + iterator.next(); + } + Assertions.assertEquals(size, iteratorSize); + } + + private NextPageMode nextPageMode; + private final PagingStatistics pagingStatistics = new PagingStatistics(); + + private static final class PagingStatistics { + private int totalPages = 3; + private int pageSize = 5; + + private int numberOfPageRetrievals; + + private void resetAll() { + resetStatistics(); + totalPages = 3; + pageSize = 5; + } + + private void resetStatistics() { + numberOfPageRetrievals = 0; + } + } + + // mock class and API for pageable operation + public enum NextPageMode { + CONTINUATION_TOKEN, NEXT_LINK + } + + private static final class TodoItem { + private final int id; + + private TodoItem(int id) { + this.id = id; + } + + public int getId() { + return id; + } + } + + private static final class TodoPage { + private final List items; + private final String continuationToken; + private final String nextLink; + + private TodoPage(List items, String continuationToken, String nextLink) { + this.items = items; + this.continuationToken = continuationToken; + this.nextLink = nextLink; + } + + public List getItems() { + return items; + } + + public String getContinuationToken() { + return continuationToken; + } + + public String getNextLink() { + return nextLink; + } + } + + private PagedIterable list() { + pagingStatistics.resetStatistics(); + return new PagedIterable<>((pagingOptions) -> listSinglePage(pagingOptions), + (pagingOptions, nextLink) -> listNextSinglePage(pagingOptions, nextLink)); + } + + private PagedResponse listSinglePage(PagingOptions pagingOptions) { + Response res = listSync(pagingOptions); + return new PagedResponse<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), res.getBody(), + res.getValue().getItems(), res.getValue().getContinuationToken(), res.getValue().getNextLink(), null, null, + null); + } + + private PagedResponse listNextSinglePage(PagingOptions pagingOptions, String nextLink) { + Response res = (nextLink == null) ? listSync(pagingOptions) : listNextSync(nextLink); + return new PagedResponse<>(res.getRequest(), res.getStatusCode(), res.getHeaders(), res.getBody(), + res.getValue().getItems(), res.getValue().getContinuationToken(), res.getValue().getNextLink(), null, null, + null); + } + + private Response listSync(PagingOptions pagingOptions) { + ++pagingStatistics.numberOfPageRetrievals; + // mock request on first page + if (pagingStatistics.totalPages == 0) { + return new HttpResponse<>(httpRequest, 200, httpHeaders, new TodoPage(Collections.emptyList(), null, null)); + } else { + switch (nextPageMode) { + case NEXT_LINK: { + // first page + return new HttpResponse<>(httpRequest, 200, httpHeaders, + new TodoPage(createTodoItemList(0), null, "1")); + } + + case CONTINUATION_TOKEN: { + if (pagingOptions.getContinuationToken() == null) { + // first page + return new HttpResponse<>(httpRequest, 200, httpHeaders, + new TodoPage(createTodoItemList(0), "1", null)); + } else { + int pageIndex = Integer.parseInt(pagingOptions.getContinuationToken()); + int nextPageIndex = pageIndex + 1; + String newContinuationToken + = nextPageIndex >= pagingStatistics.totalPages ? null : String.valueOf(nextPageIndex); + return new HttpResponse<>(httpRequest, 200, httpHeaders, + new TodoPage(createTodoItemList(pageIndex), newContinuationToken, null)); + } + } + + default: + throw new AssertionFailedError(); + } + } + } + + private Response listNextSync(String nextLink) { + ++pagingStatistics.numberOfPageRetrievals; + // mock request on next page + int pageIndex = Integer.parseInt(nextLink); + int nextPageIndex = pageIndex + 1; + String newNextLink = nextPageIndex >= pagingStatistics.totalPages ? null : String.valueOf(nextPageIndex); + return new HttpResponse<>(httpRequest, 200, httpHeaders, + new TodoPage(createTodoItemList(pageIndex), null, newNextLink)); + } + + private List createTodoItemList(int pageIndex) { + return IntStream.range(pageIndex * pagingStatistics.pageSize, (pageIndex + 1) * pagingStatistics.pageSize) + .mapToObj(TodoItem::new) + .collect(Collectors.toList()); + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedResponseTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedResponseTests.java new file mode 100644 index 0000000000000..0834d6ffd4abc --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagedResponseTests.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +import io.clientcore.core.util.binarydata.BinaryData; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.util.List; + +public class PagedResponseTests { + + @Test + public void testPagedResponseRequired() { + final HttpRequest mockHttpRequest = new HttpRequest(HttpMethod.GET, "https://endpoint"); + final int statusCode = 200; + final HttpHeaders mockHttpHeaders = new HttpHeaders(); + final List mockValue = List.of(new Object()); + + final String continuationToken = "continuation_token"; + final String nextLink = "https://next_link"; + final String previousLink = "https://previous_link"; + final String firstLink = "https://first_link"; + final String lastLink = "https://last_link"; + + try (BinaryData mockData = BinaryData.fromString("{\"value\":[{}]}")) { + try (PagedResponse pagedResponse + = new PagedResponse<>(mockHttpRequest, statusCode, mockHttpHeaders, mockData, mockValue)) { + Assertions.assertEquals(mockHttpRequest, pagedResponse.getRequest()); + Assertions.assertEquals(statusCode, pagedResponse.getStatusCode()); + Assertions.assertEquals(mockHttpHeaders, pagedResponse.getHeaders()); + Assertions.assertEquals(mockValue, pagedResponse.getValue()); + } + + try (PagedResponse pagedResponse = new PagedResponse<>(mockHttpRequest, statusCode, mockHttpHeaders, + mockData, mockValue, continuationToken, nextLink, previousLink, firstLink, lastLink)) { + Assertions.assertEquals(mockHttpRequest, pagedResponse.getRequest()); + Assertions.assertEquals(statusCode, pagedResponse.getStatusCode()); + Assertions.assertEquals(mockHttpHeaders, pagedResponse.getHeaders()); + Assertions.assertEquals(mockValue, pagedResponse.getValue()); + + Assertions.assertEquals(continuationToken, pagedResponse.getContinuationToken()); + Assertions.assertEquals(nextLink, pagedResponse.getNextLink()); + Assertions.assertEquals(previousLink, pagedResponse.getPreviousLink()); + Assertions.assertEquals(firstLink, pagedResponse.getFirstLink()); + Assertions.assertEquals(lastLink, pagedResponse.getLastLink()); + } + } catch (IOException e) { + Assertions.fail(); + } + } +} diff --git a/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagingOptionsTests.java b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagingOptionsTests.java new file mode 100644 index 0000000000000..dfece73cb6f37 --- /dev/null +++ b/sdk/clientcore/core/src/test/java/io/clientcore/core/http/models/PagingOptionsTests.java @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package io.clientcore.core.http.models; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class PagingOptionsTests { + + @Test + public void testPagingOptions() { + final long pageSize = 100L; + final long pageIndex = 1L; + final long offset = 50L; + final String continuationToken = "continuation_token"; + + PagingOptions pagingOptions = new PagingOptions().setPageSize(pageSize) + .setPageIndex(pageIndex) + .setOffset(offset) + .setContinuationToken(continuationToken); + + Assertions.assertEquals(pageSize, pagingOptions.getPageSize()); + Assertions.assertEquals(pageIndex, pagingOptions.getPageIndex()); + Assertions.assertEquals(offset, pagingOptions.getOffset()); + Assertions.assertEquals(continuationToken, pagingOptions.getContinuationToken()); + } +}