From 0a27e343186662cf8be7e5629e3d64ac9354531a Mon Sep 17 00:00:00 2001 From: Yevhenii Voevodin Date: Mon, 27 Feb 2017 11:05:51 +0200 Subject: [PATCH] Add utility class to interact with page sources (#4232) --- .../java/org/eclipse/che/api/core/Pages.java | 240 ++++++++++++++++++ .../org/eclipse/che/api/core/PagesTest.java | 148 +++++++++++ 2 files changed, 388 insertions(+) create mode 100644 core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Pages.java create mode 100644 core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PagesTest.java diff --git a/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Pages.java b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Pages.java new file mode 100644 index 00000000000..d1147958a16 --- /dev/null +++ b/core/che-core-api-core/src/main/java/org/eclipse/che/api/core/Pages.java @@ -0,0 +1,240 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.api.core; + +import org.eclipse.che.api.core.Page.PageRef; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +/** + * Static utility methods to interact with page suppliers. + * + * @author Yevhenii Voevodin + */ +public final class Pages { + + /** An experimental value used as default page size where necessary. */ + private static final int DEFAULT_PAGE_SIZE = 50; + + /** + * Defines an interface for page supplier. + * + * @param + * the type of the element held by page + * @param + * the type of exception thrown by page supplier + */ + @FunctionalInterface + public interface PageSupplier { + + /** + * Gets a single page. + * + * @param maxItems + * max items to retrieve + * @param skipCount + * how many elements to skip + * @return page + * @throws X + * exception thrown by supplier + */ + Page getPage(int maxItems, long skipCount) throws X; + } + + /** + * Eagerly fetches all the elements page by page and returns a stream of them. + * + * @param supplier + * page supplier + * @param size + * how many items to retrieve per page + * @param + * the type of the element held by page + * @param + * the type of exception thrown by page supplier + * @return stream of fetched elements + * @throws X + * when supplier throws exception + */ + public static Stream stream(PageSupplier supplier, int size) throws X { + return eagerFetch(supplier, size).stream(); + } + + /** + * Fetches elements like {@link #stream(PageSupplier, int)} method does + * using default page size which is equal to {@value #DEFAULT_PAGE_SIZE}. + */ + public static Stream stream(PageSupplier supplier) throws X { + return stream(supplier, DEFAULT_PAGE_SIZE); + } + + /** + * Eagerly fetches all the elements page by page and returns an iterable of them. + * + * @param supplier + * pages supplier + * @param size + * how many items to retrieve per page + * @param + * the type of the element held by page + * @param + * the type of exception thrown by page supplier + * @return iterable of fetched elements + * @throws X + * when supplier throws exception + */ + public static Iterable iterate(PageSupplier supplier, int size) throws X { + return eagerFetch(supplier, size); + } + + /** + * Fetches elements like {@link #iterate(PageSupplier, int)} method does + * using default page size which is equal to {@value #DEFAULT_PAGE_SIZE}. + */ + public static Iterable iterate(PageSupplier supplier) throws X { + return iterate(supplier, DEFAULT_PAGE_SIZE); + } + + /** + * Returns a stream which is based on lazy fetching paged iterator. + * + * @param supplier + * pages supplier + * @param size + * how many items to retrieve per page + * @param + * the type of the element held by page + * @param + * exception thrown by supplier + * @return stream of elements + * @throws RuntimeException + * wraps any exception occurred during pages fetch + */ + public static Stream streamLazily(PageSupplier supplier, int size) { + return StreamSupport.stream(new PagedIterable<>(supplier, size).spliterator(), false); + } + + /** + * Fetches elements like {@link #streamLazily(PageSupplier, int)} method does + * using default page size which is equal to {@value #DEFAULT_PAGE_SIZE}. + */ + public static Stream streamLazily(PageSupplier supplier) { + return streamLazily(supplier, DEFAULT_PAGE_SIZE); + } + + /** + * Returns an iterable which iterator lazily fetches page by page, + * doesn't poll the next page until the last item from previous page is not processed. + * The first page is polled while iterable is created. + * + * @param supplier + * pages supplier + * @param size + * how many items to retrieve per page + * @param + * the type of the element held by page + * @param + * the type of exception thrown by page supplier + * @return stream of elements + * @throws RuntimeException + * wraps any exception occurred during pages fetch + */ + public static Iterable iterateLazily(PageSupplier supplier, int size) { + return new PagedIterable<>(supplier, size); + } + + /** + * Returns an iterable like {@link #streamLazily(PageSupplier, int)} method does + * using default page size which is equal to {@value #DEFAULT_PAGE_SIZE}. + */ + public static Iterable iterateLazily(PageSupplier supplier) { + return iterateLazily(supplier, DEFAULT_PAGE_SIZE); + } + + private static List eagerFetch(PageSupplier supplier, int size) throws X { + Page page = supplier.getPage(size, 0); + ArrayList container = new ArrayList<>(page.hasNextPage() ? page.getItemsCount() * 2 : page.getItemsCount()); + while (page.hasNextPage()) { + container.addAll(page.getItems()); + PageRef next = page.getNextPageRef(); + page = supplier.getPage(next.getPageSize(), next.getItemsBefore()); + } + container.addAll(page.getItems()); + return container; + } + + private static class PagedIterable implements Iterable { + + private final PageSupplier supplier; + private final int size; + + private PagedIterable(PageSupplier supplier, int size) { + this.supplier = supplier; + this.size = size; + } + + @Override + public Iterator iterator() { + return new PagedIterator<>(supplier, size); + } + } + + private static class PagedIterator implements Iterator { + + private final PageSupplier supplier; + private final int size; + + private Page page; + private Iterator delegate; + + private PagedIterator(PageSupplier supplier, int size) { + this.supplier = supplier; + this.size = size; + fetchPage(0); + } + + @Override + public boolean hasNext() { + if (delegate.hasNext()) { + return true; + } + if (!page.hasNextPage()) { + return false; + } + fetchPage(page.getNextPageRef().getItemsBefore()); + return delegate.hasNext(); + } + + @Override + public E next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + return delegate.next(); + } + + private void fetchPage(long skip) { + try { + page = supplier.getPage(size, skip); + delegate = page.getItems().iterator(); + } catch (Exception x) { + throw new RuntimeException(x.getMessage(), x); + } + } + } + + private Pages() {} +} diff --git a/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PagesTest.java b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PagesTest.java new file mode 100644 index 00000000000..55888299276 --- /dev/null +++ b/core/che-core-api-core/src/test/java/org/eclipse/che/api/core/PagesTest.java @@ -0,0 +1,148 @@ +/******************************************************************************* + * Copyright (c) 2012-2017 Codenvy, S.A. + * All rights reserved. This program and the accompanying materials + * are made available under the terms of the Eclipse Public License v1.0 + * which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v10.html + * + * Contributors: + * Codenvy, S.A. - initial API and implementation + *******************************************************************************/ +package org.eclipse.che.api.core; + +import com.google.common.collect.Lists; + +import org.testng.annotations.BeforeSuite; +import org.testng.annotations.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static org.mockito.Matchers.anyInt; +import static org.mockito.Matchers.anyLong; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertTrue; + +/** + * Tests {@link Pages}. + * + * @author Yevhenii Voevodin + */ +public class PagesTest { + + private TestPagesSupplier testSource; + + @BeforeSuite + private void setUp() { + String[] strings = new String[10]; + for (int i = 0; i < strings.length; i++) { + strings[i] = "test-string-" + i; + } + testSource = new TestPagesSupplier(strings); + } + + @Test + public void eagerlyStreamsAllElements() { + List result = Pages.stream(testSource::getStrings, 2).collect(Collectors.toList()); + + assertEquals(result, testSource.strings); + } + + @Test + public void eagerlyIteratesAllElements() { + ArrayList result = Lists.newArrayList(Pages.iterate(testSource::getStrings, 2)); + + assertEquals(result, testSource.strings); + } + + @Test + public void lazyStreamsAllElements() { + List result = Pages.streamLazily(testSource::getStrings, 2).collect(Collectors.toList()); + + assertEquals(result, testSource.strings); + } + + @Test + public void lazyIteratesAllElements() { + ArrayList result = Lists.newArrayList(Pages.iterateLazily(testSource::getStrings, 2)); + + assertEquals(result, testSource.strings); + } + + @Test + public void lazyStreamingDoesNotPollNextPageUntilNeeded() { + TestPagesSupplier src = spy(new TestPagesSupplier("string1", "string2", "string3")); + + assertTrue(Pages.streamLazily(src::getStrings, 1).anyMatch(s -> s.equals("string2"))); + + verify(src, times(2)).getStrings(anyInt(), anyLong()); + verify(src).getStrings(1, 0); + verify(src).getStrings(1, 1); + } + + @Test + public void lazyIteratingDoesNotPollNextPageUntilNeeded() { + TestPagesSupplier src = spy(new TestPagesSupplier("string1", "string2", "string3")); + + Iterator it = Pages.iterateLazily(src::getStrings, 1).iterator(); + it.next(); + it.next(); + + verify(src, times(2)).getStrings(anyInt(), anyLong()); + verify(src).getStrings(1, 0); + verify(src).getStrings(1, 1); + } + + @Test + public void returnsEmptyStreamWhenFetchingEagerly() { + Stream stream = Pages.stream(new TestPagesSupplier()::getStrings); + + assertFalse(stream.findAny().isPresent()); + } + + @Test + public void returnsIterableWithNoElementsWhileFetchingEagerly() { + Iterator it = Pages.iterate(new TestPagesSupplier()::getStrings).iterator(); + + assertFalse(it.hasNext()); + } + + @Test + public void returnsEmptyStreamWhenFetchingLazily() { + Stream stream = Pages.streamLazily(new TestPagesSupplier()::getStrings); + + assertFalse(stream.findAny().isPresent()); + } + + @Test + public void returnsIterableWithNoeElementsWhileFetchingLazily() { + Iterator it = Pages.iterateLazily(new TestPagesSupplier()::getStrings).iterator(); + + assertFalse(it.hasNext()); + } + + private static class TestPagesSupplier { + + private final List strings; + + private TestPagesSupplier(String... strings) { + this.strings = Arrays.asList(strings); + } + + public Page getStrings(int max, long skip) { + List items = strings.stream() + .skip(skip) + .limit(max) + .collect(Collectors.toList()); + return new Page<>(items, skip, max, strings.size()); + } + } +}