diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java index b7580962f..d96673532 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisList.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.support.collections; import java.util.Collection; import java.util.Collections; +import java.util.ConcurrentModificationException; import java.util.Iterator; import java.util.List; import java.util.ListIterator; @@ -39,42 +40,32 @@ import org.springframework.util.Assert; * @author Costin Leau * @author Christoph Strobl * @author Mark Paluch + * @author John Blum */ public class DefaultRedisList extends AbstractRedisCollection implements RedisList { - private final BoundListOperations listOps; - - private volatile int maxSize = 0; private volatile boolean capped = false; - private class DefaultRedisListIterator extends RedisIterator { + private volatile int maxSize = 0; - public DefaultRedisListIterator(Iterator delegate) { - super(delegate); - } - - @Override - protected void removeFromRedisStorage(E item) { - DefaultRedisList.this.remove(item); - } - } + private final BoundListOperations listOps; /** * Constructs a new, uncapped {@link DefaultRedisList} instance. * * @param key Redis key of this list. - * @param operations {@link RedisOperations} for the value type of this list. + * @param operations {@link RedisOperations} used to retrieve values of the declared {@link E type} from this list. */ public DefaultRedisList(String key, RedisOperations operations) { this(operations.boundListOps(key)); } /** - * Constructs a new {@link DefaultRedisList} instance. + * Constructs a new {@link DefaultRedisList} instance constrained to the given {@link Integer max size}. * * @param key Redis key of this list. - * @param operations {@link RedisOperations} for the value type of this list. - * @param maxSize + * @param operations {@link RedisOperations} used to retrieve values of the declared {@link E type} from this list. + * @param maxSize {@link Integer maximum number of elements} allowed to be stored in this list. * @since 2.6 */ public DefaultRedisList(String key, RedisOperations operations, int maxSize) { @@ -91,10 +82,10 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } /** - * Constructs a new {@link DefaultRedisList} instance. + * Constructs a new {@link DefaultRedisList} instance constrained to the given {@link Integer max size}. * * @param boundOps {@link BoundListOperations} for the value type of this list. - * @param maxSize + * @param maxSize {@link Integer maximum number of elements} allowed to be stored in this list. */ public DefaultRedisList(BoundListOperations boundOps, int maxSize) { @@ -165,6 +156,7 @@ public class DefaultRedisList extends AbstractRedisCollection implements R return result; } + @SuppressWarnings("unchecked") private void potentiallyCap(RedisList destination) { if (destination instanceof DefaultRedisList) { ((DefaultRedisList) destination).cap(); @@ -217,11 +209,12 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override public boolean remove(Object o) { Long result = listOps.remove(1, o); - return (result != null && result.longValue() > 0); + return result != null && result > 0L; } @Override public void add(int index, E element) { + if (index == 0) { listOps.leftPush(element); cap(); @@ -244,23 +237,26 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } @Override - public boolean addAll(int index, Collection c) { + public boolean addAll(int index, Collection collection) { + // insert collection in reverse if (index == 0) { - Collection reverseC = CollectionUtils.reverse(c); - for (E e : reverseC) { - listOps.leftPush(e); + Collection reverseCollection = CollectionUtils.reverse(collection); + + for (E element : reverseCollection) { + listOps.leftPush(element); cap(); } + return true; } int size = size(); if (index == size()) { - for (E e : c) { - listOps.rightPush(e); + for (E element : collection) { + listOps.rightPush(element); cap(); } return true; @@ -275,34 +271,40 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override public E get(int index) { + if (index < 0 || index > size()) { throw new IndexOutOfBoundsException(); } + return listOps.index(index); } @Override - public int indexOf(Object o) { + @SuppressWarnings("unchecked") + public int indexOf(Object element) { + + Long index = listOps.indexOf((E) element); - Long index = listOps.indexOf((E) o); return index != null ? index.intValue() : -1; } @Override - public int lastIndexOf(Object o) { + @SuppressWarnings("unchecked") + public int lastIndexOf(Object element) { + + Long index = listOps.lastIndexOf((E) element); - Long index = listOps.lastIndexOf((E) o); return index != null ? index.intValue() : -1; } @Override public ListIterator listIterator() { - throw new UnsupportedOperationException(); + return listIterator(0); } @Override public ListIterator listIterator(int index) { - throw new UnsupportedOperationException(); + return new ListItr(index); } @Override @@ -311,10 +313,10 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } @Override - public E set(int index, E e) { + public E set(int index, E element) { E object = get(index); - listOps.set(index, e); + listOps.set(index, element); return object; } @@ -338,8 +340,8 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } @Override - public boolean offer(E e) { - listOps.rightPush(e); + public boolean offer(E element) { + listOps.rightPush(element); cap(); return true; } @@ -372,14 +374,14 @@ public class DefaultRedisList extends AbstractRedisCollection implements R // @Override - public void addFirst(E e) { - listOps.leftPush(e); + public void addFirst(E element) { + listOps.leftPush(element); cap(); } @Override - public void addLast(E e) { - add(e); + public void addLast(E element) { + add(element); } @Override @@ -396,22 +398,22 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override public E getLast() { - E e = peekLast(); - if (e == null) { + E element = peekLast(); + if (element == null) { throw new NoSuchElementException(); } - return e; + return element; } @Override - public boolean offerFirst(E e) { - addFirst(e); + public boolean offerFirst(E element) { + addFirst(element); return true; } @Override - public boolean offerLast(E e) { - addLast(e); + public boolean offerLast(E element) { + addLast(element); return true; } @@ -442,16 +444,16 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override public E pop() { - E e = poll(); - if (e == null) { + E element = poll(); + if (element == null) { throw new NoSuchElementException(); } - return e; + return element; } @Override - public void push(E e) { - addFirst(e); + public void push(E element) { + addFirst(element); } @Override @@ -460,52 +462,52 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } @Override - public boolean removeFirstOccurrence(Object o) { - return remove(o); + public boolean removeFirstOccurrence(Object element) { + return remove(element); } @Override public E removeLast() { - E e = pollLast(); - if (e == null) { + E element = pollLast(); + if (element == null) { throw new NoSuchElementException(); } - return e; + return element; } @Override - public boolean removeLastOccurrence(Object o) { - Long result = listOps.remove(-1, o); - return (result != null && result.longValue() > 0); + public boolean removeLastOccurrence(Object element) { + Long result = listOps.remove(-1, element); + return result != null && result > 0L; } // // BlockingQueue // @Override - public int drainTo(Collection c, int maxElements) { - if (this.equals(c)) { + public int drainTo(Collection collection, int maxElements) { + + if (this.equals(collection)) { throw new IllegalArgumentException("Cannot drain a queue to itself"); } - int size = size(); - int loop = (size >= maxElements ? maxElements : size); + int loop = (Math.min(size(), maxElements)); for (int index = 0; index < loop; index++) { - c.add(poll()); + collection.add(poll()); } return loop; } @Override - public int drainTo(Collection c) { - return drainTo(c, size()); + public int drainTo(Collection collection) { + return drainTo(collection, size()); } @Override - public boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException { - return offer(e); + public boolean offer(E element, long timeout, TimeUnit unit) throws InterruptedException { + return offer(element); } @Override @@ -515,8 +517,8 @@ public class DefaultRedisList extends AbstractRedisCollection implements R } @Override - public void put(E e) throws InterruptedException { - offer(e); + public void put(E element) throws InterruptedException { + offer(element); } @Override @@ -535,13 +537,13 @@ public class DefaultRedisList extends AbstractRedisCollection implements R // @Override - public boolean offerFirst(E e, long timeout, TimeUnit unit) throws InterruptedException { - return offerFirst(e); + public boolean offerFirst(E element, long timeout, TimeUnit unit) { + return offerFirst(element); } @Override - public boolean offerLast(E e, long timeout, TimeUnit unit) throws InterruptedException { - return offerLast(e); + public boolean offerLast(E element, long timeout, TimeUnit unit) { + return offerLast(element); } @Override @@ -552,18 +554,18 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override @Nullable - public E pollLast(long timeout, TimeUnit unit) throws InterruptedException { + public E pollLast(long timeout, TimeUnit unit) { return listOps.rightPop(timeout, unit); } @Override - public void putFirst(E e) throws InterruptedException { - add(e); + public void putFirst(E element) { + add(element); } @Override - public void putLast(E e) throws InterruptedException { - put(e); + public void putLast(E element) throws InterruptedException { + put(element); } @Override @@ -574,7 +576,7 @@ public class DefaultRedisList extends AbstractRedisCollection implements R @Override @Nullable - public E takeLast() throws InterruptedException { + public E takeLast() { return pollLast(0, TimeUnit.SECONDS); } @@ -592,4 +594,131 @@ public class DefaultRedisList extends AbstractRedisCollection implements R listOps.trim(0, maxSize - 1); } } + + private class DefaultRedisListIterator extends RedisIterator { + + public DefaultRedisListIterator(Iterator delegate) { + super(delegate); + } + + @Override + protected void removeFromRedisStorage(E item) { + DefaultRedisList.this.remove(item); + } + } + + private class Itr implements Iterator { + + /** + * Index of the {@link E element} in this iteration to be returned by subsequent call to {@link #next()}. + */ + int cursor = 0; + + /** + * Index of the {@link E element} in this iteration last returned by a call to {@link #next()}. + */ + int lastReturnedElementIndex = -1; + + @Nullable + E lastReturnedElement; + + @Override + public boolean hasNext() { + return this.cursor < size(); + } + + @Override + public E next() { + + try { + int index = this.cursor; + this.lastReturnedElement = get(index); + this.cursor = index + 1; + this.lastReturnedElementIndex = index; + return this.lastReturnedElement; + } catch (IndexOutOfBoundsException cause) { + throw new NoSuchElementException(cause); + } + } + + @Override + @SuppressWarnings("all") + public void remove() { + + Assert.state(this.lastReturnedElement != null, + "Next must be called before remove"); + + if (!DefaultRedisList.this.remove(this.lastReturnedElement)) { + throw new ConcurrentModificationException(); + } + + this.lastReturnedElementIndex = -1; + this.lastReturnedElement = null; + this.cursor--; + } + } + + private class ListItr extends Itr implements ListIterator { + + ListItr(int index) { + this.cursor = index; + } + + @Override + public boolean hasPrevious() { + return this.cursor > 0; + } + + @Override + public int nextIndex() { + return this.cursor; + } + + @Override + public int previousIndex() { + return this.cursor - 1; + } + + @Override + public void add(E element) { + + try { + int index = this.cursor; + DefaultRedisList.this.add(index, element); + this.lastReturnedElementIndex = -1; + this.lastReturnedElement = null; + this.cursor = index + 1; + } catch (IndexOutOfBoundsException cause) { + throw new ConcurrentModificationException(); + } + } + + @Override + public E previous() { + + try { + int index = this.cursor - 1; + this.lastReturnedElement = get(index); + this.lastReturnedElementIndex = index; + this.cursor = index; + return this.lastReturnedElement; + } catch (IndexOutOfBoundsException cause) { + throw new NoSuchElementException(); + } + } + + @Override + + public void set(E element) { + + Assert.state(this.lastReturnedElement != null, + "next() or previous() must be called before set(:E)"); + + try { + DefaultRedisList.this.set(this.lastReturnedElementIndex, element); + } catch (IndexOutOfBoundsException cause) { + throw new ConcurrentModificationException(); + } + } + } } diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisList.java b/src/main/java/org/springframework/data/redis/support/collections/RedisList.java index 8aba9d5ca..da0094834 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisList.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisList.java @@ -15,7 +15,7 @@ */ package org.springframework.data.redis.support.collections; -import static org.springframework.data.redis.connection.RedisListCommands.*; +import static org.springframework.data.redis.connection.RedisListCommands.Direction; import java.time.Duration; import java.util.Deque; @@ -36,6 +36,7 @@ import org.springframework.util.Assert; * * @author Costin Leau * @author Mark Paluch + * @author John Blum */ public interface RedisList extends RedisCollection, List, BlockingDeque { @@ -51,11 +52,12 @@ public interface RedisList extends RedisCollection, List, BlockingDeque } /** - * Constructs a new {@link RedisList} instance. + * Factory method used to construct a new {@link RedisList} from a Redis list reference by the given {@link String + * key}. * * @param key Redis key of this list. * @param operations {@link RedisOperations} for the value type of this list. - * @param maxSize + * @param maxSize {@link Integer} used to constrain the size of the list. * @since 2.6 */ static RedisList create(String key, RedisOperations operations, int maxSize) { @@ -73,10 +75,10 @@ public interface RedisList extends RedisCollection, List, BlockingDeque } /** - * Constructs a new {@link DefaultRedisList} instance. + * Constructs a new {@link DefaultRedisList}. * * @param boundOps {@link BoundListOperations} for the value type of this list. - * @param maxSize + * @param maxSize {@link Integer} constraining the size of the list. * @since 2.6 */ static RedisList create(BoundListOperations boundOps, int maxSize) { @@ -98,6 +100,25 @@ public interface RedisList extends RedisCollection, List, BlockingDeque @Nullable E moveFirstTo(RedisList destination, Direction destinationPosition); + /** + * Atomically returns and removes the first element of the list stored at the bound key, and pushes the element at the + * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored + * at {@link RedisList destination}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param destination must not be {@literal null}. + * @param destinationPosition must not be {@literal null}. + * @param timeout + * @param unit must not be {@literal null}. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @Nullable + E moveFirstTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit); + /** * Atomically returns and removes the first element of the list stored at the bound key, and pushes the element at the * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored @@ -123,25 +144,6 @@ public interface RedisList extends RedisCollection, List, BlockingDeque TimeoutUtils.toMillis(timeout.toMillis(), TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); } - /** - * Atomically returns and removes the first element of the list stored at the bound key, and pushes the element at the - * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored - * at {@link RedisList destination}. - *

- * Blocks connection until element available or {@code timeout} reached. - * - * @param destination must not be {@literal null}. - * @param destinationPosition must not be {@literal null}. - * @param timeout - * @param unit must not be {@literal null}. - * @return - * @since 2.6 - * @see Direction#first() - * @see Direction#last() - */ - @Nullable - E moveFirstTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit); - /** * Atomically returns and removes the last element of the list stored at the bound key, and pushes the element at the * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored @@ -157,6 +159,25 @@ public interface RedisList extends RedisCollection, List, BlockingDeque @Nullable E moveLastTo(RedisList destination, Direction destinationPosition); + /** + * Atomically returns and removes the last element of the list stored at the bound key, and pushes the element at the + * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored + * at {@link RedisList destination}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param destination must not be {@literal null}. + * @param destinationPosition must not be {@literal null}. + * @param timeout + * @param unit must not be {@literal null}. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @Nullable + E moveLastTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit); + /** * Atomically returns and removes the last element of the list stored at the bound key, and pushes the element at the * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored @@ -182,25 +203,6 @@ public interface RedisList extends RedisCollection, List, BlockingDeque TimeoutUtils.toMillis(timeout.toMillis(), TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); } - /** - * Atomically returns and removes the last element of the list stored at the bound key, and pushes the element at the - * first/last element (head/tail depending on the {@link Direction destinationPosition} argument) of the list stored - * at {@link RedisList destination}. - *

- * Blocks connection until element available or {@code timeout} reached. - * - * @param destination must not be {@literal null}. - * @param destinationPosition must not be {@literal null}. - * @param timeout - * @param unit must not be {@literal null}. - * @return - * @since 2.6 - * @see Direction#first() - * @see Direction#last() - */ - @Nullable - E moveLastTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit); - /** * Get elements between {@code start} and {@code end} from list at the bound key. * @@ -229,4 +231,92 @@ public interface RedisList extends RedisCollection, List, BlockingDeque * @see Redis Documentation: LTRIM */ RedisList trim(long start, long end); + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @param element element to be added to the head of the collection. + */ + @Override + default void addFirst(E element) { + add(0, element); + } + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @param element element to be added to be added the end of the collection. + */ + default void addLast(E element) { + add(element); + } + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @return the head of this {@link Deque}. + */ + @Nullable + default E getFirst() { + return peekFirst(); + } + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @return the tail of this {@link Deque}. + */ + @Nullable + default E getLast() { + return peekLast(); + } + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @return the head of this {@link Deque}. + */ + @Nullable + default E removeFirst() { + return pollFirst(); + } + + /** + * {@inheritDoc} + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @return the tail of this {@link Deque}. + */ + @Nullable + default E removeLast() { + return pollLast(); + } + + /** + * Returns a reverse-ordered view of this collection. + *

+ * The encounter order of elements returned by the view is the inverse of the encounter order of the elements + * stored in this collection. The reverse ordering affects all order-sensitive operations, including any operations + * on further views of the returned view. If the collection implementation permits modifications to this view, + * the modifications "write-through" to the underlying collection. Changes to the underlying collection might + * or might not be visible in this reversed view, depending upon the implementation. + *

+ * This method is forward-compatible with Java 21 {@literal SequencedCollections}. + * + * @return a reverse-ordered view of this collection. + */ + default RedisList reversed() { + return new ReversedRedisListView<>(this); + } } diff --git a/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java b/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java new file mode 100644 index 000000000..7c78304c0 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/support/collections/ReversedRedisListView.java @@ -0,0 +1,739 @@ +/* + * Copyright 2023 the original author or authors. + * + * 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 + * + * https://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 org.springframework.data.redis.support.collections; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.TimeUnit; +import java.util.function.Consumer; +import java.util.function.IntFunction; +import java.util.function.Predicate; +import java.util.function.UnaryOperator; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; + +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.RedisListCommands.Direction; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.lang.Nullable; + +/** + * Implementation and view of an existing {@link RedisList} where the elements in the list (deque) are returned in + * reverse order. + * + * @author John Blum + * @author Mark Paluch + * @param {@link Class type} of the {@link Object elements} contained in the underlying, wrapped {@link RedisList}. + * @since 3.2 + */ +class ReversedRedisListView implements RedisList { + + private final RedisList base; + + ReversedRedisListView(RedisList list) { + this.base = list; + } + + // ========== RedisCollection ========== + + @Override + public String getKey() { + return this.base.getKey(); + } + + @Nullable + @Override + public Long getExpire() { + return this.base.getExpire(); + } + + @Override + public RedisOperations getOperations() { + return this.base.getOperations(); + } + + @Nullable + @Override + public DataType getType() { + return this.base.getType(); + } + + @Nullable + @Override + public Boolean expire(long timeout, TimeUnit unit) { + return this.base.expire(timeout, unit); + } + + @Nullable + @Override + public Boolean expireAt(Date date) { + return this.base.expireAt(date); + } + + @Nullable + @Override + public Boolean persist() { + return this.base.persist(); + } + + @Nullable + @Override + public E moveFirstTo(RedisList destination, Direction destinationPosition) { + return this.base.moveLastTo(destination, destinationPosition); + } + + @Nullable + @Override + public E moveFirstTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit) { + return base.moveLastTo(destination, destinationPosition, timeout, unit); + } + + @Nullable + @Override + public E moveLastTo(RedisList destination, Direction destinationPosition) { + return base.moveFirstTo(destination, destinationPosition); + } + + @Nullable + @Override + public E moveLastTo(RedisList destination, Direction destinationPosition, long timeout, TimeUnit unit) { + return base.moveFirstTo(destination, destinationPosition, timeout, unit); + } + + @Override + public List range(long start, long end) { + return this.base.range(end, start); + } + + @Override + public void rename(String newKey) { + this.base.rename(newKey); + } + + @Override + public RedisList trim(int start, int end) { + this.base.trim(end, start); + return this; + } + + @Override + public RedisList trim(long start, long end) { + this.base.trim(end, start); + return this; + } + + // ========== Iterable ========== + + @Override + public void forEach(Consumer action) { + for (E e : this) + action.accept(e); + } + + @Override + public Iterator iterator() { + return new DescendingIterator(); + } + + @Override + public Spliterator spliterator() { + return Spliterators.spliteratorUnknownSize(new DescendingIterator(), 0); + } + + // ========== Collection ========== + + @Override + public boolean add(E element) { + this.base.add(0, element); + return true; + } + + @Override + @SuppressWarnings("unchecked") + public boolean addAll(Collection collection) { + + return !org.springframework.util.CollectionUtils.isEmpty(collection) + && this.base.addAll(0, Arrays.asList(reverse((E[]) collection.toArray()))); + } + + @Override + public void clear() { + this.base.clear(); + } + + @Override + public boolean contains(Object element) { + return this.base.contains(element); + } + + @Override + public boolean containsAll(Collection collection) { + return this.base.containsAll(collection); + } + + public boolean equals(Object obj) { + + if (this == obj) { + return true; + } + + if (!(obj instanceof List that)) { + return false; + } + + ListIterator thisListIterator = this.listIterator(); + ListIterator thatListIterator = that.listIterator(); + + while (thisListIterator.hasNext() && thatListIterator.hasNext()) { + if (!(Objects.equals(thisListIterator.next(), thatListIterator.next()))) { + return false; + } + } + + return !(thisListIterator.hasNext() || thatListIterator.hasNext()); + } + + public int hashCode() { + int hashCode = 1; + for (E element : this) { + hashCode = 31 * hashCode + Objects.hashCode(element); + } + return hashCode; + } + + @Override + public boolean isEmpty() { + return this.base.isEmpty(); + } + + @Override + public Stream parallelStream() { + return StreamSupport.stream(spliterator(), true); + } + + @Override + public boolean remove(Object element) { + + Iterator it = iterator(); + + while (it.hasNext()) { + if (Objects.equals(element, it.next())) { + it.remove(); + return true; + } + } + + return false; + } + + @Override + public boolean removeAll(Collection collection) { + + Objects.requireNonNull(collection); + + Iterator it = iterator(); + boolean modified = false; + + while (it.hasNext()) { + if (collection.contains(it.next())) { + it.remove(); + modified = true; + } + } + + return modified; + } + + @Override + public boolean retainAll(Collection collection) { + + Objects.requireNonNull(collection); + + Iterator it = iterator(); + boolean modified = false; + + while (it.hasNext()) { + if (!collection.contains(it.next())) { + it.remove(); + modified = true; + } + } + + return modified; + } + + @Override + public int size() { + return this.base.size(); + } + + @Override + public Stream stream() { + return StreamSupport.stream(spliterator(), false); + } + + @Override + public Object[] toArray() { + return reverse(this.base.toArray()); + } + + @Override + public T[] toArray(T[] array) { + return toArrayReversed(this.base, array); + } + + @Override + public T[] toArray(IntFunction generator) { + return reverse(this.base.toArray(generator)); + } + + public String toString() { + + Iterator it = iterator(); + + if (!it.hasNext()) { + return "[]"; + } + + StringBuilder stringBuilder = new StringBuilder("["); + + for (;;) { + E element = it.next(); + stringBuilder.append(element == this ? "(this Collection)" : element); + if (!it.hasNext()) { + return stringBuilder.append(']').toString(); + } + stringBuilder.append(',').append(' '); + } + } + + // ========== List ========== + + @Override + public void add(int index, E element) { + + int size = this.base.size(); + + checkClosedRange(index, size); + this.base.add(size - index, element); + } + + @Override + public boolean addAll(int index, Collection c) { + + int size = base.size(); + + checkClosedRange(index, size); + + @SuppressWarnings("unchecked") + E[] adds = (E[]) c.toArray(); + + if (adds.length == 0) { + return false; + } else { + base.addAll(size - index, Arrays.asList(reverse(adds))); + return true; + } + } + + @Override + public E get(int index) { + int size = base.size(); + Objects.checkIndex(index, size); + return this.base.get(size - index - 1); + } + + @Override + @SuppressWarnings("all") + public int indexOf(Object element) { + int lastIndex = this.base.lastIndexOf(element); + return lastIndex == -1 ? -1 : this.base.size() - lastIndex - 1; + } + + @Override + @SuppressWarnings("all") + public int lastIndexOf(Object element) { + int index = this.base.indexOf(element); + return index == -1 ? -1 : this.base.size() - index - 1; + } + + @Override + public ListIterator listIterator() { + return new DescendingListIterator(base.size(), 0); + } + + @Override + public ListIterator listIterator(int index) { + int size = this.base.size(); + checkClosedRange(index, size); + return new DescendingListIterator(size, index); + } + + @Override + public E remove(int index) { + int size = this.base.size(); + Objects.checkIndex(index, size); + return this.base.remove(size - index - 1); + } + + @Override + public boolean removeIf(Predicate filter) { + return this.base.removeIf(filter); + } + + @Override + public void replaceAll(UnaryOperator operator) { + this.base.replaceAll(operator); + } + + @Override + public void sort(Comparator comparator) { + this.base.sort(Collections.reverseOrder(comparator)); + } + + @Override + public E set(int index, E element) { + int size = this.base.size(); + Objects.checkIndex(index, size); + return this.base.set(size - index - 1, element); + } + + @Override + public List subList(int fromIndex, int toIndex) { + throw new UnsupportedOperationException(); + } + + // ========== BlockingDeque ========== + + @Override + public E element() { + return peekLast(); + } + + @Override + public boolean offer(E element) { + return this.base.offerFirst(element); + } + + @Override + public boolean offer(E element, long timeout, TimeUnit unit) throws InterruptedException { + return this.base.offerFirst(element, timeout, unit); + } + + @Override + public boolean offerFirst(E element) { + return this.base.offerLast(element); + } + + @Override + public boolean offerFirst(E element, long timeout, TimeUnit unit) throws InterruptedException { + return this.base.offerLast(element, timeout, unit); + } + + @Override + public boolean offerLast(E element) { + return this.base.offerFirst(element); + } + + @Override + public boolean offerLast(E element, long timeout, TimeUnit unit) throws InterruptedException { + return this.base.offerFirst(element, timeout, unit); + } + + @Override + public void putFirst(E element) throws InterruptedException { + this.base.putLast(element); + } + + @Override + public void putLast(E element) throws InterruptedException { + this.base.putFirst(element); + } + + @Nullable + @Override + public E pollFirst(long timeout, TimeUnit unit) throws InterruptedException { + return this.base.pollLast(timeout, unit); + } + + @Nullable + @Override + public E pollLast(long timeout, TimeUnit unit) throws InterruptedException { + return this.base.pollFirst(timeout, unit); + } + + @Override + public void put(E element) throws InterruptedException { + this.base.offerFirst(element); + } + + @Override + public boolean removeFirstOccurrence(Object element) { + return this.base.removeLastOccurrence(element); + } + + @Override + public boolean removeLastOccurrence(Object element) { + return this.base.removeFirstOccurrence(element); + } + + @Override + public E take() throws InterruptedException { + return takeFirst(); + } + + @Override + public E takeFirst() throws InterruptedException { + return this.base.takeLast(); + } + + @Override + public E takeLast() throws InterruptedException { + return this.base.takeFirst(); + } + + // ========== Deque ========== + + @Override + public Iterator descendingIterator() { + return this.base.iterator(); + } + + @Override + public int drainTo(Collection collection) { + return drainTo(collection, size()); + } + + @Override + public int drainTo(Collection collection, int maxElements) { + + if (this.equals(collection)) { + throw new IllegalArgumentException("Cannot drain a queue to itself"); + } + + int loop = Math.min(size(), maxElements); + + for (int index = 0; index < loop; index++) { + collection.add(poll()); + } + + return loop; + } + + @Override + public E peek() { + return peekLast(); + } + + @Override + public E peekFirst() { + return this.base.peekLast(); + } + + @Override + public E peekLast() { + return this.base.peekFirst(); + } + + @Override + public E poll() { + return pollLast(); + } + + @Nullable + @Override + public E poll(long timeout, TimeUnit unit) throws InterruptedException { + return pollLast(0, TimeUnit.SECONDS); + } + + @Override + public E pollFirst() { + return this.base.pollLast(); + } + + @Override + public E pollLast() { + return this.base.pollFirst(); + } + + @Override + public E pop() { + + E element = poll(); + + if (element == null) { + throw new NoSuchElementException(); + } + + return element; + } + + @Override + public void push(E element) { + this.base.addLast(element); + } + + @Override + public int remainingCapacity() { + return this.base.remainingCapacity(); + } + + @Override + public E remove() { + return pollLast(); + } + + @Override + public RedisList reversed() { + return this.base; + } + + class DescendingIterator implements Iterator { + + final ListIterator it = base.listIterator(base.size()); + + @Override + public boolean hasNext() { + return this.it.hasPrevious(); + } + + @Override + public E next() { + return this.it.previous(); + } + + @Override + public void remove() { + this.it.remove(); + } + } + + class DescendingListIterator implements ListIterator { + + final ListIterator it; + + DescendingListIterator(int size, int position) { + + if (position < 0 || position > size) { + String message = String.format("Position [%d] is out of bounds: [0, %d]", position, size); + throw new IndexOutOfBoundsException(message); + } + + this.it = base.listIterator(size - position); + } + + @Override + public boolean hasNext() { + return it.hasPrevious(); + } + + @Override + public E next() { + return it.previous(); + } + + @Override + public boolean hasPrevious() { + return it.hasNext(); + } + + @Override + public E previous() { + return it.next(); + } + + @Override + public int nextIndex() { + return base.size() - it.nextIndex(); + } + + @Override + public int previousIndex() { + return nextIndex() - 1; + } + + @Override + public void remove() { + it.remove(); + } + + @Override + public void set(E e) { + it.set(e); + } + + @Override + public void add(E e) { + it.add(e); + it.previous(); + } + } + + /** + * Reverses the elements of an array in-place. + * + * @param the array component type + * @param array the array to be reversed + * @return the reversed array, always the same array as the argument + */ + static T[] reverse(T[] array) { + + int limit = array.length / 2; + + for (int i = 0, j = array.length - 1; i < limit; i++, j--) { + swap(array, i, j); + } + + return array; + } + + private static void swap(T[] array, int indexOne, int indexTwo) { + T element = array[indexOne]; + array[indexOne] = array[indexTwo]; + array[indexTwo] = element; + } + + static T[] toArrayReversed(Collection collection, T[] array) { + + T[] newArray = reverse(collection.toArray(Arrays.copyOfRange(array, 0, 0))); + + if (newArray.length > array.length) { + return newArray; + } else { + System.arraycopy(newArray, 0, array, 0, newArray.length); + if (array.length > newArray.length) { + array[newArray.length] = null; + } + return array; + } + } + + static void checkClosedRange(int index, int size) { + if (index < 0 || index > size) { + throw new IndexOutOfBoundsException("Index: " + index + ", Size: " + size); + } + } +} diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java index d45c9cd2f..2176bb09a 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListIntegrationTests.java @@ -15,41 +15,43 @@ */ package org.springframework.data.redis.support.collections; -import static org.assertj.core.api.Assertions.*; -import static org.assertj.core.api.Assumptions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.ListIterator; import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; - import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.connection.RedisListCommands; -import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.parametrized.ParameterizedRedisTest; /** - * Integration test for RedisList + * Integration tests for RedisList * * @author Costin Leau * @author Jennifer Hickey * @author Mark Paluch + * @author John Blum */ public abstract class AbstractRedisListIntegrationTests extends AbstractRedisCollectionIntegrationTests { protected RedisList list; /** - * Constructs a new AbstractRedisListTests instance. + * Constructs a new {@link AbstractRedisListIntegrationTests}. * - * @param factory - * @param template + * @param factory {@link ObjectFactory} used to create different types of elements to store in the list. + * @param template {@link RedisTemplate} used to perform operations on Redis. */ @SuppressWarnings("rawtypes") AbstractRedisListIntegrationTests(ObjectFactory factory, RedisTemplate template) { @@ -61,11 +63,12 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis public void setUp() throws Exception { super.setUp(); - list = (RedisList) collection; + this.list = (RedisList) this.collection; } @ParameterizedRedisTest void testAddIndexObjectHead() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -80,6 +83,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testAddIndexObjectTail() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -94,6 +98,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testAddIndexObjectMiddle() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -107,6 +112,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void addAllIndexCollectionHead() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -118,7 +124,9 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis List asList = Arrays.asList(t3, t4); assertThat(list.get(0)).isEqualTo(t1); + list.addAll(0, asList); + // verify insertion order assertThat(list.get(0)).isEqualTo(t3); assertThat(list.get(1)).isEqualTo(t4); @@ -126,6 +134,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void addAllIndexCollectionTail() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -146,6 +155,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void addAllIndexCollectionMiddle() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -164,8 +174,6 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @EnabledOnCommand("LPOS") void testIndexOfObject() { - assumeThat(template.getConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); - T t1 = getT(); T t2 = getT(); @@ -180,6 +188,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testOffer() { + T t1 = getT(); assertThat(list.offer(t1)).isTrue(); @@ -188,11 +197,17 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testPeek() { + assertThat(list.peek()).isNull(); + T t1 = getT(); + list.add(t1); + assertThat(list.peek()).isEqualTo(t1); + list.clear(); + assertThat(list.peek()).isNull(); } @@ -202,8 +217,10 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(list::element); T t1 = getT(); + list.add(t1); assertThat(list.element()).isEqualTo(t1); + list.clear(); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(list::element); } @@ -215,9 +232,13 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testPoll() { + assertThat(list.poll()).isNull(); + T t1 = getT(); + list.add(t1); + assertThat(list.poll()).isEqualTo(t1); assertThat(list.poll()).isNull(); } @@ -232,16 +253,20 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testRemove() { + assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(list::remove); T t1 = getT(); + list.add(t1); + assertThat(list.remove()).isEqualTo(t1); assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(list::remove); } @ParameterizedRedisTest // GH-2039 @EnabledOnCommand("LMOVE") + @SuppressWarnings("unchecked") void testMoveFirstTo() { RedisList target = new DefaultRedisList(template.boundListOps(collection.getKey() + ":target")); @@ -263,6 +288,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest // GH-2039 @EnabledOnCommand("LMOVE") + @SuppressWarnings("unchecked") void testMoveLastTo() { RedisList target = new DefaultRedisList(template.boundListOps(collection.getKey() + ":target")); @@ -284,12 +310,15 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testRange() { + T t1 = getT(); T t2 = getT(); assertThat(list.range(0, -1)).isEmpty(); + list.add(t1); list.add(t2); + assertThat(list.range(0, -1)).hasSize(2); assertThat(list.range(0, 0).get(0)).isEqualTo(t1); assertThat(list.range(1, 1).get(0)).isEqualTo(t2); @@ -302,22 +331,28 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testSet() { + T t1 = getT(); T t2 = getT(); + list.add(t1); list.set(0, t1); + assertThat(list.set(0, t2)).isEqualTo(t1); assertThat(list.get(0)).isEqualTo(t2); } @ParameterizedRedisTest void testTrim() { + T t1 = getT(); T t2 = getT(); assertThat(list.trim(0, 0)).isEmpty(); + list.add(t1); list.add(t2); + assertThat(list).hasSize(2); assertThat(list.trim(0L, 0L)).hasSize(1); assertThat(list).hasSize(1); @@ -327,14 +362,22 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @SuppressWarnings("unchecked") @ParameterizedRedisTest - void testCappedCollection() throws Exception { + void testCappedCollection() { + RedisList cappedList = new DefaultRedisList(template.boundListOps(collection.getKey() + ":capped"), 1); + T first = getT(); + cappedList.offer(first); + assertThat(cappedList).hasSize(1); + cappedList.add(getT()); + assertThat(cappedList).hasSize(1); + T last = getT(); + cappedList.add(last); assertThat(cappedList).hasSize(1); assertThat(cappedList.get(0)).isEqualTo(first); @@ -342,15 +385,15 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testAddFirst() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); - list.addFirst(t1); - list.addFirst(t2); - list.addFirst(t3); + Arrays.asList(t1, t2, t3).forEach(this.list::addFirst); Iterator iterator = list.iterator(); + assertThat(iterator.next()).isEqualTo(t3); assertThat(iterator.next()).isEqualTo(t2); assertThat(iterator.next()).isEqualTo(t1); @@ -358,11 +401,23 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testAddLast() { - testAdd(); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + Arrays.asList(t1, t2, t3).forEach(this.list::addLast); + + Iterator iterator = list.iterator(); + + assertThat(iterator.next()).isEqualTo(t1); + assertThat(iterator.next()).isEqualTo(t2); + assertThat(iterator.next()).isEqualTo(t3); } @ParameterizedRedisTest void testDescendingIterator() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -372,14 +427,70 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis list.add(t3); Iterator iterator = list.descendingIterator(); + assertThat(iterator.next()).isEqualTo(t3); assertThat(iterator.next()).isEqualTo(t2); assertThat(iterator.next()).isEqualTo(t1); + } + @ParameterizedRedisTest // GH-2602 + void testListIteratorAddNextPreviousIsCorrect() { + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + ListIterator listIterator = this.list.listIterator(); + + assertThat(listIterator).isNotNull(); + + Arrays.asList(t1, t2, t3).forEach(listIterator::add); + + assertThat(this.list).containsExactly(t1, t2, t3); + assertThat(listIterator.hasNext()).isFalse(); + assertThat(listIterator.hasPrevious()).isTrue(); + assertThat(listIterator.previous()).isEqualTo(t3); + assertThat(listIterator.previous()).isEqualTo(t2); + assertThat(listIterator.previous()).isEqualTo(t1); + assertThat(listIterator.hasPrevious()).isFalse(); + assertThat(listIterator.hasNext()).isTrue(); + assertThat(listIterator.next()).isEqualTo(t1); + assertThat(listIterator.next()).isEqualTo(t2); + assertThat(listIterator.next()).isEqualTo(t3); + assertThat(listIterator.hasNext()).isFalse(); + assertThat(listIterator.hasPrevious()).isTrue(); + } + + @ParameterizedRedisTest // GH-2602 + public void testListIteratorSetIsCorrect() { + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + T t4 = getT(); + T t5 = getT(); + + Collections.addAll(this.list, t1, t2, t3, t2, t5); + + assertThat(this.list).containsExactly(t1, t2, t3, t2, t5); + + ListIterator listIterator = this.list.listIterator(); + + int index = 0; + + while (listIterator.hasNext()) { + listIterator.next(); + if (index++ == 3) { + listIterator.set(t4); + } + } + + assertThat(this.list).containsExactly(t1, t2, t3, t4, t5); } @ParameterizedRedisTest void testDrainToCollectionWithMaxElements() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -391,12 +502,14 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis List c = new ArrayList<>(); list.drainTo(c, 2); + assertThat(list).hasSize(1).contains(t3); assertThat(c).hasSize(2).contains(t1, t2); } @ParameterizedRedisTest void testDrainToCollection() { + T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -408,12 +521,14 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis List c = new ArrayList<>(); list.drainTo(c); + assertThat(list).isEmpty(); assertThat(c).hasSize(3).contains(t1, t2, t3); } @ParameterizedRedisTest void testGetFirst() { + T t1 = getT(); T t2 = getT(); @@ -445,6 +560,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testPeekLast() { + T t1 = getT(); T t2 = getT(); @@ -462,6 +578,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @ParameterizedRedisTest void testPollLast() { + T t1 = getT(); T t2 = getT(); @@ -469,6 +586,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis list.add(t2); T last = list.pollLast(); + assertThat(last).isEqualTo(t2); assertThat(list).hasSize(1).contains(t1); } @@ -483,6 +601,7 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis list.add(t2); T last = list.pollLast(1, TimeUnit.MILLISECONDS); + assertThat(last).isEqualTo(t2); assertThat(list).hasSize(1).contains(t1); } @@ -532,8 +651,8 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis list.add(t2); list.add(t1); list.add(t2); - list.removeLastOccurrence(t2); + assertThat(list).hasSize(3).containsExactly(t1, t2, t1); } @@ -556,8 +675,6 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis @EnabledOnCommand("LPOS") void lastIndexOf() { - assumeThat(template.getConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); - T t1 = getT(); T t2 = getT(); T t3 = getT(); @@ -569,4 +686,163 @@ public abstract class AbstractRedisListIntegrationTests extends AbstractRedis assertThat(list.lastIndexOf(t1)).isEqualTo(2); } + + @ParameterizedRedisTest // GH-2602 + void testReversed() { + + T elementOne = getT(); + T elementTwo = getT(); + T elementThree = getT(); + + Collections.addAll(this.list, elementOne, elementTwo, elementThree); + + assertThat(this.list).containsExactly(elementOne, elementTwo, elementThree); + + RedisList reversedList = this.list.reversed(); + + assertThat(reversedList).isNotNull(); + assertThat(reversedList).isNotSameAs(this.list); + assertThat(reversedList).hasSameSizeAs(this.list); + assertThat(reversedList).containsExactly(elementThree, elementTwo, elementOne); + assertThat(reversedList.reversed()).isEqualTo(this.list); + } + + @ParameterizedRedisTest // // GH-2602 + public void testReversedListIterator() { + + T elementOne = getT(); + T elementTwo = getT(); + T elementThree = getT(); + T elementFour = getT(); + + Collections.addAll(this.list, elementOne, elementTwo, elementThree, elementFour); + + assertThat(this.list).containsExactly(elementOne, elementTwo, elementThree, elementFour); + + List expectedList = Arrays.asList(elementFour, elementThree, elementTwo, elementOne); + + RedisList reversedList = this.list.reversed(); + + assertThat(reversedList).containsExactly(elementFour, elementThree, elementTwo, elementOne); + + Iterator reversedListIterator = reversedList.iterator(); + + assertThat(reversedListIterator).isNotNull(); + assertThat(reversedListIterator).hasNext(); + + int index = -1; + + while (reversedListIterator.hasNext()) { + assertThat(reversedListIterator.next()).isEqualTo(expectedList.get(++index)); + if (index == 1) { + reversedListIterator.remove(); + ++index; + } + } + + assertThat(reversedList).containsExactly(elementFour, elementTwo, elementOne); + + RedisList reorderedList = reversedList.reversed(); + + assertThat(reorderedList).isEqualTo(this.list); + assertThat(reorderedList).hasSameSizeAs(reversedList); + assertThat(reorderedList).containsExactly(elementOne, elementTwo, elementFour); + } + + @ParameterizedRedisTest // GH-2602 + void testReversedWithAddFirst() { + + T elementOne = getT(); + T elementTwo = getT(); + T elementThree = getT(); + + Collections.addAll(this.list, elementOne, elementTwo); + + RedisList reversedList = this.list.reversed(); + + reversedList.addFirst(elementThree); + + assertThat(reversedList).containsExactly(elementThree, elementTwo, elementOne); + + RedisList reorderedList = reversedList.reversed(); + + assertThat(reorderedList).containsExactly(elementOne, elementTwo, elementThree); + } + + @ParameterizedRedisTest // GH-2602 + void testReversedWithAddLast() { + + T elementZero = getT(); + T elementOne = getT(); + T elementTwo = getT(); + + Collections.addAll(this.list, elementOne, elementTwo); + + assertThat(this.list).containsExactly(elementOne, elementTwo); + + RedisList reversedList = this.list.reversed(); + + reversedList.addLast(elementZero); + + assertThat(reversedList).containsExactly(elementTwo, elementOne, elementZero); + + RedisList reorderedList = reversedList.reversed(); + + assertThat(reorderedList).containsExactly(elementZero, elementOne, elementTwo); + } + + @ParameterizedRedisTest // GH-2602 + void testReversedWithRemoveFirst() { + + T elementOne = getT(); + T elementTwo = getT(); + T elementThree = getT(); + + Collections.addAll(this.list, elementOne, elementTwo, elementThree); + + RedisList reversedList = this.list.reversed(); + + assertThat(reversedList).containsExactly(elementThree, elementTwo, elementOne); + assertThat(reversedList.removeFirst()).isEqualTo(elementThree); + assertThat(reversedList).containsExactly(elementTwo, elementOne); + + RedisList reorderedList = reversedList.reversed(); + + assertThat(reorderedList).containsExactly(elementOne, elementTwo); + } + + @ParameterizedRedisTest // GH-2602 + void testReversedWithRemoveLast() { + + T elementOne = getT(); + T elementTwo = getT(); + T elementThree = getT(); + + Collections.addAll(this.list, elementOne, elementTwo, elementThree); + + RedisList reversedList = this.list.reversed(); + + assertThat(reversedList).containsExactly(elementThree, elementTwo, elementOne); + assertThat(reversedList.removeLast()).isEqualTo(elementOne); + assertThat(reversedList).containsExactly(elementThree, elementTwo); + + RedisList reorderedList = reversedList.reversed(); + + assertThat(reorderedList).containsExactly(elementTwo, elementThree); + } + + @SuppressWarnings("unused") + private static String toString(List list) { + + StringBuilder stringBuilder = new StringBuilder("["); + + boolean comma = false; + + for (Object element : list) { + stringBuilder.append(comma ? ", " : "").append(element); + comma = true; + } + + return stringBuilder.append("]").toString(); + } } diff --git a/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java index 4a8137ced..2d5a0138a 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisListIntegrationTests.java @@ -22,14 +22,15 @@ import org.springframework.data.redis.core.RedisTemplate; * Parameterized instance of Redis tests. * * @author Costin Leau + * @author John Blum */ public class RedisListIntegrationTests extends AbstractRedisListIntegrationTests { /** - * Constructs a new RedisListTests instance. + * Constructs a new, parameterized {@link RedisListIntegrationTests}. * - * @param factory - * @param connFactory + * @param factory {@link ObjectFactory} used to create different types of elements to store in the list. + * @param template {@link RedisTemplate} used to perform operations on Redis. */ public RedisListIntegrationTests(ObjectFactory factory, RedisTemplate template) { super(factory, template); @@ -40,7 +41,6 @@ public class RedisListIntegrationTests extends AbstractRedisListIntegrationTests } AbstractRedisCollection createCollection() { - String redisName = getClass().getName(); - return new DefaultRedisList<>(redisName, template); + return new DefaultRedisList(getClass().getName(), this.template); } }