diff --git a/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ChunkGetter.java b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ChunkGetter.java new file mode 100644 index 0000000..a539002 --- /dev/null +++ b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ChunkGetter.java @@ -0,0 +1,26 @@ +/* + * + * Copyright (C) 2023 Joerg Bayer (SG-O) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.sg_o.lib.tagy.util; + +import java.util.List; + +public interface ChunkGetter { + List getChunk(int length, int offset); + + int getTotal(); +} diff --git a/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ListUpdateCompleteListener.java b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ListUpdateCompleteListener.java new file mode 100644 index 0000000..7487df2 --- /dev/null +++ b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/ListUpdateCompleteListener.java @@ -0,0 +1,30 @@ +/* + * + * Copyright (C) 2023 Joerg Bayer (SG-O) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.sg_o.lib.tagy.util; + +public interface ListUpdateCompleteListener { + + /** + * Called when the list has been updated. + * + * @param pagedList the updated list + * @param minIndex the index of the first loaded element in the updated list (inclusive) + * @param maxIndex the index of the last loaded element in the updated list (exclusive) + */ + void onListUpdateComplete(PagedList pagedList, int minIndex, int maxIndex); +} diff --git a/tagyCore/src/main/java/de/sg_o/lib/tagy/util/PagedList.java b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/PagedList.java new file mode 100644 index 0000000..d6843cb --- /dev/null +++ b/tagyCore/src/main/java/de/sg_o/lib/tagy/util/PagedList.java @@ -0,0 +1,538 @@ +/* + * + * Copyright (C) 2023 Joerg Bayer (SG-O) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.sg_o.lib.tagy.util; + +import org.jetbrains.annotations.NotNull; + +import java.lang.reflect.Array; +import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.FutureTask; + +/** + * An immutable list that loads chunks of data in the background and + * provides a list of the data. + * + * @param the type of element in the list + */ +public class PagedList implements List { + private static final Boolean[] updating = new Boolean[]{false, false, false, false, false}; + private final ExecutorService executor = Executors.newFixedThreadPool(5); + + private List page0 = new ArrayList<>(); //centerPage - 2 + private List page1 = new ArrayList<>(); //centerPage - 1 + private List page2 = new ArrayList<>(); //centerPage + private List page3 = new ArrayList<>(); //centerPage + 1 + private List page4 = new ArrayList<>(); //centerPage + 2 + + private final int thirdPage; + private final boolean allowAutomaticLoading; + + private int centerPage = -3; + + private final ChunkGetter chunkGetter; + + private final List> updateCompleteListeners = new ArrayList<>(); + + + /** + * Creates a new PagedList. + * + * @param chunkGetter The chunk loading implementation + * @param pageLength The length of the pages required + */ + public PagedList(@NotNull ChunkGetter chunkGetter, int pageLength) { + this(chunkGetter, pageLength, true); + } + + /** + * Creates a new PagedList. + * + * @param chunkGetter The chunk loading implementation + * @param pageLength The length of the pages required + * @param allowAutomaticLoading If true, the list will automatically load + * the required page when accessing a page that + * is not loaded yet. If false, the list will + * throw an exception if accessing a page that + * is not loaded yet. + * Note that this setting only affects the + * behavior of the {@link #get(int)} + * @see #preparePages(int) + * @see #get(int) + */ + public PagedList(@NotNull ChunkGetter chunkGetter, int pageLength, boolean allowAutomaticLoading) { + if (pageLength < 1) pageLength = 1; + this.chunkGetter = chunkGetter; + this.thirdPage = (pageLength + 2) / 3; + this.allowAutomaticLoading = allowAutomaticLoading; + } + + private int calculateCenterPage(int index, boolean fromEnd) { + int center = index / thirdPage; + if (fromEnd) { + center--; + } else { + center++; + } + return center; + } + + private int calculatePage(int index) { + return index / thirdPage; + } + + private int calculateIndexInPage(int index) { + return index % thirdPage; + } + + private List getListForPage(int page) { + awaitUpdate(page + 2); + if (page == -2) { + synchronized (updating[0]) { + return page0; + } + } + if (page == -1) { + synchronized (updating[1]) { + return page1; + } + } + if (page == 0) { + synchronized (updating[2]) { + return page2; + } + } + if (page == 1) { + synchronized (updating[3]) { + return page3; + } + } + if (page == 2) { + synchronized (updating[4]) { + return page4; + } + } + throw new IndexOutOfBoundsException("Not loaded, yet"); + } + + private void awaitAllUpdates() { + for (int i = 0; i < updating.length; i++) { + awaitUpdate(i); + } + } + + private void awaitUpdate(int page) { + if (page < 0 || page > 4) return; + while (updating[page]) { + try { + //noinspection BusyWait + Thread.sleep(10); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + + private void notifyUpdateComplete() { + if (isUpdating()) return; + for (ListUpdateCompleteListener listener : updateCompleteListeners) { + listener.onListUpdateComplete(this, firstAvailableIndex(), lastAvailableIndex()); + } + } + + public void preparePages(int index) { + this.preparePages(index, false); + } + + /** + * Prepares the list by loading the pages around the given index. + * + * @param index The index to center the list around. + * @param fromEnd If true, the index is considered to be the last element in the used portion of the list. + */ + public void preparePages(int index, boolean fromEnd) { + int newCenterPage = calculateCenterPage(index, fromEnd); + int distanceFromOld = newCenterPage - this.centerPage; + if (distanceFromOld == 0) return; + if (distanceFromOld > 0 && distanceFromOld < 5) { + for (int i = 0; i < distanceFromOld; i++) { + shiftUp(); + } + reloadFromEnd(distanceFromOld); + } else if (distanceFromOld < 0 && distanceFromOld > -5) { + for (int i = 0; i > distanceFromOld; i--) { + shiftDown(); + } + reloadFromStart(distanceFromOld * -1); + } else { + this.centerPage = newCenterPage; + getAll(); + } + } + + private void shiftUp() { + awaitAllUpdates(); + this.centerPage++; + page0 = page1; + page1 = page2; + page2 = page3; + page3 = page4; + } + + private void shiftDown() { + awaitAllUpdates(); + this.centerPage--; + page4 = page3; + page3 = page2; + page2 = page1; + page1 = page0; + } + + private void reloadFromStart(int to) { + for (int i = 0; i < to; i++) { + getChunk((centerPage - 2) + i, i); + } + } + + private void reloadFromEnd(int to) { + for (int i = 0; i < to; i++) { + getChunk((centerPage + 2) - i, 4 - i); + } + } + + private void getAll() { + getChunk(centerPage - 2, 0); + getChunk(centerPage - 1, 1); + getChunk(centerPage, 2); + getChunk(centerPage + 1, 3); + getChunk(centerPage + 2, 4); + } + + private void getChunk(int chunk, int page) { + int offset = thirdPage * chunk; + awaitUpdate(page); + synchronized (updating[page]) { + if (offset < 0) { + setPage(null, page); + return; + } + updating[page] = true; + } + FutureTask> futureTask = new FutureTask<>(() -> { + synchronized (updating[page]) { + try { + List read = chunkGetter.getChunk(thirdPage, offset); + setPage(read, page); + updating[page] = false; + notifyUpdateComplete(); + return read; + } catch (Exception e) { + setPage(null, page); + updating[page] = false; + notifyUpdateComplete(); + return null; + } + } + } + ); + executor.execute(futureTask); + } + + private void setPage(List list, int page) { + if (page == 0) { + page0 = list; + return; + } + if (page == 1) { + page1 = list; + return; + } + if (page == 2) { + page2 = list; + return; + } + if (page == 3) { + page3 = list; + return; + } + if (page == 4) { + page4 = list; + } + } + + public boolean isUpdating() { + for (Boolean b : updating) { + if (b) return true; + } + return false; + } + + public int firstAvailableIndex() { + int index = (centerPage - 1) * thirdPage; + if (index < 0) index = 0; + return index; + } + + public int lastAvailableIndex() { + int firstIndex = firstAvailableIndex(); + if (page1 != null) firstIndex += page1.size(); + if (page2 != null) firstIndex += page2.size(); + if (page3 != null) firstIndex += page3.size(); + return firstIndex; + } + + public void addUpdateCompleteListener(@NotNull ListUpdateCompleteListener listUpdateCompleteListener) { + this.updateCompleteListeners.add(listUpdateCompleteListener); + } + + public boolean removeUpdateCompleteListener(@NotNull ListUpdateCompleteListener listUpdateCompleteListener) { + return this.updateCompleteListeners.remove(listUpdateCompleteListener); + } + + @Override + public int size() { + return chunkGetter.getTotal(); + } + + @Override + public boolean isEmpty() { + return size() < 1; + } + + @Override + public boolean contains(Object o) { + throw new UnsupportedOperationException("Not supported"); + } + + @NotNull + @Override + public Iterator iterator() { + return listIterator(); + } + + + @Override + public Object @NotNull [] toArray() { + Iterator iterator = iterator(); + Object[] output = new Object[size()]; + for (int i = 0; i < output.length; i++) { + if (!iterator.hasNext()) { + continue; + } + output[i] = iterator.next(); + } + return output; + } + + @SuppressWarnings("unchecked") + @Override + public T1 @NotNull [] toArray(T1 @NotNull [] a) { + Iterator iterator = iterator(); + int size = this.size(); + if (a.length < this.size()) { + a = (T1[]) Array.newInstance(a.getClass().getComponentType(), size); + } + for (int i = 0; i < a.length; i++) { + if (!iterator.hasNext()) { + a[i] = null; + continue; + } + a[i] = (T1) iterator.next(); + } + return a; + } + + @Override + public boolean add(T t) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean remove(Object o) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean containsAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean addAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean addAll(int index, @NotNull Collection c) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean removeAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean retainAll(@NotNull Collection c) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public void clear() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public T get(int index) { + int pageNumber = calculatePage(index); + int offset = pageNumber - this.centerPage; + if (this.allowAutomaticLoading) { + if (offset < -1) { + preparePages(index, false); + return get(index); + } + if (offset > 1) { + preparePages(index, true); + return get(index); + } + } + List page = getListForPage(offset); + if (page == null) throw new ArrayIndexOutOfBoundsException(); + int indexInPage = calculateIndexInPage(index); + return page.get(indexInPage); + } + + @Override + public T set(int index, T element) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public void add(int index, T element) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public T remove(int index) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public int indexOf(Object o) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public int lastIndexOf(Object o) { + throw new UnsupportedOperationException("Not supported"); + } + + @NotNull + @Override + public ListIterator listIterator() { + return listIterator(0); + } + + @NotNull + @Override + public ListIterator listIterator(int index) { + return new ListIterator() { + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + private final PagedList creatingPagedList = new PagedList<>(chunkGetter, size() / 5); + + private int i = index; + private final int size = size(); + + @Override + public boolean hasNext() { + return i < size; + } + + @Override + public T next() { + T result = creatingPagedList.get(i); + i++; + return result; + } + + @Override + public boolean hasPrevious() { + return i > 0; + } + + @Override + public T previous() { + i--; + return creatingPagedList.get(i); + } + + @Override + public int nextIndex() { + return i; + } + + @Override + public int previousIndex() { + return i - 1; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public void set(T t) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public void add(T t) { + throw new UnsupportedOperationException("Not supported"); + } + }; + } + + @NotNull + @Override + public List subList(int fromIndex, int toIndex) { + int size = toIndex - fromIndex; + if (size < 0) size = 0; + List output = new ArrayList<>(size); + @SuppressWarnings("MismatchedQueryAndUpdateOfCollection") + PagedList creatingPagedList = new PagedList<>(chunkGetter, size / 5); + for (int i = fromIndex; i < toIndex; i++) { + output.add(creatingPagedList.get(i)); + } + return output; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PagedList pagedList = (PagedList) o; + return thirdPage == pagedList.thirdPage && allowAutomaticLoading == pagedList.allowAutomaticLoading && Objects.equals(chunkGetter, pagedList.chunkGetter); + } + + @Override + public int hashCode() { + return Objects.hash(thirdPage, allowAutomaticLoading, chunkGetter); + } +} diff --git a/tagyCore/src/test/java/de/sg_o/test/tagy/util/PagedListTest.java b/tagyCore/src/test/java/de/sg_o/test/tagy/util/PagedListTest.java new file mode 100644 index 0000000..33c36c1 --- /dev/null +++ b/tagyCore/src/test/java/de/sg_o/test/tagy/util/PagedListTest.java @@ -0,0 +1,389 @@ +/* + * + * Copyright (C) 2023 Joerg Bayer (SG-O) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package de.sg_o.test.tagy.util; + +import de.sg_o.lib.tagy.util.ChunkGetter; +import de.sg_o.lib.tagy.util.ListUpdateCompleteListener; +import de.sg_o.lib.tagy.util.PagedList; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +public class PagedListTest { + List testData = new ArrayList<>(); + + PagedList pagedList0; + PagedList pagedList1; + PagedList pagedList2; + PagedList pagedList3; + PagedList pagedList4; + PagedList pagedList5; + PagedList pagedList6; + PagedList pagedList7; + PagedList pagedList8; + PagedList pagedList9; + PagedList pagedList10; + PagedList pagedList11; + + @BeforeEach + void setUp() { + for (int i = 0; i < 100; i++) { + testData.add("TestData-" + i); + } + + ChunkGetter cg0 = new ChunkGetter() { + @Override + public List getChunk(int length, int offset) { + int end = offset + length; + if (end > testData.size()) { + end = testData.size(); + } + return testData.subList(offset, end); + } + + @Override + public int getTotal() { + return testData.size(); + } + }; + + pagedList0 = new PagedList<>(cg0, 5, false); + pagedList1 = new PagedList<>(cg0, 5); + pagedList2 = new PagedList<>(cg0, 6); + pagedList3 = new PagedList<>(cg0, 7); + pagedList4 = new PagedList<>(cg0, 8); + pagedList5 = new PagedList<>(cg0, 9); + pagedList6 = new PagedList<>(cg0, 10); + pagedList7 = new PagedList<>(cg0, 0); + pagedList8 = new PagedList<>(cg0, 1); + pagedList9 = new PagedList<>(cg0, 2); + pagedList10 = new PagedList<>(cg0, 3); + pagedList11 = new PagedList<>(cg0, 4); + } + + @Test + void testGet() { + AtomicBoolean listenerHit = new AtomicBoolean(false); + ListUpdateCompleteListener listUpdateCompleteListener = (pagedList, minIndex, maxIndex) -> { + assertEquals(pagedList0, pagedList); + assertEquals(0, minIndex); + assertEquals(6, maxIndex); + listenerHit.set(true); + }; + pagedList0.addUpdateCompleteListener(listUpdateCompleteListener); + assertFalse(pagedList0.isEmpty()); + pagedList0.preparePages(0); + assertEquals(testData.get(0), pagedList0.get(0)); + assertFalse(pagedList0.isUpdating()); + assertEquals(testData.get(1), pagedList0.get(1)); + assertEquals(testData.get(2), pagedList0.get(2)); + assertEquals(testData.get(3), pagedList0.get(3)); + assertEquals(testData.get(4), pagedList0.get(4)); + assertTrue(listenerHit.get()); + assertTrue(pagedList0.removeUpdateCompleteListener(listUpdateCompleteListener)); + + listenerHit.set(false); + listUpdateCompleteListener = (pagedList, minIndex, maxIndex) -> { + assertEquals(pagedList0, pagedList); + assertEquals(20, minIndex); + assertEquals(26, maxIndex); + listenerHit.set(true); + }; + pagedList0.addUpdateCompleteListener(listUpdateCompleteListener); + pagedList0.preparePages(20); + assertEquals(testData.get(20), pagedList0.get(20)); + assertEquals(testData.get(21), pagedList0.get(21)); + assertEquals(testData.get(22), pagedList0.get(22)); + assertEquals(testData.get(23), pagedList0.get(23)); + assertEquals(testData.get(24), pagedList0.get(24)); + assertTrue(listenerHit.get()); + assertTrue(pagedList0.removeUpdateCompleteListener(listUpdateCompleteListener)); + + pagedList0.preparePages(21); + assertEquals(testData.get(20), pagedList0.get(20)); + assertEquals(testData.get(21), pagedList0.get(21)); + assertEquals(testData.get(22), pagedList0.get(22)); + assertEquals(testData.get(23), pagedList0.get(23)); + assertEquals(testData.get(24), pagedList0.get(24)); + + pagedList0.preparePages(19); + assertEquals(testData.get(20), pagedList0.get(20)); + assertEquals(testData.get(21), pagedList0.get(21)); + assertEquals(testData.get(22), pagedList0.get(22)); + assertEquals(testData.get(23), pagedList0.get(23)); + assertEquals(testData.get(24), pagedList0.get(24)); + + pagedList0.preparePages(20); + assertEquals(testData.get(20), pagedList0.get(20)); + assertEquals(testData.get(21), pagedList0.get(21)); + assertEquals(testData.get(22), pagedList0.get(22)); + assertEquals(testData.get(23), pagedList0.get(23)); + assertEquals(testData.get(24), pagedList0.get(24)); + + pagedList0.preparePages(1); + assertEquals(testData.get(0), pagedList0.get(0)); + assertEquals(testData.get(1), pagedList0.get(1)); + assertEquals(testData.get(2), pagedList0.get(2)); + assertEquals(testData.get(3), pagedList0.get(3)); + assertEquals(testData.get(4), pagedList0.get(4)); + + assertEquals(testData.get(5), pagedList0.get(5)); + assertEquals(testData.get(6), pagedList0.get(6)); + assertEquals(testData.get(7), pagedList0.get(7)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(8)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(9)); + + pagedList0.preparePages(6); + assertEquals(testData.get(5), pagedList0.get(5)); + assertEquals(testData.get(6), pagedList0.get(6)); + assertEquals(testData.get(7), pagedList0.get(7)); + assertEquals(testData.get(8), pagedList0.get(8)); + assertEquals(testData.get(9), pagedList0.get(9)); + + assertEquals(testData.get(8), pagedList0.get(8)); + assertEquals(testData.get(7), pagedList0.get(7)); + assertEquals(testData.get(6), pagedList0.get(6)); + assertEquals(testData.get(5), pagedList0.get(5)); + assertEquals(testData.get(4), pagedList0.get(4)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(3)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(2)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(1)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList0.get(0)); + } + + @Test + void testGetAutomatic() { + testGetAutomatic(pagedList1); + testGetAutomatic(pagedList2); + testGetAutomatic(pagedList3); + testGetAutomatic(pagedList4); + testGetAutomatic(pagedList5); + testGetAutomatic(pagedList6); + testGetAutomatic(pagedList7); + testGetAutomatic(pagedList8); + testGetAutomatic(pagedList9); + testGetAutomatic(pagedList10); + testGetAutomatic(pagedList11); + } + + + void testGetAutomatic(PagedList pagedList) { + assertEquals(testData.get(0), pagedList.get(0)); + assertEquals(testData.get(1), pagedList.get(1)); + assertEquals(testData.get(2), pagedList.get(2)); + assertEquals(testData.get(3), pagedList.get(3)); + assertEquals(testData.get(4), pagedList.get(4)); + + pagedList.preparePages(20); + assertEquals(testData.get(20), pagedList.get(20)); + assertEquals(testData.get(21), pagedList.get(21)); + assertEquals(testData.get(22), pagedList.get(22)); + assertEquals(testData.get(23), pagedList.get(23)); + assertEquals(testData.get(24), pagedList.get(24)); + + assertEquals(testData.get(20), pagedList.get(20)); + assertEquals(testData.get(21), pagedList.get(21)); + assertEquals(testData.get(22), pagedList.get(22)); + assertEquals(testData.get(23), pagedList.get(23)); + assertEquals(testData.get(24), pagedList.get(24)); + + assertEquals(testData.get(20), pagedList.get(20)); + assertEquals(testData.get(21), pagedList.get(21)); + assertEquals(testData.get(22), pagedList.get(22)); + assertEquals(testData.get(23), pagedList.get(23)); + assertEquals(testData.get(24), pagedList.get(24)); + + assertEquals(testData.get(20), pagedList.get(20)); + assertEquals(testData.get(21), pagedList.get(21)); + assertEquals(testData.get(22), pagedList.get(22)); + assertEquals(testData.get(23), pagedList.get(23)); + assertEquals(testData.get(24), pagedList.get(24)); + + assertEquals(testData.get(0), pagedList.get(0)); + assertEquals(testData.get(1), pagedList.get(1)); + assertEquals(testData.get(2), pagedList.get(2)); + assertEquals(testData.get(3), pagedList.get(3)); + assertEquals(testData.get(4), pagedList.get(4)); + + assertEquals(testData.get(5), pagedList.get(5)); + assertEquals(testData.get(6), pagedList.get(6)); + assertEquals(testData.get(7), pagedList.get(7)); + assertEquals(testData.get(8), pagedList.get(8)); + assertEquals(testData.get(9), pagedList.get(9)); + + assertEquals(testData.get(5), pagedList.get(5)); + assertEquals(testData.get(6), pagedList.get(6)); + assertEquals(testData.get(7), pagedList.get(7)); + assertEquals(testData.get(8), pagedList.get(8)); + assertEquals(testData.get(9), pagedList.get(9)); + + assertEquals(testData.get(8), pagedList.get(8)); + assertEquals(testData.get(7), pagedList.get(7)); + assertEquals(testData.get(6), pagedList.get(6)); + assertEquals(testData.get(5), pagedList.get(5)); + assertEquals(testData.get(4), pagedList.get(4)); + assertEquals(testData.get(3), pagedList.get(3)); + assertEquals(testData.get(2), pagedList.get(2)); + assertEquals(testData.get(1), pagedList.get(1)); + assertEquals(testData.get(0), pagedList.get(0)); + + assertEquals(testData.get(95), pagedList.get(95)); + assertEquals(testData.get(96), pagedList.get(96)); + assertEquals(testData.get(97), pagedList.get(97)); + assertEquals(testData.get(98), pagedList.get(98)); + assertEquals(testData.get(99), pagedList.get(99)); + assertThrows(IndexOutOfBoundsException.class, () -> pagedList.get(100)); + + List sublist = pagedList.subList(5, 45); + assertEquals(testData.subList(5, 45), sublist); + } + + @Test + void testIterator() { + int index = 0; + Iterator iterator = pagedList0.iterator(); + while (iterator.hasNext()) { + assertEquals(testData.get(index), iterator.next()); + index++; + } + + index = 0; + iterator = pagedList1.iterator(); + while (iterator.hasNext()) { + assertEquals(testData.get(index), iterator.next()); + index++; + } + + index = 0; + for (String entry : pagedList0) { + assertEquals(testData.get(index), entry); + index++; + } + + index = 0; + for (String entry : pagedList1) { + assertEquals(testData.get(index), entry); + index++; + } + + String[] array0 = pagedList0.toArray(new String[0]); + String[] array1 = pagedList1.toArray(new String[120]); + Object[] array2 = pagedList1.toArray(); + + assertEquals(testData.size(), array0.length); + assertEquals(120, array1.length); + assertEquals(testData.size(), array2.length); + + index = 0; + for (String entry : array0) { + assertEquals(testData.get(index), entry); + index++; + } + + index = 0; + for (String entry : array1) { + if (index >= testData.size()) { + assertNull(entry); + continue; + } + assertEquals(testData.get(index), entry); + index++; + } + + index = 0; + for (Object entry : array2) { + assertEquals(testData.get(index), entry); + index++; + } + + ListIterator listIterator0 = pagedList0.listIterator(50); + index = 50; + while (listIterator0.hasNext()) { + assertEquals(testData.get(index), listIterator0.next()); + index++; + } + + while (listIterator0.hasPrevious()) { + index--; + assertEquals(testData.get(index), listIterator0.previous()); + } + + assertThrows(RuntimeException.class, listIterator0::remove); + assertThrows(RuntimeException.class, () -> listIterator0.set("Test")); + assertThrows(RuntimeException.class, () -> listIterator0.add("Test")); + + ListIterator listIterator1 = pagedList0.listIterator(20); + index = listIterator1.nextIndex(); + while (listIterator1.hasNext()) { + assertEquals(testData.get(index), listIterator1.next()); + index++; + } + index = listIterator1.previousIndex(); + while (listIterator1.hasPrevious()) { + assertEquals(testData.get(index), listIterator1.previous()); + index--; + } + } + + @Test + void notImplemented() { + assertThrows(UnsupportedOperationException.class, () -> pagedList0.contains("Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.add("Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.remove("Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.containsAll(testData)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.addAll(testData)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.addAll(0, testData)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.removeAll(testData)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.retainAll(testData)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.clear()); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.set(0, "Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.add(0, "Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.remove(0)); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.indexOf("Test")); + assertThrows(UnsupportedOperationException.class, () -> pagedList0.lastIndexOf("Test")); + } + + @Test + void equalsAndHashCode() { + assertNotEquals(pagedList0, pagedList1); + assertEquals(pagedList7, pagedList8); + + assertEquals(1640710054L, pagedList0.hashCode()); + assertEquals(1640709868L, pagedList1.hashCode()); + assertEquals(1640709868L, pagedList2.hashCode()); + assertEquals(1640710829L, pagedList3.hashCode()); + assertEquals(1640710829L, pagedList4.hashCode()); + assertEquals(1640710829L, pagedList5.hashCode()); + assertEquals(1640711790L, pagedList6.hashCode()); + assertEquals(1640708907L, pagedList7.hashCode()); + assertEquals(1640708907L, pagedList8.hashCode()); + assertEquals(1640708907L, pagedList9.hashCode()); + assertEquals(1640708907L, pagedList10.hashCode()); + assertEquals(1640709868L, pagedList11.hashCode()); + + assertNotEquals(pagedList0.hashCode(), pagedList1.hashCode()); + assertEquals(pagedList7.hashCode(), pagedList8.hashCode()); + } +}