diff --git a/pom.xml b/pom.xml index 8657b1992a..4f6159d3f3 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,13 @@ - + 4.0.0 org.springframework.data spring-data-redis - 2.6.0-SNAPSHOT + 2.6.0-2039-SNAPSHOT Spring Data Redis diff --git a/src/main/asciidoc/new-features.adoc b/src/main/asciidoc/new-features.adoc index 920916f201..f8bd2db966 100644 --- a/src/main/asciidoc/new-features.adoc +++ b/src/main/asciidoc/new-features.adoc @@ -7,7 +7,7 @@ This section briefly covers items that are new and noteworthy in the latest rele == New in Spring Data Redis 2.6 * Support for `SubscriptionListener` when using `MessageListener` for subscription confirmation callbacks. `ReactiveRedisMessageListenerContainer` and `ReactiveRedisOperations` provide `receiveLater(…)` and `listenToLater(…)` methods to await until Redis acknowledges the subscription. -* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `COPY`, `GETEX`, `GETDEL`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`). +* Support Redis 6.2 commands (`LPOP`/`RPOP` with `count`, `LMOVE`/`BLMOVE`, `COPY`, `GETEX`, `GETDEL`, `ZPOPMIN`, `BZPOPMIN`, `ZPOPMAX`, `BZPOPMAX`, `ZMSCORE`, `ZDIFF`, `ZDIFFSTORE`, `ZINTER`, `ZUNION`). [[new-in-2.5.0]] == New in Spring Data Redis 2.5 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 34222cb937..a4ffda6ae4 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -731,6 +731,44 @@ public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { return convertAndReturn(delegate.lInsert(key, where, pivot, value), Converters.identityConverter()); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to) { + return convertAndReturn(delegate.lMove(sourceKey, destinationKey, from, to), Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bLMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction, double) + */ + @Override + public byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout) { + return convertAndReturn(delegate.bLMove(sourceKey, destinationKey, from, to, timeout), + Converters.identityConverter()); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#lMove(java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public String lMove(String sourceKey, String destinationKey, Direction from, Direction to) { + return convertAndReturn(delegate.lMove(serialize(sourceKey), serialize(destinationKey), from, to), bytesToString); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#bLMove(java.lang.String, java.lang.String, org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction, double) + */ + @Override + public String bLMove(String sourceKey, String destinationKey, Direction from, Direction to, double timeout) { + return convertAndReturn(delegate.bLMove(serialize(sourceKey), serialize(destinationKey), from, to, timeout), + bytesToString); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lLen(byte[]) 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 d7b2f346aa..fb792f3e9d 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -714,6 +714,20 @@ default Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { return listCommands().lInsert(key, where, pivot, value); } + /** @deprecated in favor of {@link RedisConnection#listCommands()}}. */ + @Override + @Deprecated + default byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to) { + return listCommands().lMove(sourceKey, destinationKey, from, to); + } + + /** @deprecated in favor of {@link RedisConnection#listCommands()}}. */ + @Override + @Deprecated + default byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout) { + return listCommands().bLMove(sourceKey, destinationKey, from, to, timeout); + } + /** @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 33516275bd..a04b97530a 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java @@ -51,7 +51,28 @@ public interface ReactiveListCommands { * @author Christoph Strobl */ enum Direction { - LEFT, RIGHT + + LEFT, RIGHT; + + /** + * Alias for {@link Direction#LEFT}. + * + * @since 2.6 + * @return + */ + public static Direction first() { + return LEFT; + } + + /** + * Alias for {@link Direction#RIGHT}. + * + * @since 2.6 + * @return + */ + public static Direction last() { + return RIGHT; + } } /** @@ -635,6 +656,189 @@ default Mono lInsert(ByteBuffer key, Position position, ByteBuffer pivot, */ Flux> lInsert(Publisher commands); + /** + * {@code LMOVE} command parameters. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + class LMoveCommand extends KeyCommand { + + private final @Nullable ByteBuffer destinationKey; + private final @Nullable Direction from; + private final @Nullable Direction to; + + public LMoveCommand(@Nullable ByteBuffer sourceKey, @Nullable ByteBuffer destinationKey, @Nullable Direction from, + @Nullable Direction to) { + super(sourceKey); + this.destinationKey = destinationKey; + this.from = from; + this.to = to; + } + + /** + * Creates a new {@link LMoveCommand} given a {@link ByteBuffer sourceKey}. + * + * @param sourceKey must not be {@literal null}. + * @param sourceDirection must not be {@literal null}. + * @return a new {@link LMoveCommand} for {@link ByteBuffer value}. + */ + public static LMoveCommand from(ByteBuffer sourceKey, Direction sourceDirection) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(sourceDirection, "Direction must not be null!"); + + return new LMoveCommand(sourceKey, null, sourceDirection, null); + } + + /** + * Applies the {@link ByteBuffer destinationKey}. Constructs a new command instance with all previously configured + * properties. + * + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @return a new {@link LMoveCommand} with {@literal pivot} applied. + */ + public LMoveCommand to(ByteBuffer destinationKey, Direction destinationDirection) { + + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(destinationDirection, "Direction must not be null!"); + + return new LMoveCommand(getKey(), destinationKey, from, destinationDirection); + } + + /** + * Applies the {@link Duration timeout}. Constructs a new command instance with all previously configured + * properties. + * + * @param timeout must not be {@literal null}. + * @return a new {@link LMoveCommand} with {@literal pivot} applied. + */ + public BLMoveCommand timeout(Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + + return new BLMoveCommand(getKey(), destinationKey, from, to, timeout); + } + + @Nullable + public ByteBuffer getDestinationKey() { + return destinationKey; + } + + @Nullable + public Direction getFrom() { + return from; + } + + @Nullable + public Direction getTo() { + return to; + } + } + + /** + * {@code BLMOVE} command parameters. + * + * @author Mark Paluch + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + class BLMoveCommand extends LMoveCommand { + + private final @Nullable Duration timeout; + + private BLMoveCommand(@Nullable ByteBuffer sourceKey, @Nullable ByteBuffer destinationKey, @Nullable Direction from, + @Nullable Direction to, @Nullable Duration timeout) { + super(sourceKey, destinationKey, from, to); + this.timeout = timeout; + } + + @Nullable + public Duration getTimeout() { + return timeout; + } + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + default Mono lMove(ByteBuffer sourceKey, ByteBuffer destinationKey, Direction from, Direction to) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return lMove(Mono.just(LMoveCommand.from(sourceKey, from).to(destinationKey, to))).map(CommandResponse::getOutput) + .next(); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + Flux> lMove(Publisher commands); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + default Mono bLMove(ByteBuffer sourceKey, ByteBuffer destinationKey, Direction from, Direction to, + Duration timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative!"); + + return bLMove(Mono.just(BLMoveCommand.from(sourceKey, from).to(destinationKey, to).timeout(timeout))) + .map(CommandResponse::getOutput).next(); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param commands must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + Flux> bLMove(Publisher commands); + /** * {@code LSET} 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 50a58ca103..fb7a887ed7 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisListCommands.java @@ -37,6 +37,33 @@ enum Position { BEFORE, AFTER } + /** + * List move direction. + * + * @since 2.6 + */ + enum Direction { + LEFT, RIGHT; + + /** + * Alias for {@link Direction#LEFT}. + * + * @return + */ + public static Direction first() { + return LEFT; + } + + /** + * Alias for {@link Direction#RIGHT}. + * + * @return + */ + public static Direction last() { + return RIGHT; + } + } + /** * Append {@code values} to {@code key}. * @@ -169,6 +196,43 @@ default Long lPos(byte[] key, byte[] element) { @Nullable Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value); + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: LMOVE + * @see #bLMove(byte[], byte[], Direction, Direction, double) + */ + @Nullable + byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + * @see #lMove(byte[], byte[], Direction, Direction) + */ + @Nullable + byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout); + /** * Set the {@code value} list element at {@code index}. * 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 469aeed494..9ead36c37f 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -846,6 +846,43 @@ default Long lPos(String key, String element) { */ Long lInsert(String key, Position where, String pivot, String value); + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: LMOVE + * @see #bLMove(byte[], byte[], Direction, Direction, double) + * @see #lMove(byte[], byte[], Direction, Direction) + */ + @Nullable + String lMove(String sourceKey, String destinationKey, Direction from, Direction to); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + * @see #lMove(byte[], byte[], Direction, Direction) + * @see #bLMove(byte[], byte[], Direction, Direction, double) + */ + @Nullable + String bLMove(String sourceKey, String destinationKey, Direction from, Direction to, double timeout); + /** * Set the {@code value} list element at {@code index}. * 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 38dd2aac2d..4467d37f54 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 @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.args.ListDirection; import redis.clients.jedis.params.LPosParams; import java.util.Arrays; @@ -220,6 +221,46 @@ public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + try { + return connection.getCluster().lmove(sourceKey, destinationKey, ListDirection.valueOf(from.name()), + ListDirection.valueOf(to.name())); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bLMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction, double) + */ + @Override + public byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + try { + return connection.getCluster().blmove(sourceKey, destinationKey, ListDirection.valueOf(from.name()), + ListDirection.valueOf(to.name()), timeout); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lSet(byte[], long, 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 c354e0c139..c3df142baa 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 @@ -18,6 +18,7 @@ import redis.clients.jedis.BinaryJedis; import redis.clients.jedis.MultiKeyPipelineBase; import redis.clients.jedis.Protocol; +import redis.clients.jedis.args.ListDirection; import redis.clients.jedis.params.LPosParams; import java.util.Collections; @@ -177,6 +178,38 @@ public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { JedisConverters.toListPosition(where), pivot, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return connection.invoke().just(BinaryJedis::lmove, MultiKeyPipelineBase::lmove, sourceKey, destinationKey, + ListDirection.valueOf(from.name()), ListDirection.valueOf(to.name())); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bLMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction, double) + */ + @Override + public byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return connection.invoke().just(BinaryJedis::blmove, MultiKeyPipelineBase::blmove, sourceKey, destinationKey, + ListDirection.valueOf(from.name()), ListDirection.valueOf(to.name()), timeout); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lSet(byte[], long, byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index f986127d21..48940eb089 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -47,6 +47,7 @@ import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.connection.RedisListCommands.Direction; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisNode.NodeType; @@ -1025,6 +1026,27 @@ static long getUpperBoundIndex(org.springframework.data.domain.Range range return getUpperBound(range).orElse(INDEXED_RANGE_END); } + static LMoveArgs toLmoveArgs(Enum from, Enum to) { + + if (from.name().equals(Direction.LEFT.name()) && to.name().equals(Direction.LEFT.name())) { + return LMoveArgs.Builder.leftLeft(); + } + + if (from.name().equals(Direction.LEFT.name()) && to.name().equals(Direction.RIGHT.name())) { + return LMoveArgs.Builder.leftRight(); + } + + if (from.name().equals(Direction.RIGHT.name()) && to.name().equals(Direction.LEFT.name())) { + return LMoveArgs.Builder.rightLeft(); + } + + if (from.name().equals(Direction.RIGHT.name()) && to.name().equals(Direction.RIGHT.name())) { + return LMoveArgs.Builder.rightRight(); + } + + throw new IllegalArgumentException(String.format("Unsupported combination of arguments: %s/%s", from, to)); + } + /** * @author Christoph Strobl * @since 1.8 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 bcd77c587b..47a6bde30b 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 @@ -72,7 +72,8 @@ public List lPos(byte[] key, byte[] element, @Nullable Integer rank, @Null return connection.invoke().just(RedisListAsyncCommands::lpos, key, element, count, args); } - return connection.invoke().from(RedisListAsyncCommands::lpos, key, element, args).getOrElse(Collections::singletonList, Collections::emptyList); + return connection.invoke().from(RedisListAsyncCommands::lpos, key, element, args) + .getOrElse(Collections::singletonList, Collections::emptyList); } /* @@ -176,6 +177,39 @@ public Long lInsert(byte[] key, Position where, byte[] pivot, byte[] value) { value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#lMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public byte[] lMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return connection.invoke().just(RedisListAsyncCommands::lmove, sourceKey, destinationKey, + LettuceConverters.toLmoveArgs(from, to)); + + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisListCommands#bLMove(byte[], byte[], org.springframework.data.redis.connection.RedisListCommands.Direction, org.springframework.data.redis.connection.RedisListCommands.Direction, double) + */ + @Override + public byte[] bLMove(byte[] sourceKey, byte[] destinationKey, Direction from, Direction to, double timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return connection.invoke(connection.getAsyncDedicatedConnection()).just(RedisListAsyncCommands::blmove, sourceKey, + destinationKey, LettuceConverters.toLmoveArgs(from, to), timeout); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisListCommands#lSet(byte[], long, 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 9fef5f6fc9..3341e4fb12 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.LMoveArgs; import io.lettuce.core.LPosArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -22,8 +23,10 @@ import java.nio.ByteBuffer; import java.time.temporal.ChronoUnit; import java.util.Arrays; +import java.util.concurrent.TimeUnit; import org.reactivestreams.Publisher; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.ReactiveListCommands; @@ -34,6 +37,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.data.redis.core.TimeoutUtils; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -212,6 +216,50 @@ public Flux> lInsert(Publisher> lMove(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Source key must not be null!"); + Assert.notNull(command.getFrom(), "Source direction must not be null!"); + Assert.notNull(command.getDestinationKey(), "Destination key must not be null!"); + Assert.notNull(command.getTo(), "Destination direction must not be null!"); + + LMoveArgs lMoveArgs = LettuceConverters.toLmoveArgs(command.getFrom(), command.getTo()); + + return cmd.lmove(command.getKey(), command.getDestinationKey(), lMoveArgs) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#bLMove(Publisher) + */ + @Override + public Flux> bLMove(Publisher commands) { + + return connection.executeDedicated(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Source key must not be null!"); + Assert.notNull(command.getFrom(), "Source direction must not be null!"); + Assert.notNull(command.getDestinationKey(), "Destination key must not be null!"); + Assert.notNull(command.getTo(), "Destination direction must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + LMoveArgs lMoveArgs = LettuceConverters.toLmoveArgs(command.getFrom(), command.getTo()); + double timeout = TimeoutUtils.toDoubleSeconds(command.getTimeout().toMillis(), TimeUnit.MILLISECONDS); + + return cmd.blmove(command.getKey(), command.getDestinationKey(), lMoveArgs, timeout) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveListCommands#lSet(org.reactivestreams.Publisher) 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 f73661b957..87aff4d080 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundListOperations.java @@ -19,6 +19,7 @@ import java.util.List; import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.connection.RedisListCommands.Direction; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -139,6 +140,58 @@ public interface BoundListOperations extends BoundKeyOperations { @Nullable Long rightPush(V pivot, V value); + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at the bound key, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + @Nullable + V move(Direction from, K destinationKey, Direction to); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at the bound key, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + @Nullable + V move(Direction from, K destinationKey, Direction to, Duration timeout); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at the bound key, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @param unit + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + @Nullable + V move(Direction from, K destinationKey, Direction to, long timeout, TimeUnit unit); + /** * Set the {@code value} list element at {@code index}. * 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 6fa5c0bb23..f06d5091d5 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundListOperations.java @@ -15,16 +15,18 @@ */ package org.springframework.data.redis.core; +import java.time.Duration; import java.util.List; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; -import org.springframework.lang.Nullable; +import org.springframework.data.redis.connection.RedisListCommands.Direction; /** * Default implementation for {@link BoundListOperations}. * * @author Costin Leau + * @author Mark Paluch */ class DefaultBoundListOperations extends DefaultBoundKeyOperations implements BoundListOperations { @@ -227,6 +229,33 @@ public Long rightPush(V pivot, V value) { return ops.rightPush(getKey(), pivot, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundListOperations#move(org.springframework.data.redis.connection.RedisListCommands.Direction, java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public V move(Direction from, K destinationKey, Direction to) { + return ops.move(getKey(), from, destinationKey, to); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundListOperations#move(org.springframework.data.redis.connection.RedisListCommands.Direction, java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction, java.time.Duration) + */ + @Override + public V move(Direction from, K destinationKey, Direction to, Duration timeout) { + return ops.move(getKey(), from, destinationKey, to, timeout); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundListOperations#move(org.springframework.data.redis.connection.RedisListCommands.Direction, java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit) + */ + @Override + public V move(Direction from, K destinationKey, Direction to, long timeout, TimeUnit unit) { + return ops.move(getKey(), from, destinationKey, to, timeout, unit); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundListOperations#trim(long, long) 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 f3026f303d..db434929d6 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultListOperations.java @@ -20,6 +20,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisListCommands.Direction; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.util.CollectionUtils; @@ -363,6 +364,40 @@ protected byte[] inRedis(byte[] rawSourceKey, RedisConnection connection) { }, true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ListOperations#move(java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction, java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public V move(K sourceKey, Direction from, K destinationKey, Direction to) { + + byte[] rawDestKey = rawKey(destinationKey); + return execute(new ValueDeserializingRedisCallback(sourceKey) { + + @Override + protected byte[] inRedis(byte[] rawSourceKey, RedisConnection connection) { + return connection.lMove(rawSourceKey, rawDestKey, from, to); + } + }, true); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ListOperations#move(java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction, java.lang.Object, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit) + */ + @Override + public V move(K sourceKey, Direction from, K destinationKey, Direction to, long timeout, TimeUnit unit) { + + byte[] rawDestKey = rawKey(destinationKey); + return execute(new ValueDeserializingRedisCallback(sourceKey) { + + @Override + protected byte[] inRedis(byte[] rawSourceKey, RedisConnection connection) { + return connection.bLMove(rawSourceKey, rawDestKey, from, to, TimeoutUtils.toDoubleSeconds(timeout, unit)); + } + }, true); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.ListOperations#set(java.lang.Object, long, 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 135e466513..adca75fb88 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 org.reactivestreams.Publisher; import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.ReactiveListCommands.Direction; import org.springframework.data.redis.connection.ReactiveListCommands.LPosCommand; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.serializer.RedisSerializationContext; @@ -199,6 +200,39 @@ public Mono rightPush(K key, V pivot, V value) { return createMono(connection -> connection.lInsert(rawKey(key), Position.AFTER, rawValue(pivot), rawValue(value))); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#move(K, Direction, K, Direction) + */ + @Override + public Mono move(K sourceKey, Direction from, K destinationKey, Direction to) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + + return createMono( + connection -> connection.lMove(rawKey(sourceKey), rawKey(destinationKey), from, to).map(this::readValue)); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#move(K, Direction, K, Direction, Duration) + */ + @Override + public Mono move(K sourceKey, Direction from, K destinationKey, Direction to, Duration timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(from, "From direction must not be null!"); + Assert.notNull(to, "To direction must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + + return createMono(connection -> connection.bLMove(rawKey(sourceKey), rawKey(destinationKey), from, to, timeout) + .map(this::readValue)); + } + /* (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveListOperations#set(java.lang.Object, long, 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 19125c491b..b7ca5e97ce 100644 --- a/src/main/java/org/springframework/data/redis/core/ListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ListOperations.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.core; +import static org.springframework.data.redis.connection.RedisListCommands.*; + import java.time.Duration; import java.util.Collection; import java.util.List; @@ -181,6 +183,168 @@ public interface ListOperations { @Nullable Long rightPush(K key, V pivot, V value); + /** + * Value object representing the {@code where from} part for the {@code LMOVE} command. + * + * @param + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + class MoveFrom { + + final K key; + final Direction direction; + + MoveFrom(K key, Direction direction) { + this.key = key; + this.direction = direction; + } + + public static MoveFrom fromHead(K key) { + return new MoveFrom<>(key, Direction.first()); + } + + public static MoveFrom fromTail(K key) { + return new MoveFrom<>(key, Direction.last()); + } + + } + + /** + * Value object representing the {@code where to} from part for the {@code LMOVE} command. + * + * @param + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + class MoveTo { + + final K key; + final Direction direction; + + MoveTo(K key, Direction direction) { + this.key = key; + this.direction = direction; + } + + public static MoveTo toHead(K key) { + return new MoveTo<>(key, Direction.first()); + } + + public static MoveTo toTail(K key) { + return new MoveTo<>(key, Direction.last()); + } + + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + @Nullable + default V move(MoveFrom from, MoveTo to) { + + Assert.notNull(from, "Move from must not be null"); + Assert.notNull(to, "Move to must not be null"); + + return move(from.key, from.direction, to.key, to.direction); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + @Nullable + V move(K sourceKey, Direction from, K destinationKey, Direction to); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout must not be {@literal null} or negative. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + @Nullable + default V move(MoveFrom from, MoveTo to, Duration timeout) { + + Assert.notNull(from, "Move from must not be null"); + Assert.notNull(to, "Move to must not be null"); + Assert.notNull(timeout, "Timeout must not be null"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative"); + + return move(from.key, from.direction, to.key, to.direction, + TimeoutUtils.toMillis(timeout.toMillis(), TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param sourceKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout must not be {@literal null} or negative. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + @Nullable + default V move(K sourceKey, Direction from, K destinationKey, Direction to, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative"); + + return move(sourceKey, from, destinationKey, to, TimeoutUtils.toMillis(timeout.toMillis(), TimeUnit.MILLISECONDS), + TimeUnit.MILLISECONDS); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param sourceKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @param unit + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + @Nullable + V move(K sourceKey, Direction from, K destinationKey, Direction to, long timeout, TimeUnit unit); + /** * Set the {@code value} list element at {@code index}. * 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 f9ba2fae58..efcb764c93 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java @@ -15,12 +15,18 @@ */ package org.springframework.data.redis.core; +import static org.springframework.data.redis.connection.ReactiveListCommands.*; + import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.time.Duration; import java.util.Collection; +import org.springframework.data.redis.core.ListOperations.MoveFrom; +import org.springframework.data.redis.core.ListOperations.MoveTo; +import org.springframework.util.Assert; + /** * Redis list specific operations. * @@ -165,6 +171,83 @@ public interface ReactiveListOperations { */ Mono rightPush(K key, V pivot, V value); + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + default Mono move(MoveFrom from, MoveTo to) { + + Assert.notNull(from, "Move from must not be null"); + Assert.notNull(to, "Move to must not be null"); + + return move(from.key, Direction.valueOf(from.direction.name()), to.key, Direction.valueOf(to.direction.name())); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + * + * @param sourceKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @return + * @since 2.6 + * @see Redis Documentation: LMOVE + */ + Mono move(K sourceKey, Direction from, K destinationKey, Direction to); + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + default Mono move(MoveFrom from, MoveTo to, Duration timeout) { + + Assert.notNull(from, "Move from must not be null"); + Assert.notNull(to, "Move to must not be null"); + Assert.notNull(timeout, "Timeout must not be null"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative"); + + return move(from.key, Direction.valueOf(from.direction.name()), to.key, Direction.valueOf(to.direction.name()), + timeout); + } + + /** + * Atomically returns and removes the first/last element (head/tail depending on the {@code from} argument) of the + * list stored at {@code sourceKey}, and pushes the element at the first/last element (head/tail depending on the + * {@code to} argument) of the list stored at {@code destinationKey}. + *

+ * Blocks connection until element available or {@code timeout} reached. + * + * @param sourceKey must not be {@literal null}. + * @param from must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param to must not be {@literal null}. + * @param timeout + * @return {@literal null} when used in pipeline / transaction. + * @since 2.6 + * @see Redis Documentation: BLMOVE + */ + Mono move(K sourceKey, Direction from, K destinationKey, Direction to, Duration timeout); + /** * Set the {@code value} list element at {@code index}. * diff --git a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java index f7d3e8e99b..9237d899ab 100644 --- a/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java +++ b/src/main/java/org/springframework/data/redis/core/TimeoutUtils.java @@ -64,7 +64,26 @@ public static long toSeconds(Duration duration) { */ public static long toSeconds(long timeout, TimeUnit unit) { return roundUpIfNecessary(timeout, unit.toSeconds(timeout)); + } + /** + * Converts the given timeout to seconds with a fraction of seconds. + * + * @param timeout The timeout to convert + * @param unit The timeout's unit + * @return The converted timeout + * @since 2.6 + */ + public static double toDoubleSeconds(long timeout, TimeUnit unit) { + + switch (unit) { + case MILLISECONDS: + case MICROSECONDS: + case NANOSECONDS: + return unit.toMillis(timeout) / 1000d; + default: + return unit.toSeconds(timeout); + } } /** 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 d8a67a0052..3ed6d3ab93 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 @@ -24,9 +24,11 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.RedisListCommands; import org.springframework.data.redis.core.BoundListOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Default implementation for {@link RedisList}. Suitable for not just lists, but also queues (FIFO ordering) or stacks @@ -36,6 +38,7 @@ * * @author Costin Leau * @author Christoph Strobl + * @author Mark Paluch */ public class DefaultRedisList extends AbstractRedisCollection implements RedisList { @@ -66,6 +69,18 @@ public DefaultRedisList(String key, RedisOperations operations) { this(operations.boundListOps(key)); } + /** + * Constructs a new {@link DefaultRedisList} instance. + * + * @param key Redis key of this list. + * @param operations {@link RedisOperations} for the value type of this list. + * @param maxSize + * @since 2.6 + */ + public DefaultRedisList(String key, RedisOperations operations, int maxSize) { + this(operations.boundListOps(key), maxSize); + } + /** * Constructs a new, uncapped {@link DefaultRedisList} instance. * @@ -100,6 +115,78 @@ public void setMaxSize(int maxSize) { capped = (maxSize > 0); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.support.collections.RedisList#moveFirstTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public E moveFirstTo(RedisList destination, RedisListCommands.Direction destinationPosition) { + + Assert.notNull(destination, "Destination must not be null"); + Assert.notNull(destinationPosition, "Destination position must not be null"); + + E result = listOps.move(RedisListCommands.Direction.first(), destination.getKey(), destinationPosition); + potentiallyCap(destination); + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.support.collections.RedisList#moveFirstTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit) + */ + @Override + public E moveFirstTo(RedisList destination, RedisListCommands.Direction destinationPosition, long timeout, + TimeUnit unit) { + + Assert.notNull(destination, "Destination must not be null"); + Assert.notNull(destinationPosition, "Destination position must not be null"); + Assert.notNull(unit, "TimeUnit must not be null"); + + E result = listOps.move(RedisListCommands.Direction.first(), destination.getKey(), destinationPosition, timeout, + unit); + potentiallyCap(destination); + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.support.collections.RedisList#moveLastTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction) + */ + @Override + public E moveLastTo(RedisList destination, RedisListCommands.Direction destinationPosition) { + + Assert.notNull(destination, "Destination must not be null"); + Assert.notNull(destinationPosition, "Destination position must not be null"); + + E result = listOps.move(RedisListCommands.Direction.last(), destination.getKey(), destinationPosition); + potentiallyCap(destination); + return result; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.support.collections.RedisList#moveLastTo(org.springframework.data.redis.support.collections.RedisList, org.springframework.data.redis.connection.RedisListCommands.Direction, long, java.util.concurrent.TimeUnit) + */ + @Override + public E moveLastTo(RedisList destination, RedisListCommands.Direction destinationPosition, long timeout, + TimeUnit unit) { + + Assert.notNull(destination, "Destination must not be null"); + Assert.notNull(destinationPosition, "Destination position must not be null"); + Assert.notNull(unit, "TimeUnit must not be null"); + + E result = listOps.move(RedisListCommands.Direction.last(), destination.getKey(), destinationPosition, timeout, + unit); + potentiallyCap(destination); + return result; + } + + private void potentiallyCap(RedisList destination) { + if (destination instanceof DefaultRedisList) { + ((DefaultRedisList) destination).cap(); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.support.collections.RedisList#range(long, long) @@ -119,6 +206,16 @@ public RedisList trim(int start, int end) { return this; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.support.collections.RedisList#trim(long, long) + */ + @Override + public RedisList trim(long start, long end) { + listOps.trim(start, end); + return this; + } + /* * (non-Javadoc) * @see java.util.AbstractCollection#iterator() diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java b/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java index bf3df48096..6e1053f478 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisCollectionFactoryBean.java @@ -106,7 +106,7 @@ public void afterPropertiesSet() { private RedisStore createStore(DataType dt) { switch (dt) { case LIST: - return new DefaultRedisList(key, template); + return RedisList.create(key, template); case SET: return new DefaultRedisSet(key, template); 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 5024441351..ee83e9183e 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,20 +15,218 @@ */ package org.springframework.data.redis.support.collections; +import static org.springframework.data.redis.connection.RedisListCommands.*; + +import java.time.Duration; import java.util.Deque; import java.util.List; import java.util.Queue; import java.util.concurrent.BlockingDeque; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.core.BoundListOperations; +import org.springframework.data.redis.core.RedisOperations; +import org.springframework.data.redis.core.TimeoutUtils; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * Redis extension for the {@link List} contract. Supports {@link List}, {@link Queue} and {@link Deque} contracts as * well as their equivalent blocking siblings {@link BlockingDeque} and {@link BlockingDeque}. * * @author Costin Leau + * @author Mark Paluch */ public interface RedisList extends RedisCollection, List, BlockingDeque { - List range(long begin, long end); + /** + * Constructs a new, uncapped {@link RedisList} instance. + * + * @param key Redis key of this list. + * @param operations {@link RedisOperations} for the value type of this list. + * @since 2.6 + */ + static RedisList create(String key, RedisOperations operations) { + return new DefaultRedisList<>(key, operations); + } + + /** + * Constructs a new {@link RedisList} instance. + * + * @param key Redis key of this list. + * @param operations {@link RedisOperations} for the value type of this list. + * @param maxSize + * @since 2.6 + */ + static RedisList create(String key, RedisOperations operations, int maxSize) { + return new DefaultRedisList<>(key, operations, maxSize); + } + + /** + * Constructs a new, uncapped {@link DefaultRedisList} instance. + * + * @param boundOps {@link BoundListOperations} for the value type of this list. + * @since 2.6 + */ + static RedisList create(BoundListOperations boundOps) { + return new DefaultRedisList<>(boundOps); + } + + /** + * Constructs a new {@link DefaultRedisList} instance. + * + * @param boundOps {@link BoundListOperations} for the value type of this list. + * @param maxSize + * @since 2.6 + */ + static RedisList create(BoundListOperations boundOps, int maxSize) { + return new DefaultRedisList<>(boundOps, maxSize); + } + + /** + * 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}. + * + * @param destination must not be {@literal null}. + * @param destinationPosition must not be {@literal null}. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @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 must not be {@literal null} or negative. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @Nullable + default E moveFirstTo(RedisList destination, Direction destinationPosition, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative"); + + return moveFirstTo(destination, destinationPosition, + 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 + * at {@link RedisList destination}. + * + * @param destination must not be {@literal null}. + * @param destinationPosition must not be {@literal null}. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @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 must not be {@literal null} or negative. + * @return + * @since 2.6 + * @see Direction#first() + * @see Direction#last() + */ + @Nullable + default E moveLastTo(RedisList destination, Direction destinationPosition, Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null"); + Assert.isTrue(!timeout.isNegative(), "Timeout must not be negative"); + + return moveLastTo(destination, destinationPosition, + 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. + * + * @param start + * @param end + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: LRANGE + */ + List range(long start, long end); + + /** + * Trim list at the bound key to elements between {@code start} and {@code end}. + * + * @param start + * @param end + * @see Redis Documentation: LTRIM + */ + RedisList trim(int start, int end); - RedisList trim(int begin, int end); + /** + * Trim list at the bound key to elements between {@code start} and {@code end}. + * + * @param start + * @param end + * @since 2.6 + * @see Redis Documentation: LTRIM + */ + RedisList trim(long start, long end); } 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 0aeb887d0a..4a35af22b7 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1359,6 +1359,39 @@ void testLLen() { verifyResults(Arrays.asList(new Object[] { 1L, 2L, 3L, 4L, 4L })); } + @Test // GH-2039 + @EnabledOnCommand("LMOVE") + void testLMove() { + + actual.add(connection.rPush("From", "hello")); + actual.add(connection.rPush("From", "big")); + actual.add(connection.rPush("From", "world")); + actual.add(connection.rPush("To", "bar")); + actual.add(connection.lMove("From", "To", RedisListCommands.Direction.LEFT, RedisListCommands.Direction.RIGHT)); + actual.add(connection.lRange("From", 0, -1)); + actual.add(connection.lRange("To", 0, -1)); + + verifyResults(Arrays.asList(1L, 2L, 3L, 1L, "hello", Arrays.asList("big", "world"), Arrays.asList("bar", "hello"))); + } + + @Test // GH-2039 + @EnabledOnCommand("BLMOVE") + void testBLMove() { + + actual.add(connection.rPush("From", "hello")); + actual.add(connection.rPush("From", "big")); + actual.add( + connection.bLMove("From", "To", RedisListCommands.Direction.LEFT, RedisListCommands.Direction.RIGHT, 0.01d)); + actual.add( + connection.bLMove("From", "To", RedisListCommands.Direction.LEFT, RedisListCommands.Direction.RIGHT, 0.01d)); + actual.add( + connection.bLMove("From", "To", RedisListCommands.Direction.LEFT, RedisListCommands.Direction.RIGHT, 0.01d)); + actual.add(connection.lRange("From", 0, -1)); + actual.add(connection.lRange("To", 0, -1)); + + verifyResults(Arrays.asList(1L, 2L, "hello", "big", null, Collections.emptyList(), Arrays.asList("hello", "big"))); + } + @Test void testLSet() { actual.add(connection.rPush("PopList", "hello")); @@ -1406,7 +1439,6 @@ void testRPopLPush() { actual.add(connection.lRange("PopList", 0, -1)); actual.add(connection.lRange("pop2", 0, -1)); verifyResults(Arrays.asList(1L, 2L, 1L, "world", Arrays.asList("hello"), Arrays.asList("world", "hey"))); - } @Test diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index f8c1d7b1f7..0d4f0a5cd8 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -288,6 +288,12 @@ public interface ClusterConnectionTests { // DATAREDIS-315 void lInsertShouldAddElementAtPositionCorrectly(); + // GH-2039 + void lMoveShouldMoveElementsCorrectly(); + + // GH-2039 + void blMoveShouldMoveElementsCorrectly(); + // DATAREDIS-315 void lLenShouldCountValuesCorrectly(); diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 158e1c6997..59eed9488f 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -23,6 +23,7 @@ import static org.springframework.data.redis.connection.ClusterTestVariables.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import static org.springframework.data.redis.connection.RedisListCommands.*; import static org.springframework.data.redis.connection.RedisZSetCommands.*; import static org.springframework.data.redis.core.ScanOptions.*; @@ -59,12 +60,12 @@ import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; -import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.ValueEncoding.RedisValueEncoding; +import org.springframework.data.redis.connection.RedisListCommands.*; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.script.DigestUtils; @@ -1031,6 +1032,39 @@ public void lInsertShouldAddElementAtPositionCorrectly() { assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(2)).isEqualTo("booh!"); } + @Test // GH-2039 + @EnabledOnCommand("LMOVE") + public void lMoveShouldMoveElementsCorrectly() { + + nativeConnection.rpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT)) + .isEqualTo(VALUE_3_BYTES); + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT)) + .isEqualTo(VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_1, 0, -1)).containsExactly(VALUE_1); + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_2, VALUE_3); + } + + @Test // GH-2039 + @EnabledOnCommand("BLMOVE") + public void blMoveShouldMoveElementsCorrectly() { + + nativeConnection.rpush(SAME_SLOT_KEY_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT)) + .isEqualTo(VALUE_3_BYTES); + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT)) + .isEqualTo(VALUE_2_BYTES); + assertThat( + clusterConnection.bLMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, Direction.RIGHT, Direction.LEFT, 0.01)) + .isNull(); + + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_1, 0, -1)).isEmpty(); + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_2, VALUE_3); + } + @Test // DATAREDIS-315 public void lLenShouldCountValuesCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index acb2aa4cf9..5a85ac6376 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -1067,6 +1067,38 @@ public void lInsertShouldAddElementAtPositionCorrectly() { assertThat(nativeConnection.lrange(KEY_1, 0, -1).get(2)).isEqualTo("booh!"); } + @Test // GH-2039 + @EnabledOnCommand("LMOVE") + public void lMoveShouldMoveElementsCorrectly() { + + nativeConnection.rpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, RedisListCommands.Direction.RIGHT, + RedisListCommands.Direction.LEFT)).isEqualTo(VALUE_3_BYTES); + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, RedisListCommands.Direction.RIGHT, + RedisListCommands.Direction.LEFT)).isEqualTo(VALUE_2_BYTES); + + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_1, 0, -1)).containsExactly(VALUE_1); + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_2, VALUE_3); + } + + @Test // GH-2039 + @EnabledOnCommand("BLMOVE") + public void blMoveShouldMoveElementsCorrectly() { + + nativeConnection.rpush(SAME_SLOT_KEY_1, VALUE_2, VALUE_3); + + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, RedisListCommands.Direction.RIGHT, + RedisListCommands.Direction.LEFT)).isEqualTo(VALUE_3_BYTES); + assertThat(clusterConnection.lMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, RedisListCommands.Direction.RIGHT, + RedisListCommands.Direction.LEFT)).isEqualTo(VALUE_2_BYTES); + assertThat(clusterConnection.bLMove(SAME_SLOT_KEY_1_BYTES, SAME_SLOT_KEY_2_BYTES, RedisListCommands.Direction.RIGHT, + RedisListCommands.Direction.LEFT, 0.01)).isNull(); + + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_1, 0, -1)).isEmpty(); + assertThat(nativeConnection.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_2, VALUE_3); + } + @Test // DATAREDIS-315 public void lLenShouldCountValuesCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java index 282319dff3..7b30035960 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandIntegrationTests.java @@ -29,6 +29,7 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveListCommands; import org.springframework.data.redis.connection.ReactiveListCommands.LPosCommand; import org.springframework.data.redis.connection.ReactiveListCommands.PopResult; import org.springframework.data.redis.connection.ReactiveListCommands.PushCommand; @@ -201,6 +202,38 @@ void lInsertShouldAddValueCorrectlyAfterExisting() { assertThat(nativeCommands.lrange(KEY_1, 0, -1)).containsExactly(VALUE_1, VALUE_2, VALUE_3); } + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void lMoveShouldMoveValueCorrectly() { + + nativeCommands.rpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.rpush(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + connection.listCommands().lMove(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER, + ReactiveListCommands.Direction.RIGHT, ReactiveListCommands.Direction.LEFT).as(StepVerifier::create) + .expectNext(VALUE_3_BBUFFER).verifyComplete(); + assertThat(nativeCommands.lrange(SAME_SLOT_KEY_1, 0, -1)).containsExactly(VALUE_1, VALUE_2); + assertThat(nativeCommands.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_3, VALUE_2, VALUE_3); + } + + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void blMoveShouldMoveValueCorrectly() { + + nativeCommands.rpush(SAME_SLOT_KEY_1, VALUE_3); + nativeCommands.rpush(SAME_SLOT_KEY_2, VALUE_2, VALUE_3); + + connection.listCommands() + .bLMove(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER, ReactiveListCommands.Direction.RIGHT, + ReactiveListCommands.Direction.LEFT, Duration.ofMillis(10)) + .as(StepVerifier::create).expectNext(VALUE_3_BBUFFER).verifyComplete(); + connection.listCommands().bLMove(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER, + ReactiveListCommands.Direction.RIGHT, ReactiveListCommands.Direction.LEFT, Duration.ofMillis(10)) + .as(StepVerifier::create).verifyComplete(); + assertThat(nativeCommands.lrange(SAME_SLOT_KEY_1, 0, -1)).isEmpty(); + assertThat(nativeCommands.lrange(SAME_SLOT_KEY_2, 0, -1)).containsExactly(VALUE_3, VALUE_2, VALUE_3); + } + @ParameterizedRedisTest // DATAREDIS-525 void lSetSouldSetValueCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java index 403104e152..8e6d1b360e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultListOperationsIntegrationIntegrationTests.java @@ -267,7 +267,6 @@ void rightPushAllShouldThrowExceptionWhenCalledWithNull() { } @ParameterizedRedisTest // DATAREDIS-288 - void testLeftPushAllCollection() { assumeThat(redisTemplate.getConnectionFactory() instanceof LettuceConnectionFactory).isTrue(); @@ -300,6 +299,53 @@ void leftPushAllShouldThrowExceptionWhenCalledWithNull() { .isThrownBy(() -> listOps.leftPushAll(keyFactory.instance(), (Collection) null)); } + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void move() { + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + V v4 = valueFactory.instance(); + + listOps.rightPushAll(source, v1, v2, v3, v4); + + assertThat(listOps.move(ListOperations.MoveFrom.fromHead(source), ListOperations.MoveTo.toTail(target))) + .isEqualTo(v1); + assertThat(listOps.move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target))) + .isEqualTo(v4); + + assertThat(listOps.range(source, 0, -1)).containsExactly(v2, v3); + assertThat(listOps.range(target, 0, -1)).containsExactly(v4, v1); + } + + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("BLMOVE") + void moveWithTimeout() { + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + + V v1 = valueFactory.instance(); + V v4 = valueFactory.instance(); + + listOps.rightPushAll(source, v1, v4); + + assertThat(listOps.move(ListOperations.MoveFrom.fromHead(source), ListOperations.MoveTo.toTail(target))) + .isEqualTo(v1); + assertThat(listOps.move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target))) + .isEqualTo(v4); + + assertThat(listOps.move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target), + Duration.ofMillis(10))).isNull(); + + assertThat(listOps.range(source, 0, -1)).isEmpty(); + assertThat(listOps.range(target, 0, -1)).containsExactly(v4, v1); + } + @ParameterizedRedisTest // DATAREDIS-1196 @EnabledOnCommand("LPOS") void indexOf() { 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 14224aa355..1c1d94ca7a 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java @@ -45,7 +45,6 @@ @SuppressWarnings("unchecked") public class DefaultReactiveListOperationsIntegrationTests { - private final ReactiveRedisTemplate redisTemplate; private final ReactiveListOperations listOperations; @@ -296,6 +295,71 @@ void rightPushWithPivot() { .verifyComplete(); } + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void move() { + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + V v4 = valueFactory.instance(); + + listOperations.rightPushAll(source, v1, v2, v3, v4).as(StepVerifier::create).expectNext(4L).verifyComplete(); + + listOperations.move(ListOperations.MoveFrom.fromHead(source), ListOperations.MoveTo.toTail(target)) + .as(StepVerifier::create).expectNext(v1).verifyComplete(); + listOperations.move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target)) + .as(StepVerifier::create).expectNext(v4).verifyComplete(); + + listOperations.range(source, 0, -1) // + .as(StepVerifier::create) // + .expectNext(v2) // + .expectNext(v3) // + .verifyComplete(); + + listOperations.range(target, 0, -1) // + .as(StepVerifier::create) // + .expectNext(v4) // + .expectNext(v1) // + .verifyComplete(); + } + + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("BLMOVE") + void moveWithTimeout() { + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + + V v1 = valueFactory.instance(); + V v4 = valueFactory.instance(); + + listOperations.rightPushAll(source, v1, v4).as(StepVerifier::create).expectNext(2L).verifyComplete(); + + listOperations + .move(ListOperations.MoveFrom.fromHead(source), ListOperations.MoveTo.toTail(target), Duration.ofMillis(10)) + .as(StepVerifier::create).expectNext(v1).verifyComplete(); + listOperations + .move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target), Duration.ofMillis(10)) + .as(StepVerifier::create).expectNext(v4).verifyComplete(); + listOperations + .move(ListOperations.MoveFrom.fromTail(source), ListOperations.MoveTo.toHead(target), Duration.ofMillis(10)) + .as(StepVerifier::create).verifyComplete(); + + listOperations.range(source, 0, -1) // + .as(StepVerifier::create) // + .verifyComplete(); + + listOperations.range(target, 0, -1) // + .as(StepVerifier::create) // + .expectNext(v4) // + .expectNext(v1) // + .verifyComplete(); + } + @ParameterizedRedisTest // DATAREDIS-602 void set() { diff --git a/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java b/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java index bb9dced6c7..59cf7226e8 100644 --- a/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java +++ b/src/test/java/org/springframework/data/redis/support/BoundKeyParams.java @@ -26,7 +26,6 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.support.atomic.RedisAtomicInteger; import org.springframework.data.redis.support.atomic.RedisAtomicLong; -import org.springframework.data.redis.support.collections.DefaultRedisList; import org.springframework.data.redis.support.collections.DefaultRedisMap; import org.springframework.data.redis.support.collections.DefaultRedisSet; import org.springframework.data.redis.support.collections.RedisList; @@ -47,7 +46,7 @@ public static Collection testParams() { StringRedisTemplate templateJS = new StringRedisTemplate(jedisConnFactory); DefaultRedisMap mapJS = new DefaultRedisMap("bound:key:map", templateJS); DefaultRedisSet setJS = new DefaultRedisSet("bound:key:set", templateJS); - RedisList list = new DefaultRedisList("bound:key:list", templateJS); + RedisList list = RedisList.create("bound:key:list", templateJS); // Lettuce LettuceConnectionFactory lettuceConnFactory = LettuceConnectionFactoryExtension @@ -56,7 +55,7 @@ public static Collection testParams() { StringRedisTemplate templateLT = new StringRedisTemplate(lettuceConnFactory); DefaultRedisMap mapLT = new DefaultRedisMap("bound:key:mapLT", templateLT); DefaultRedisSet setLT = new DefaultRedisSet("bound:key:setLT", templateLT); - RedisList listLT = new DefaultRedisList("bound:key:listLT", templateLT); + RedisList listLT = RedisList.create("bound:key:listLT", templateLT); StringObjectFactory sof = new StringObjectFactory(); 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 9ba583fd70..6b3b6cc903 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 @@ -28,6 +28,7 @@ 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; @@ -38,6 +39,7 @@ * * @author Costin Leau * @author Jennifer Hickey + * @author Mark Paluch */ public abstract class AbstractRedisListIntegrationTests extends AbstractRedisCollectionIntegrationTests { @@ -238,6 +240,48 @@ void testRemove() { assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(list::remove); } + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void testMoveFirstTo() { + + RedisList target = new DefaultRedisList(template.boundListOps(collection.getKey() + ":target")); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t3); + + assertThat(list.moveFirstTo(target, RedisListCommands.Direction.first())).isEqualTo(t1); + assertThat(list.moveFirstTo(target, RedisListCommands.Direction.first())).isEqualTo(t2); + assertThat(list.moveFirstTo(target, RedisListCommands.Direction.last())).isEqualTo(t3); + assertThat(list).isEmpty(); + assertThat(target).hasSize(3).containsSequence(t2, t1, t3); + } + + @ParameterizedRedisTest // GH-2039 + @EnabledOnCommand("LMOVE") + void testMoveLastTo() { + + RedisList target = new DefaultRedisList(template.boundListOps(collection.getKey() + ":target")); + + T t1 = getT(); + T t2 = getT(); + T t3 = getT(); + + list.add(t1); + list.add(t2); + list.add(t3); + + assertThat(list.moveLastTo(target, RedisListCommands.Direction.first())).isEqualTo(t3); + assertThat(list.moveLastTo(target, RedisListCommands.Direction.first())).isEqualTo(t2); + assertThat(list.moveLastTo(target, RedisListCommands.Direction.last())).isEqualTo(t1); + assertThat(list).isEmpty(); + assertThat(target).hasSize(3).containsSequence(t2, t3, t1); + } + @ParameterizedRedisTest void testRange() { T t1 = getT(); @@ -275,9 +319,10 @@ void testTrim() { list.add(t1); list.add(t2); assertThat(list).hasSize(2); - assertThat(list.trim(0, 0)).hasSize(1); + assertThat(list.trim(0L, 0L)).hasSize(1); assertThat(list).hasSize(1); assertThat(list.get(0)).isEqualTo(t1); + assertThat(list).hasSize(1); } @SuppressWarnings("unchecked") 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 06d2baa697..251975e178 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 @@ -36,7 +36,7 @@ public RedisListIntegrationTests(ObjectFactory factory, RedisTemplate(store.getKey(), store.getOperations()); + return RedisList.create(store.getKey(), store.getOperations()); } AbstractRedisCollection createCollection() {