diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java index 772b8920a..963e4b4bf 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -702,6 +702,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return convertAndReturn(delegate.lPop(key), identityConverter); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lPos(byte[], byte[], java.lang.Integer, java.lang.Integer) + */ + @Override + public List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count) { + return convertAndReturn(delegate.lPos(key, element, rank, count), identityConverter); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lPush(byte[], byte[][]) @@ -2138,6 +2147,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return convertAndReturn(delegate.lPop(serialize(key)), bytesToString); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#lPos(java.lang.String, java.lang.String, java.lang.Integer, java.lang.Integer) + */ + @Override + public List lPos(String key, String element, @Nullable Integer rank, @Nullable Integer count) { + return lPos(serialize(key), serialize(element), rank, count); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#lPush(java.lang.String, java.lang.String[]) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 684273ab8..fcd40b8d4 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -621,6 +621,13 @@ public interface DefaultedRedisConnection extends RedisConnection { return listCommands().rPush(key, values); } + /** @deprecated in favor of {@link RedisConnection#listCommands()}}. */ + @Override + @Deprecated + default List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count) { + return listCommands().lPos(key, element, rank, count); + } + /** @deprecated in favor of {@link RedisConnection#listCommands()}}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java index 58ab9c268..79b512558 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java @@ -312,6 +312,125 @@ public interface ReactiveListCommands { */ Flux> lTrim(Publisher commands); + /** + * {@code LPOS} command parameters. + * + * @author Christoph Strobl + * @since 2.4 + * @see Redis Documentation: LPOS + */ + class LPosCommand extends KeyCommand { + + private final ByteBuffer element; + private final @Nullable Integer count; + private final @Nullable Integer rank; + + private LPosCommand(@Nullable ByteBuffer key, ByteBuffer element, @Nullable Integer count, @Nullable Integer rank) { + + super(key); + this.element = element; + this.count = count; + this.rank = rank; + } + + /** + * Creates a new {@link LPosCommand} for given {@literal element}. + * + * @param element + * @return a new {@link LPosCommand} for {@literal element}. + */ + public static LPosCommand lPosOf(ByteBuffer element) { + return new LPosCommand(null, element, null, null); + } + + /** + * Applies the {@literal key}. Constructs a new command instance with all previously configured properties. + * + * @param key must not be {@literal null}. + * @return a new {@link LPosCommand} with {@literal key} applied. + */ + public LPosCommand from(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + return new LPosCommand(key, element, count, rank); + } + + /** + * Applies the {@literal count} parameter specifying the number of matches to return. Constructs a new command + * instance with all previously configured properties. + * + * @param count can be {@literal null}. + * @return a new {@link LPosCommand} with {@literal count} applied. + */ + public LPosCommand count(Integer count) { + return new LPosCommand(getKey(), element, count, rank); + } + + /** + * Applies the {@literal rank} parameter specifying the "rank" of the first element to return. Constructs a new + * command instance with all previously configured properties. + * + * @param rank can be {@literal null}. + * @return a new {@link LPosCommand} with {@literal count} applied. + */ + public LPosCommand rank(Integer rank) { + return new LPosCommand(getKey(), element, count, rank); + } + + @Nullable + public Integer getCount() { + return count; + } + + @Nullable + public Integer getRank() { + return rank; + } + + @Nullable + public ByteBuffer getElement() { + return element; + } + } + + /** + * Get first index of the {@literal element} from list at {@literal key}. + * + * @param key must not be {@literal null}. + * @param element + * @return a {@link Mono} emitting the elements index. + * @since 2.4 + * @see Redis Documentation: LINDEX + */ + default Mono lPos(ByteBuffer key, ByteBuffer element) { + + Assert.notNull(key, "Key must not be null!"); + + return lPos(LPosCommand.lPosOf(element).from(key)).next(); + } + + /** + * Get indices of the {@literal element} from list at {@link LPosCommand#getKey()}. + * + * @param command must not be {@literal null}. + * @return a {@link Flux} emitting the elements indices one by one. + * @since 2.4 + * @see Redis Documentation: LINDEX + */ + default Flux lPos(LPosCommand command) { + return lPos(Mono.just(command)).map(NumericResponse::getOutput); + } + + /** + * Get indices of the {@literal element} from list at {@link LPosCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return a {@link Flux} emitting the elements indices one by one. + * @since 2.4 + * @see Redis Documentation: LINDEX + */ + Flux> lPos(Publisher commands); + /** * {@code LINDEX} command parameters. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java index 1ce9f9fe5..f55ef5571 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection; import java.util.List; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * List-specific commands supported by Redis. @@ -46,6 +47,36 @@ public interface RedisListCommands { @Nullable Long rPush(byte[] key, byte[]... values); + /** + * Returns the index of matching elements inside the list stored at given {@literal key}.
+ * Requires Redis 6.0.6. + * + * @param key must not be {@literal null}. + * @param element must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: LPOS + * @since 2.4 + */ + @Nullable + default Long lPos(byte[] key, byte[] element) { + return CollectionUtils.firstElement(lPos(key, element, null, null)); + } + + /** + * Returns the index of matching elements inside the list stored at given {@literal key}.
+ * Requires Redis 6.0.6. + * + * @param key must not be {@literal null}. + * @param element must not be {@literal null}. + * @param rank specifies the "rank" of the first element to return, in case there are multiple matches. A rank of 1 + * means to return the first match, 2 to return the second match, and so forth. + * @param count number of matches to return. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: LPOS + * @since 2.4 + */ + List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count); + /** * Prepend {@code values} to {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 8f8c44460..8f6be25cd 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -49,6 +49,7 @@ import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.lang.Nullable; +import org.springframework.util.CollectionUtils; /** * Convenience extension of {@link RedisConnection} that accepts and returns {@link String}s instead of byte arrays. @@ -685,6 +686,36 @@ public interface StringRedisConnection extends RedisConnection { */ Long rPush(String key, String... values); + /** + * Returns the index of matching elements inside the list stored at given {@literal key}.
+ * Requires Redis 6.0.6. + * + * @param key must not be {@literal null}. + * @param element must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: LPOS + * @since 2.4 + */ + @Nullable + default Long lPos(String key, String element) { + return CollectionUtils.firstElement(lPos(key, element, null, null)); + } + + /** + * Returns the index of matching elements inside the list stored at given {@literal key}.
+ * Requires Redis 6.0.6. + * + * @param key must not be {@literal null}. + * @param element must not be {@literal null}. + * @param rank specifies the "rank" of the first element to return, in case there are multiple matches. A rank of 1 + * means to return the first match, 2 to return the second match, and so forth. + * @param count number of matches to return. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: LPOS + * @since 2.4 + */ + List lPos(String key, String element, @Nullable Integer rank, @Nullable Integer count); + /** * Prepend {@code values} to {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java index 80a8faa7d..77a5202c3 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterListCommands.java @@ -20,9 +20,11 @@ import java.util.Collections; import java.util.List; import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.ClusterSlotHashUtil; import org.springframework.data.redis.connection.RedisListCommands; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisMultiKeyClusterCommandCallback; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -55,6 +57,19 @@ class JedisClusterListCommands implements RedisListCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#l{lPos(byte[], byte[], java.lang.Integer, java.lang.Integer) + */ + @Override + public List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(element, "Element must not be null!"); + + throw new InvalidDataAccessApiUsageException("LPOS is not supported by jedis."); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lPush(byte[], byte[][]) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java index 4346f0e1f..5c8d011bb 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisListCommands.java @@ -20,7 +20,9 @@ import redis.clients.jedis.Protocol; import java.util.ArrayList; import java.util.List; +import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.connection.RedisListCommands; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -60,6 +62,19 @@ class JedisListCommands implements RedisListCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#l{lPos(byte[], byte[], java.lang.Integer, java.lang.Integer) + */ + @Override + public List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(element, "Element must not be null!"); + + throw new InvalidDataAccessApiUsageException("LPOS is not supported by jedis."); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lPush(byte[], byte[][]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index 18bbe4bc4..95af8a25c 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -1136,6 +1136,7 @@ public class LettuceConnection extends AbstractRedisConnection { COMMAND_OUTPUT_TYPE_MAPPING.put(LINSERT, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(LLEN, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(LPUSH, IntegerOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(LPOS, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(LPUSHX, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(LREM, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(PTTL, IntegerOutput.class); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java index 7c33c2798..1d51b32a2 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceListCommands.java @@ -15,13 +15,16 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.LPosArgs; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; +import java.util.Collections; import java.util.List; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisListCommands; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,6 +64,47 @@ class LettuceListCommands implements RedisListCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#l{lPos(byte[], byte[], java.lang.Integer, java.lang.Integer) + */ + @Override + public List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Nullable Integer count) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(element, "Element must not be null!"); + + LPosArgs args = new LPosArgs(); + if (rank != null) { + args.rank(rank); + } + try { + if (isPipelined()) { + if (count != null) { + pipeline(connection.newLettuceResult(getAsyncConnection().lpos(key, element, count, args))); + } else { + pipeline(connection.newLettuceResult(getAsyncConnection().lpos(key, element, args))); + } + return null; + } + if (isQueueing()) { + if (count != null) { + transaction(connection.newLettuceResult(getAsyncConnection().lpos(key, element, count, args))); + } else { + transaction(connection.newLettuceResult(getAsyncConnection().lpos(key, element, args))); + } + return null; + } + if (count != null) { + return getConnection().lpos(key, element, count, args); + } + + return Collections.singletonList(getConnection().lpos(key, element, args)); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lPush(byte[], byte[][]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java index d3758e4f4..65602c0c0 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.LPosArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -146,8 +147,32 @@ class LettuceReactiveListCommands implements ReactiveListCommands { LettuceConverters.getLowerBoundIndex(range), // LettuceConverters.getUpperBoundIndex(range)); - return result - .map(LettuceConverters::stringToBoolean).map(value -> new BooleanResponse<>(command, value)); + return result.map(LettuceConverters::stringToBoolean).map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lPos(org.reactivestreams.Publisher) + */ + @Override + public Flux> lPos(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + LPosArgs args = new LPosArgs(); + if (command.getRank() != null) { + args.rank(command.getRank()); + } + + Flux values; + if (command.getCount() != null) { + values = cmd.lpos(command.getKey(), command.getElement(), command.getCount(), args); + } else { + values = cmd.lpos(command.getKey(), command.getElement(), args).flux(); + } + + return values.map(value -> new NumericResponse<>(command, value)); })); } @@ -238,7 +263,8 @@ class LettuceReactiveListCommands implements ReactiveListCommands { Assert.notNull(command.getDirection(), "Direction must not be null!"); Mono popResult = ObjectUtils.nullSafeEquals(Direction.RIGHT, command.getDirection()) - ? cmd.rpop(command.getKey()) : cmd.lpop(command.getKey()); + ? cmd.rpop(command.getKey()) + : cmd.lpop(command.getKey()); return popResult.map(value -> new ByteBufferResponse<>(command, value)); })); diff --git a/src/main/java/org/springframework/data/redis/core/BoundListOperations.java b/src/main/java/org/springframework/data/redis/core/BoundListOperations.java index fee5e34e7..7482da475 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundListOperations.java @@ -169,6 +169,28 @@ public interface BoundListOperations extends BoundKeyOperations { @Nullable V index(long index); + /** + * Returns the index of the first occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param value must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction or when not contained in list. + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Long indexOf(V value); + + /** + * Returns the index of the last occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param value must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction or when not contained in list. + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Long lastIndexOf(V value); + /** * Removes and returns first element in list stored at the bound key. * diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java index a999732db..8cd26003f 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java @@ -55,6 +55,24 @@ class DefaultBoundListOperations extends DefaultBoundKeyOperations impl return ops.index(getKey(), index); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundListOperations#indexOf(java.lang.Object) + */ + @Override + public Long indexOf(V value) { + return ops.indexOf(getKey(), value); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundListOperations#lastIndexOf(java.lang.Object) + */ + @Override + public Long lastIndexOf(V value) { + return ops.lastIndexOf(getKey(), value); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundListOperations#leftPop() diff --git a/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java index 3172a4a6b..c6126938e 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java @@ -53,6 +53,34 @@ class DefaultListOperations extends AbstractOperations implements Li }, true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ListOperations#indexOf(java.lang.Object, java.lang.Object) + */ + @Override + public Long indexOf(K key, V value) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(value); + return execute(connection -> connection.lPos(rawKey, rawValue), true); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ListOperations#lastIndexOf(java.lang.Object, java.lang.Object) + */ + @Override + public Long lastIndexOf(K key, V value) { + + byte[] rawKey = rawKey(key); + byte[] rawValue = rawValue(value); + return execute(connection -> { + + List indexes = connection.lPos(rawKey, rawValue, -1, null); + return CollectionUtils.firstElement(indexes); + }, true); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ListOperations#leftPop(java.lang.Object) diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java index 7aa2019b7..05897dab4 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java @@ -28,6 +28,7 @@ import java.util.function.Function; import org.reactivestreams.Publisher; import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.ReactiveListCommands.LPosCommand; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.util.Assert; @@ -232,6 +233,30 @@ class DefaultReactiveListOperations implements ReactiveListOperations connection.lIndex(rawKey(key), index).map(this::readValue)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#indexOf(java.lang.Object, java.lang.Object) + */ + @Override + public Mono indexOf(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lPos(rawKey(key), rawValue(value))); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#lastIndexOf(java.lang.Object, java.lang.Object) + */ + @Override + public Mono lastIndexOf(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lPos(LPosCommand.lPosOf(rawValue(value)).from(rawKey(key)).rank(-1))); + } + /* (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveListOperations#leftPop(java.lang.Object) */ diff --git a/src/main/java/org/springframework/data/redis/core/ListOperations.java b/src/main/java/org/springframework/data/redis/core/ListOperations.java index 015e87c32..851b98930 100644 --- a/src/main/java/org/springframework/data/redis/core/ListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ListOperations.java @@ -213,6 +213,30 @@ public interface ListOperations { @Nullable V index(K key, long index); + /** + * Returns the index of the first occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction or when not contained in list. + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Long indexOf(K key, V value); + + /** + * Returns the index of the last occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction or when not contained in list. + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Long lastIndexOf(K key, V value); + /** * Removes and returns first element in list stored at {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java index d12589a19..61204eee3 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java @@ -20,7 +20,6 @@ import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collection; -import java.util.List; /** * Redis list specific operations. @@ -197,6 +196,30 @@ public interface ReactiveListOperations { */ Mono index(K key, long index); + /** + * Returns the index of the first occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Mono indexOf(K key, V value); + + /** + * Returns the index of the last occurrence of the specified value in the list at at {@code key}.
+ * Requires Redis 6.0.6 + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + * @since 2.4 + * @see Redis Documentation: LPOS + */ + Mono lastIndexOf(K key, V value); + /** * Removes and returns first element in list stored at {@code key}. * 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 eedd0265a..864e28fc9 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 @@ -250,7 +250,9 @@ public class DefaultRedisList extends AbstractRedisCollection implements R */ @Override public int indexOf(Object o) { - throw new UnsupportedOperationException(); + + Long index = listOps.indexOf((E) o); + return index != null ? index.intValue() : -1; } /* @@ -259,7 +261,9 @@ public class DefaultRedisList extends AbstractRedisCollection implements R */ @Override public int lastIndexOf(Object o) { - throw new UnsupportedOperationException(); + + Long index = listOps.lastIndexOf((E) o); + return index != null ? index.intValue() : -1; } /* diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java index cf73f6397..7a75d97f9 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1479,6 +1479,72 @@ public abstract class AbstractConnectionIntegrationTests { verifyResults(Arrays.asList(new Object[] { 2l, Arrays.asList(new String[] { "baz", "bar" }) })); } + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPos() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c")); + + assertThat((Long) getResults().get(1)).isEqualTo(2); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPosRank() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c", 2, null)); + + assertThat((List) getResults().get(1)).containsExactly(6L); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPosNegativeRank() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c", -1, null)); + + assertThat((List) getResults().get(1)).containsExactly(7L); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPosCount() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c", null, 2)); + + assertThat((List) getResults().get(1)).containsExactly(2L, 6L); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPosRankCount() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c", -1, 2)); + + assertThat((List) getResults().get(1)).containsExactly(7L, 6L); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + @WithRedisDriver({ RedisDriver.LETTUCE }) + public void lPosCountZero() { + + actual.add(connection.rPush("mylist", "a", "b", "c", "1", "2", "3", "c", "c")); + actual.add(connection.lPos("mylist", "c", null, 0)); + + assertThat((List) getResults().get(1)).containsExactly(2L, 6L, 7L); + } + // Set operations @Test diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java index bb228e79b..c2136469d 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java @@ -23,19 +23,23 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.Arrays; +import org.junit.Rule; import org.junit.Test; - import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveListCommands.LPosCommand; import org.springframework.data.redis.connection.ReactiveListCommands.PopResult; import org.springframework.data.redis.connection.ReactiveListCommands.PushCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.data.redis.test.util.MinimumRedisVersionRule; +import org.springframework.test.annotation.IfProfileValue; /** * @author Christoph Strobl @@ -44,6 +48,8 @@ import org.springframework.data.redis.connection.RedisListCommands.Position; */ public class LettuceReactiveListCommandTests extends LettuceReactiveCommandsTestsBase { + @Rule public MinimumRedisVersionRule redisVersion = new MinimumRedisVersionRule(); + @Test // DATAREDIS-525 public void rPushShouldAppendValuesCorrectly() { @@ -304,4 +310,88 @@ public class LettuceReactiveListCommandTests extends LettuceReactiveCommandsTest assertThat(nativeCommands.llen(KEY_2)).isEqualTo(2L); assertThat(nativeCommands.lindex(KEY_2, 0)).isEqualTo(VALUE_3); } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPos() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands().lPos(KEY_1_BBUFFER, ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))) // + .as(StepVerifier::create) // + .expectNext(2L) // + .verifyComplete(); + + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPosRank() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands() + .lPos(LPosCommand.lPosOf(ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))).from(KEY_1_BBUFFER).rank(2)) // + .as(StepVerifier::create) // + .expectNext(6L) // + .verifyComplete(); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPosNegativeRank() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands() + .lPos(LPosCommand.lPosOf(ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))).from(KEY_1_BBUFFER).rank(-1)) // + .as(StepVerifier::create) // + .expectNext(7L) // + .verifyComplete(); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPosCount() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands() + .lPos(LPosCommand.lPosOf(ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))).from(KEY_1_BBUFFER).count(2)) // + .as(StepVerifier::create) // + .expectNext(2L) // + .expectNext(6L) // + .verifyComplete(); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPosRankCount() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands() + .lPos(LPosCommand.lPosOf(ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))).from(KEY_1_BBUFFER) + .from(KEY_1_BBUFFER).rank(-1).count(2)) // + .as(StepVerifier::create) // + .expectNext(7L) // + .expectNext(6L) // + .verifyComplete(); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lPosCountZero() { + + nativeCommands.rpush(KEY_1, "a", "b", "c", "1", "2", "3", "c", "c"); + + connection.listCommands() + .lPos(LPosCommand.lPosOf(ByteBuffer.wrap("c".getBytes(StandardCharsets.UTF_8))).from(KEY_1_BBUFFER).count(0)) // + .as(StepVerifier::create) // + .expectNext(2L) // + .expectNext(6L) // + .expectNext(7L) // + .verifyComplete(); + } + } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultListOperationsTests.java b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsTests.java index 79c18034d..aefc4651f 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultListOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsTests.java @@ -27,15 +27,18 @@ import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameters; - import org.springframework.data.redis.ConnectionFactoryTracker; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RedisTestProfileValueSource; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.test.util.MinimumRedisVersionRule; +import org.springframework.test.annotation.IfProfileValue; /** * Integration test of {@link DefaultListOperations} @@ -49,6 +52,8 @@ import org.springframework.data.redis.StringObjectFactory; @RunWith(Parameterized.class) public class DefaultListOperationsTests { + @Rule public MinimumRedisVersionRule redisVersion = new MinimumRedisVersionRule(); + private RedisTemplate redisTemplate; private ObjectFactory keyFactory; @@ -284,6 +289,8 @@ public class DefaultListOperationsTests { @SuppressWarnings("unchecked") public void testLeftPushAllCollection() { + assumeTrue(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory); + K key = keyFactory.instance(); V v1 = valueFactory.instance(); @@ -311,4 +318,35 @@ public class DefaultListOperationsTests { assertThatIllegalArgumentException() .isThrownBy(() -> listOps.leftPushAll(keyFactory.instance(), (Collection) null)); } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void indexOf() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + assertThat(listOps.rightPush(key, v1)).isEqualTo(Long.valueOf(1)); + assertThat(listOps.rightPush(key, v2)).isEqualTo(Long.valueOf(2)); + assertThat(listOps.rightPush(key, v1, v3)).isEqualTo(Long.valueOf(3)); + assertThat(listOps.indexOf(key, v1)).isEqualTo(0); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lastIndexOf() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + assertThat(listOps.rightPush(key, v1)).isEqualTo(Long.valueOf(1)); + assertThat(listOps.rightPush(key, v2)).isEqualTo(Long.valueOf(2)); + assertThat(listOps.rightPush(key, v1)).isEqualTo(Long.valueOf(3)); + assertThat(listOps.rightPush(key, v3)).isEqualTo(Long.valueOf(4)); + assertThat(listOps.lastIndexOf(key, v1)).isEqualTo(2); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java index 5ce8e3db8..ef454cfc4 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java @@ -24,6 +24,7 @@ import java.util.Collection; import org.junit.AfterClass; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -34,6 +35,8 @@ import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.test.util.MinimumRedisVersionRule; +import org.springframework.test.annotation.IfProfileValue; /** * Integration tests for {@link DefaultReactiveListOperations}. @@ -45,6 +48,8 @@ import org.springframework.data.redis.serializer.RedisSerializer; @SuppressWarnings("unchecked") public class DefaultReactiveListOperationsIntegrationTests { + @Rule public MinimumRedisVersionRule redisVersion = new MinimumRedisVersionRule(); + private final ReactiveRedisTemplate redisTemplate; private final ReactiveListOperations listOperations; @@ -366,6 +371,34 @@ public class DefaultReactiveListOperationsIntegrationTests { listOperations.index(key, 1).as(StepVerifier::create).expectNext(value2).verifyComplete(); } + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void indexOf() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOperations.rightPushAll(key, v1, v2, v1, v3).as(StepVerifier::create).expectNext(4L).verifyComplete(); + + listOperations.indexOf(key, v1).as(StepVerifier::create).expectNext(0L).verifyComplete(); + } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lastIndexOf() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + listOperations.rightPushAll(key, v1, v2, v1, v3).as(StepVerifier::create).expectNext(4L).verifyComplete(); + + listOperations.lastIndexOf(key, v1).as(StepVerifier::create).expectNext(2L).verifyComplete(); + } + @Test // DATAREDIS-602 public void leftPop() { diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListTests.java index 829abe5e8..f02c1f1cc 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisListTests.java @@ -24,11 +24,15 @@ import java.util.List; import java.util.NoSuchElementException; import java.util.concurrent.TimeUnit; +import org.assertj.core.api.Assumptions; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; - import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.test.util.MinimumRedisVersionRule; +import org.springframework.test.annotation.IfProfileValue; /** * Integration test for RedisList @@ -38,6 +42,8 @@ import org.springframework.data.redis.core.RedisTemplate; */ public abstract class AbstractRedisListTests extends AbstractRedisCollectionTests { + @Rule public MinimumRedisVersionRule redisVersion = new MinimumRedisVersionRule(); + protected RedisList list; /** @@ -54,6 +60,7 @@ public abstract class AbstractRedisListTests extends AbstractRedisCollectionT @SuppressWarnings("unchecked") @Before public void setUp() throws Exception { + super.setUp(); list = (RedisList) collection; } @@ -157,8 +164,12 @@ public abstract class AbstractRedisListTests extends AbstractRedisCollectionT assertThat(list.addAll(1, asList)).isTrue(); } - @Test(expected = UnsupportedOperationException.class) + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") public void testIndexOfObject() { + + Assumptions.assumeThat(template.getConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + T t1 = getT(); T t2 = getT(); @@ -168,7 +179,7 @@ public abstract class AbstractRedisListTests extends AbstractRedisCollectionT assertThat(list.indexOf(t2)).isEqualTo(-1); list.add(t2); - assertThat(list.indexOf(t1)).isEqualTo(1); + assertThat(list.indexOf(t2)).isEqualTo(1); } @Test @@ -519,4 +530,22 @@ public abstract class AbstractRedisListTests extends AbstractRedisCollectionT public void testTakeLast() { testPollLast(); } + + @Test // DATAREDIS-1196 + @IfProfileValue(name = "redisVersion", value = "6.0.6+") + public void lastIndexOf() { + + Assumptions.assumeThat(template.getConnectionFactory()).isInstanceOf(LettuceConnectionFactory.class); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t1); + list.add(t3); + + assertThat(list.lastIndexOf(t1)).isEqualTo(2); + } }