diff --git a/src/main/antora/modules/ROOT/pages/appendix.adoc b/src/main/antora/modules/ROOT/pages/appendix.adoc index 46feff061..669bf8220 100644 --- a/src/main/antora/modules/ROOT/pages/appendix.adoc +++ b/src/main/antora/modules/ROOT/pages/appendix.adoc @@ -8,193 +8,201 @@ link:https://www.springframework.org/schema/redis/spring-redis-1.0.xsd[Spring Da [[supported-commands]] == Supported Commands + .Redis commands supported by `RedisTemplate` [width="50%",cols="<2,^1",options="header"] |========================================================= |Command |Template Support -|APPEND |X -|AUTH |X -|BGREWRITEAOF |X -|BGSAVE |X -|BITCOUNT |X -|BITFIELD |X -|BITOP |X -|BLPOP |X -|BRPOP |X -|BRPOPLPUSH |X -|CLIENT KILL |X -|CLIENT GETNAME |X -|CLIENT LIST |X -|CLIENT SETNAME |X -|CLUSTER SLOTS |- -|COMMAND |- -|COMMAND COUNT |- -|COMMAND GETKEYS |- -|COMMAND INFO |- -|CONFIG GET |X -|CONFIG RESETSTAT |X -|CONFIG REWRITE |- -|CONFIG SET |X -|DBSIZE |X -|DEBUG OBJECT |- -|DEBUG SEGFAULT |- -|DECR |X -|DECRBY |X -|DEL |X -|DISCARD |X -|DUMP |X -|ECHO |X -|EVAL |X -|EVALSHA |X -|EXEC |X -|EXISTS |X -|EXPIRE |X -|EXPIREAT |X -|FLUSHALL |X -|FLUSHDB |X -|GEOADD |X -|GEODIST |X -|GEOHASH |X -|GEOPOS |X -|GEORADIUS |X -|GEORADIUSBYMEMBER |X -|GEOSEARCH |X -|GEOSEARCHSTORE |X -|GET |X -|GETBIT |X -|GETRANGE |X -|GETSET |X -|HDEL |X -|HEXISTS |X -|HGET |X -|HGETALL |X -|HINCRBY |X -|HINCRBYFLOAT |X -|HKEYS |X -|HLEN |X -|HMGET |X -|HMSET |X -|HSCAN |X -|HSET |X -|HSETNX |X -|HVALS |X -|INCR |X -|INCRBY |X -|INCRBYFLOAT |X -|INFO |X -|KEYS |X -|LASTSAVE |X -|LINDEX |X -|LINSERT |X -|LLEN |X -|LPOP |X -|LPUSH |X -|LPUSHX |X -|LRANGE |X -|LREM |X -|LSET |X -|LTRIM |X -|MGET |X -|MIGRATE |- -|MONITOR |- -|MOVE |X -|MSET |X -|MSETNX |X -|MULTI |X -|OBJECT |- -|PERSIST |X -|PEXIPRE |X -|PEXPIREAT |X -|PFADD |X -|PFCOUNT |X -|PFMERGE |X -|PING |X -|PSETEX |X -|PSUBSCRIBE |X -|PTTL |X -|PUBLISH |X -|PUBSUB |- -|PUBSUBSCRIBE |- -|QUIT |X -|RANDOMKEY |X -|RENAME |X -|RENAMENX |X -|REPLICAOF |X -|RESTORE |X -|ROLE |- -|RPOP |X -|RPOPLPUSH |X -|RPUSH |X -|RPUSHX |X -|SADD |X -|SAVE |X -|SCAN |X -|SCARD |X -|SCRIPT EXITS |X -|SCRIPT FLUSH |X -|SCRIPT KILL |X -|SCRIPT LOAD |X -|SDIFF |X -|SDIFFSTORE |X -|SELECT |X -|SENTINEL FAILOVER |X +|APPEND |X +|AUTH |X +|BGREWRITEAOF |X +|BGSAVE |X +|BITCOUNT |X +|BITFIELD |X +|BITOP |X +|BLPOP |X +|BRPOP |X +|BRPOPLPUSH |X +|CLIENT KILL |X +|CLIENT GETNAME |X +|CLIENT LIST |X +|CLIENT SETNAME |X +|CLUSTER SLOTS |- +|COMMAND |- +|COMMAND COUNT |- +|COMMAND GETKEYS |- +|COMMAND INFO |- +|CONFIG GET |X +|CONFIG RESETSTAT |X +|CONFIG REWRITE |- +|CONFIG SET |X +|DBSIZE |X +|DEBUG OBJECT |- +|DEBUG SEGFAULT |- +|DECR |X +|DECRBY |X +|DEL |X +|DISCARD |X +|DUMP |X +|ECHO |X +|EVAL |X +|EVALSHA |X +|EXEC |X +|EXISTS |X +|EXPIRE |X +|EXPIREAT |X +|FLUSHALL |X +|FLUSHDB |X +|GEOADD |X +|GEODIST |X +|GEOHASH |X +|GEOPOS |X +|GEORADIUS |X +|GEORADIUSBYMEMBER |X +|GEOSEARCH |X +|GEOSEARCHSTORE |X +|GET |X +|GETBIT |X +|GETRANGE |X +|GETSET |X +|HDEL |X +|HEXISTS |X +|HEXPIRE |X +|HEXPIREAT |X +|HPEXPIRE |X +|HPEXPIREAT |X +|HPERSIST |X +|HTTL |X +|HPTTL |X +|HGET |X +|HGETALL |X +|HINCRBY |X +|HINCRBYFLOAT |X +|HKEYS |X +|HLEN |X +|HMGET |X +|HMSET |X +|HSCAN |X +|HSET |X +|HSETNX |X +|HVALS |X +|INCR |X +|INCRBY |X +|INCRBYFLOAT |X +|INFO |X +|KEYS |X +|LASTSAVE |X +|LINDEX |X +|LINSERT |X +|LLEN |X +|LPOP |X +|LPUSH |X +|LPUSHX |X +|LRANGE |X +|LREM |X +|LSET |X +|LTRIM |X +|MGET |X +|MIGRATE |- +|MONITOR |- +|MOVE |X +|MSET |X +|MSETNX |X +|MULTI |X +|OBJECT |- +|PERSIST |X +|PEXIPRE |X +|PEXPIREAT |X +|PFADD |X +|PFCOUNT |X +|PFMERGE |X +|PING |X +|PSETEX |X +|PSUBSCRIBE |X +|PTTL |X +|PUBLISH |X +|PUBSUB |- +|PUBSUBSCRIBE |- +|QUIT |X +|RANDOMKEY |X +|RENAME |X +|RENAMENX |X +|REPLICAOF |X +|RESTORE |X +|ROLE |- +|RPOP |X +|RPOPLPUSH |X +|RPUSH |X +|RPUSHX |X +|SADD |X +|SAVE |X +|SCAN |X +|SCARD |X +|SCRIPT EXITS |X +|SCRIPT FLUSH |X +|SCRIPT KILL |X +|SCRIPT LOAD |X +|SDIFF |X +|SDIFFSTORE |X +|SELECT |X +|SENTINEL FAILOVER |X |SENTINEL GET-MASTER-ADD-BY-NAME |- -|SENTINEL MASTER | - -|SENTINEL MASTERS |X -|SENTINEL MONITOR |X -|SENTINEL REMOVE |X -|SENTINEL RESET |- -|SENTINEL SET |- -|SENTINEL SLAVES |X -|SET |X -|SETBIT |X -|SETEX |X -|SETNX |X -|SETRANGE |X -|SHUTDOWN |X -|SINTER |X -|SINTERSTORE |X -|SISMEMBER |X -|SLAVEOF |X -|SLOWLOG |- -|SMEMBERS |X -|SMOVE |X -|SORT |X -|SPOP |X -|SRANDMEMBER |X -|SREM |X -|SSCAN |X -|STRLEN |X -|SUBSCRIBE |X -|SUNION |X -|SUNIONSTORE |X -|SYNC |- -|TIME |X -|TTL |X -|TYPE |X -|UNSUBSCRIBE |X -|UNWATCH |X -|WATCH |X -|ZADD |X -|ZCARD |X -|ZCOUNT |X -|ZINCRBY |X -|ZINTERSTORE |X -|ZLEXCOUNT |- -|ZRANGE |X -|ZRANGEBYLEX |- -|ZREVRANGEBYLEX |- -|ZRANGEBYSCORE |X -|ZRANGESTORE |X -|ZRANK |X -|ZREM |X -|ZREMRANGEBYLEX |- -|ZREMRANGEBYRANK |X -|ZREVRANGE |X -|ZREVRANGEBYSCORE |X -|ZREVRANK |X -|ZSCAN |X -|ZSCORE |X -|ZUNINONSTORE |X +|SENTINEL MASTER | - +|SENTINEL MASTERS |X +|SENTINEL MONITOR |X +|SENTINEL REMOVE |X +|SENTINEL RESET |- +|SENTINEL SET |- +|SENTINEL SLAVES |X +|SET |X +|SETBIT |X +|SETEX |X +|SETNX |X +|SETRANGE |X +|SHUTDOWN |X +|SINTER |X +|SINTERSTORE |X +|SISMEMBER |X +|SLAVEOF |X +|SLOWLOG |- +|SMEMBERS |X +|SMOVE |X +|SORT |X +|SPOP |X +|SRANDMEMBER |X +|SREM |X +|SSCAN |X +|STRLEN |X +|SUBSCRIBE |X +|SUNION |X +|SUNIONSTORE |X +|SYNC |- +|TIME |X +|TTL |X +|TYPE |X +|UNSUBSCRIBE |X +|UNWATCH |X +|WATCH |X +|ZADD |X +|ZCARD |X +|ZCOUNT |X +|ZINCRBY |X +|ZINTERSTORE |X +|ZLEXCOUNT |- +|ZRANGE |X +|ZRANGEBYLEX |- +|ZREVRANGEBYLEX |- +|ZRANGEBYSCORE |X +|ZRANGESTORE |X +|ZRANK |X +|ZREM |X +|ZREMRANGEBYLEX |- +|ZREMRANGEBYRANK |X +|ZREVRANGE |X +|ZREVRANGEBYSCORE |X +|ZREVRANK |X +|ZSCAN |X +|ZSCORE |X +|ZUNINONSTORE |X |========================================================= 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 c3f13ef4a..831a46ece 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -23,6 +23,7 @@ import java.util.function.IntFunction; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.springframework.core.convert.converter.Converter; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; @@ -35,10 +36,19 @@ import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.convert.ListConverter; import org.springframework.data.redis.connection.convert.MapConverter; import org.springframework.data.redis.connection.convert.SetConverter; -import org.springframework.data.redis.connection.stream.*; +import org.springframework.data.redis.connection.stream.ByteRecord; +import org.springframework.data.redis.connection.stream.Consumer; +import org.springframework.data.redis.connection.stream.MapRecord; +import org.springframework.data.redis.connection.stream.PendingMessages; +import org.springframework.data.redis.connection.stream.PendingMessagesSummary; +import org.springframework.data.redis.connection.stream.ReadOffset; +import org.springframework.data.redis.connection.stream.RecordId; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoConsumers; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoGroups; import org.springframework.data.redis.connection.stream.StreamInfo.XInfoStream; +import org.springframework.data.redis.connection.stream.StreamOffset; +import org.springframework.data.redis.connection.stream.StreamReadOptions; +import org.springframework.data.redis.connection.stream.StringRecord; import org.springframework.data.redis.connection.zset.Aggregate; import org.springframework.data.redis.connection.zset.DefaultTuple; import org.springframework.data.redis.connection.zset.Tuple; @@ -2566,35 +2576,36 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return this.delegate.hScan(key, options); } - @Nullable @Override public Long hStrLen(byte[] key, byte[] field) { return convertAndReturn(delegate.hStrLen(key, field), Converters.identityConverter()); } - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, + public @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, byte[]... fields) { - return this.delegate.expireHashField(key, expiration, options, fields); + return this.delegate.applyExpiration(key, expiration, options, fields); } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return this.delegate.hExpire(key, seconds, fields); + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpire(key, seconds, condition, fields); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return this.delegate.hpExpire(key, millis, fields); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hpExpire(key, millis, condition, fields); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return this.delegate.hExpireAt(key, unixTime, fields); + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + return this.delegate.hExpireAt(key, unixTime, condition, fields); } @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return this.delegate.hpExpireAt(key, unixTimeInMillis, fields); + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return this.delegate.hpExpireAt(key, unixTimeInMillis, condition, fields); } @Override @@ -2617,29 +2628,31 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return this.delegate.hTtl(key, timeUnit, fields); } - public @Nullable List expireHashField(String key, org.springframework.data.redis.core.types.Expiration expiration, + public @Nullable List applyExpiration(String key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, String... fields) { - return expireHashField(serialize(key), expiration, options, serializeMulti(fields)); + return applyExpiration(serialize(key), expiration, options, serializeMulti(fields)); } @Override - public List hExpire(String key, long seconds, String... fields) { - return hExpire(serialize(key), seconds, serializeMulti(fields)); + public List hExpire(String key, long seconds, FieldExpirationOptions.Condition condition, String... fields) { + return hExpire(serialize(key), seconds, condition, serializeMulti(fields)); } @Override - public List hpExpire(String key, long millis, String... fields) { - return hpExpire(serialize(key), millis, serializeMulti(fields)); + public List hpExpire(String key, long millis, FieldExpirationOptions.Condition condition, String... fields) { + return hpExpire(serialize(key), millis, condition, serializeMulti(fields)); } @Override - public List hExpireAt(String key, long unixTime, String... fields) { - return hExpireAt(serialize(key), unixTime, serializeMulti(fields)); + public List hExpireAt(String key, long unixTime, FieldExpirationOptions.Condition condition, String... fields) { + return hExpireAt(serialize(key), unixTime, condition, serializeMulti(fields)); } @Override - public List hpExpireAt(String key, long unixTimeInMillis, String... fields) { - return hpExpireAt(serialize(key), unixTimeInMillis, serializeMulti(fields)); + public List hpExpireAt(String key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + String... fields) { + return hpExpireAt(serialize(key), unixTimeInMillis, condition, serializeMulti(fields)); } @Override 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 979cf5300..8190caa6a 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -1483,28 +1483,58 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr @Override @Deprecated default List hExpire(byte[] key, long seconds, byte[]... fields) { - return hashCommands().hExpire(key, seconds, fields); + return hashCommands().hExpire(key, seconds, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hExpire(key, seconds, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hpExpire(byte[] key, long millis, byte[]... fields) { - return hashCommands().hpExpire(key, millis, fields); + return hashCommands().hpExpire(key, millis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return hashCommands().hpExpire(key, millis, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return hashCommands().hExpireAt(key, unixTime, fields); + return hashCommands().hExpireAt(key, unixTime, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hExpireAt(key, unixTime, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return hashCommands().hpExpireAt(key, unixTimeInMillis, fields); + return hashCommands().hpExpireAt(key, unixTimeInMillis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ + @Override + @Deprecated + default List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return hashCommands().hpExpireAt(key, unixTimeInMillis, condition, fields); } /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @@ -1538,9 +1568,10 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr /** @deprecated in favor of {@link RedisConnection#hashCommands()}}. */ @Override @Deprecated - default @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { - return hashCommands().expireHashField(key, expiration, options, fields); + default @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, FieldExpirationOptions options, + byte[]... fields) { + return hashCommands().applyExpiration(key, expiration, options, fields); } // GEO COMMANDS @@ -1914,9 +1945,8 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr /** @deprecated in favor of {@link RedisConnection#zSetCommands()}}. */ @Override @Deprecated - default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, - org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + default Long zRangeStoreByLex(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByLex(dstKey, srcKey, range, limit); } @@ -1933,7 +1963,7 @@ public interface DefaultedRedisConnection extends RedisCommands, RedisCommandsPr @Deprecated default Long zRangeStoreByScore(byte[] dstKey, byte[] srcKey, org.springframework.data.domain.Range range, - org.springframework.data.redis.connection.Limit limit) { + org.springframework.data.redis.connection.Limit limit) { return zSetCommands().zRangeStoreByScore(dstKey, srcKey, range, limit); } diff --git a/src/main/java/org/springframework/data/redis/connection/Hash.java b/src/main/java/org/springframework/data/redis/connection/Hash.java index c8d0e8a24..51e326dd2 100644 --- a/src/main/java/org/springframework/data/redis/connection/Hash.java +++ b/src/main/java/org/springframework/data/redis/connection/Hash.java @@ -18,21 +18,25 @@ package org.springframework.data.redis.connection; import java.util.Objects; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** + * Types for interacting with Hash data structures. + * * @author Christoph Strobl * @since 3.5 */ public interface Hash { + /** + * Expiration options for Hash Expiation updates. + */ class FieldExpirationOptions { - private static final FieldExpirationOptions NONE = new FieldExpirationOptions(null); - private @Nullable Condition condition; + private static final FieldExpirationOptions NONE = new FieldExpirationOptions(Condition.ALWAYS); + private final Condition condition; - FieldExpirationOptions(@Nullable Condition condition) { + FieldExpirationOptions(Condition condition) { this.condition = condition; } @@ -40,12 +44,11 @@ public interface Hash { return NONE; } - @Contract("_ -> new") public static FieldExpireOptionsBuilder builder() { return new FieldExpireOptionsBuilder(); } - public @Nullable Condition getCondition() { + public Condition getCondition() { return condition; } @@ -68,48 +71,67 @@ public interface Hash { public static class FieldExpireOptionsBuilder { - @Nullable Condition condition; + private Condition condition = Condition.ALWAYS; - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder nx() { this.condition = Condition.NX; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder xx() { this.condition = Condition.XX; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder gt() { this.condition = Condition.GT; return this; } - @Contract("_ -> this") + @Contract("-> this") public FieldExpireOptionsBuilder lt() { this.condition = Condition.LT; return this; } - @Contract("_ -> !null") public FieldExpirationOptions build() { - return condition == null ? NONE : new FieldExpirationOptions(condition); + return condition == Condition.ALWAYS ? NONE : new FieldExpirationOptions(condition); } + } public enum Condition { - /** Set expiration only when the field has no expiration. */ + /** + * Always apply expiration. + */ + ALWAYS, + + /** + * Set expiration only when the field has no expiration. + */ NX, - /** Set expiration only when the field has an existing expiration. */ + + /** + * Set expiration only when the field has an existing expiration. + */ XX, - /** Set expiration only when the new expiration is greater than current one. */ + + /** + * Set expiration only when the new expiration is greater than current one. + */ GT, - /** Set expiration only when the new expiration is greater than current one. */ + + /** + * Set expiration only when the new expiration is greater than current one. + */ LT + } + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index f58f4e32a..1e9fd94f3 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -858,7 +858,9 @@ public interface ReactiveHashCommands { private ExpireCommand(@Nullable ByteBuffer key, List fields, Expiration expiration, FieldExpirationOptions options) { + super(key, fields); + this.expiration = expiration; this.options = options; } @@ -921,6 +923,7 @@ public interface ReactiveHashCommands { default Mono hExpire(ByteBuffer key, Duration duration, ByteBuffer field) { Assert.notNull(duration, "Duration must not be null"); + return hExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); } @@ -939,9 +942,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hExpire(ByteBuffer key, Duration duration, List fields) { + Assert.notNull(duration, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expire(fields, duration).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expire(fields, duration).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -957,7 +961,7 @@ public interface ReactiveHashCommands { * @since 3.5 * @see Redis Documentation: HEXPIRE */ - Flux> expireHashField(Publisher commands); + Flux> applyExpiration(Publisher commands); /** * Expire a given {@literal field} after a given {@link Duration} of time, measured in milliseconds, has passed. @@ -975,6 +979,7 @@ public interface ReactiveHashCommands { default Mono hpExpire(ByteBuffer key, Duration duration, ByteBuffer field) { Assert.notNull(duration, "Duration must not be null"); + return hpExpire(key, duration, Collections.singletonList(field)).singleOrEmpty(); } @@ -995,7 +1000,8 @@ public interface ReactiveHashCommands { default Flux hpExpire(ByteBuffer key, Duration duration, List fields) { Assert.notNull(duration, "Duration must not be null"); - return expireHashField(Flux.just(new ExpireCommand(key, fields, + + return applyExpiration(Flux.just(new ExpireCommand(key, fields, Expiration.from(duration.toMillis(), TimeUnit.MILLISECONDS), FieldExpirationOptions.none()))) .mapNotNull(NumericResponse::getOutput); } @@ -1017,6 +1023,7 @@ public interface ReactiveHashCommands { default Mono hExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { Assert.notNull(expireAt, "Duration must not be null"); + return hExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); } @@ -1035,9 +1042,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hExpireAt(ByteBuffer key, Instant expireAt, List fields) { + Assert.notNull(expireAt, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.SECONDS).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -1058,6 +1066,7 @@ public interface ReactiveHashCommands { default Mono hpExpireAt(ByteBuffer key, Instant expireAt, ByteBuffer field) { Assert.notNull(expireAt, "Duration must not be null"); + return hpExpireAt(key, expireAt, Collections.singletonList(field)).singleOrEmpty(); } @@ -1076,9 +1085,10 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hpExpireAt(ByteBuffer key, Instant expireAt, List fields) { + Assert.notNull(expireAt, "Duration must not be null"); - return expireHashField(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key))) + return applyExpiration(Flux.just(ExpireCommand.expireAt(fields, expireAt, TimeUnit.MILLISECONDS).from(key))) .mapNotNull(NumericResponse::getOutput); } @@ -1135,7 +1145,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Mono hTtl(ByteBuffer key, ByteBuffer field) { - return hTtl(key, Collections.singletonList(field)).singleOrEmpty(); } @@ -1151,7 +1160,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hTtl(ByteBuffer key, List fields) { - return hTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); } @@ -1179,7 +1187,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Mono hpTtl(ByteBuffer key, ByteBuffer field) { - return hpTtl(key, Collections.singletonList(field)).singleOrEmpty(); } @@ -1195,7 +1202,6 @@ public interface ReactiveHashCommands { * @since 3.5 */ default Flux hpTtl(ByteBuffer key, List fields) { - return hpTtl(Flux.just(new HashFieldsCommand(key, fields))).mapNotNull(NumericResponse::getOutput); } @@ -1210,4 +1216,5 @@ public interface ReactiveHashCommands { * @see Redis Documentation: HPTTL */ Flux> hpTtl(Publisher commands); + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 5fde9d5db..f2d736b8e 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -25,6 +25,7 @@ import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; +import org.springframework.util.ObjectUtils; /** * Hash-specific commands supported by Redis. @@ -254,41 +255,78 @@ public interface RedisHashCommands { @Nullable Long hStrLen(byte[] key, byte[] field); - default @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - byte[]... fields) { - return expireHashField(key, expiration, FieldExpirationOptions.none(), fields); + default @Nullable List applyExpiration(byte[] key, + org.springframework.data.redis.core.types.Expiration expiration, byte[]... fields) { + return applyExpiration(key, expiration, FieldExpirationOptions.none(), fields); } - - @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields); - - /** - * Set time to live for given {@code fields} in seconds. - * - * @param key must not be {@literal null}. - * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. - * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ @Nullable - List hExpire(byte[] key, long seconds, byte[]... fields); + default List applyExpiration(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, + FieldExpirationOptions options, byte[]... fields) { + + if (expiration.isPersistent()) { + return hPersist(key, fields); + } + + if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); + } + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); + } + return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); + } + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + if (expiration.isUnixTimestamp()) { + return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), options.getCondition(), fields); + } + + if (expiration.isUnixTimestamp()) { + return hExpireAt(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + return hExpire(key, expiration.getExpirationTimeInSeconds(), options.getCondition(), fields); + } + + /** + * Set time to live for given {@code fields} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + default List hExpire(byte[] key, long seconds, byte[]... fields) { + return hExpire(key, seconds, FieldExpirationOptions.Condition.ALWAYS, fields); + } /** * Set time to live for given {@code fields}. * * @param key must not be {@literal null}. - * @param ttl the amount of time after which the fields will be expired in {@link Duration#toSeconds() seconds} precision, must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toSeconds() seconds} + * precision, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HEXPIRE * @since 3.5 */ @@ -298,31 +336,55 @@ public interface RedisHashCommands { } /** - * Set time to live for given {@code fields} in milliseconds. + * Set time to live for given {@code fields} in seconds. * * @param key must not be {@literal null}. - * @param millis the amount of time after which the fields will be expired in milliseconds, must not be {@literal null}. + * @param seconds the amount of time after which the fields will be expired in seconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HPEXPIRE + * @param condition the condition for expiration, must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE * @since 3.5 */ @Nullable - List hpExpire(byte[] key, long millis, byte[]... fields); + List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields); /** * Set time to live for given {@code fields} in milliseconds. * * @param key must not be {@literal null}. - * @param ttl the amount of time after which the fields will be expired in {@link Duration#toMillis() milliseconds} precision, must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + default List hpExpire(byte[] key, long millis, byte[]... fields) { + return hpExpire(key, millis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param ttl the amount of time after which the fields will be expired in {@link Duration#toMillis() milliseconds} + * precision, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPEXPIRE * @since 3.5 */ @@ -331,21 +393,61 @@ public interface RedisHashCommands { return hpExpire(key, ttl.toMillis(), fields); } + /** + * Set time to live for given {@code fields} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the fields will be expired in milliseconds, must not be + * {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields); + /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. * * @param key must not be {@literal null}. * @param unixTime the moment in time in which the field expires, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HEXPIREAT * @since 3.5 */ @Nullable - List hExpireAt(byte[] key, long unixTime, byte[]... fields); + default List hExpireAt(byte[] key, long unixTime, byte[]... fields) { + return hExpireAt(key, unixTime, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. @@ -353,24 +455,47 @@ public interface RedisHashCommands { * @param key must not be {@literal null}. * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HPEXPIREAT * @since 3.5 */ @Nullable - List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields); + default List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + return hpExpireAt(key, unixTimeInMillis, FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param condition the condition for expiration, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields); /** * Remove the expiration from given {@code field}. * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; - * {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPERSIST * @since 3.5 */ @@ -382,9 +507,10 @@ public interface RedisHashCommands { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the field exists but has no associated expiration time. - * The command returns {@code -2} if the field does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the field exists but has no associated + * expiration time. The command returns {@code -2} if the field does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -397,9 +523,10 @@ public interface RedisHashCommands { * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param fields must not be {@literal null}. - * @return for each of the fields supplied - the time to live in the {@link TimeUnit} provided; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return for each of the fields supplied - the time to live in the {@link TimeUnit} provided; or a negative value to + * signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. + * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -413,9 +540,10 @@ public interface RedisHashCommands { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ 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 ed0101641..5ff0e9946 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -71,7 +71,6 @@ import org.springframework.util.CollectionUtils; * @author Andrey Shlykov * @author ihaohong * @author Shyngys Sapraliyev - * * @see RedisCallback * @see RedisSerializer * @see StringRedisTemplate @@ -1661,7 +1660,6 @@ public interface StringRedisConnection extends RedisConnection { */ Long zRemRange(String key, long start, long end); - /** * Remove all elements between the lexicographical {@link Range}. * @@ -1941,7 +1939,8 @@ public interface StringRedisConnection extends RedisConnection { * @return * @since 1.6 * @see Redis Documentation: ZRANGEBYLEX - * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -1983,7 +1982,8 @@ public interface StringRedisConnection extends RedisConnection { * @return * @since 2.4 * @see Redis Documentation: ZREVRANGEBYLEX - * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, org.springframework.data.redis.connection.Limit) + * @see RedisZSetCommands#zRevRangeByLex(byte[], org.springframework.data.domain.Range, + * org.springframework.data.redis.connection.Limit) */ Set zRevRangeByLex(String key, org.springframework.data.domain.Range range, org.springframework.data.redis.connection.Limit limit); @@ -2333,22 +2333,41 @@ public interface StringRedisConnection extends RedisConnection { @Nullable Long hStrLen(String key, String field); - // TODO: why why whay is this such a shitty api that there's missing all the NX, XX, GT Options /** * Set time to live for given {@code field} in seconds. * * @param key must not be {@literal null}. * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HEXPIRE - * @since 3.4 + * @since 3.5 */ @Nullable - List hExpire(String key, long seconds, String... fields); + default List hExpire(String key, long seconds, String... fields) { + return hExpire(key, seconds, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in seconds. + * + * @param key must not be {@literal null}. + * @param seconds the amount of time after which the key will be expired in seconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + List hExpire(String key, long seconds, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set time to live for given {@code field} in milliseconds. @@ -2356,15 +2375,35 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPEXPIRE - * @since 3.4 + * @since 3.5 */ @Nullable - List hpExpire(String key, long millis, String... fields); + default List hpExpire(String key, long millis, String... fields) { + return hpExpire(key, millis, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set time to live for given {@code field} in milliseconds. + * + * @param key must not be {@literal null}. + * @param millis the amount of time after which the key will be expired in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HPEXPIRE + * @since 3.5 + */ + @Nullable + List hpExpire(String key, long millis, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. @@ -2372,15 +2411,35 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param unixTime the moment in time in which the field expires, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HEXPIREAT - * @since 3.4 + * @since 3.5 */ @Nullable - List hExpireAt(String key, long unixTime, String... fields); + default List hExpireAt(String key, long unixTime, String... fields) { + return hExpireAt(key, unixTime, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param unixTime the moment in time in which the field expires, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HEXPIREAT + * @since 3.5 + */ + @Nullable + List hExpireAt(String key, long unixTime, Hash.FieldExpirationOptions.Condition condition, String... fields); /** * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. @@ -2388,26 +2447,48 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not - * met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HPEXPIREAT - * @since 3.4 + * @since 3.5 */ @Nullable - List hpExpireAt(String key, long unixTimeInMillis, String... fields); + default List hpExpireAt(String key, long unixTimeInMillis, String... fields) { + return hpExpireAt(key, unixTimeInMillis, Hash.FieldExpirationOptions.Condition.ALWAYS, fields); + } + + /** + * Set the expiration for given {@code field} as a {@literal UNIX} timestamp in milliseconds. + * + * @param key must not be {@literal null}. + * @param unixTimeInMillis the moment in time in which the field expires in milliseconds, must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @see Redis Documentation: HPEXPIREAT + * @since 3.5 + */ + @Nullable + List hpExpireAt(String key, long unixTimeInMillis, Hash.FieldExpirationOptions.Condition condition, + String... fields); /** * Remove the expiration from given {@code field}. * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; - * {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction.{@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HPERSIST - * @since 3.4 + * @since 3.5 */ @Nullable List hPersist(String key, String... fields); @@ -2417,9 +2498,10 @@ public interface StringRedisConnection extends RedisConnection { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2432,9 +2514,10 @@ public interface StringRedisConnection extends RedisConnection { * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in the {@link TimeUnit} provided; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in the {@link TimeUnit} + * provided; or a negative value to signal an error. The command returns {@code -1} if the key exists but has + * no associated expiration time. The command returns {@code -2} if the key does not exist; {@literal null} + * when used in pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2446,9 +2529,10 @@ public interface StringRedisConnection extends RedisConnection { * * @param key must not be {@literal null}. * @param fields must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. - * The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in milliseconds; or a + * negative value to signal an error. The command returns {@code -1} if the key exists but has no associated + * expiration time. The command returns {@code -2} if the key does not exist; {@literal null} when used in + * pipeline / transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @@ -2678,8 +2762,7 @@ public interface StringRedisConnection extends RedisConnection { /** * Return the members of a geo set which are within the borders of the area specified by a given {@link GeoShape - * shape}. The query's center point is provided by - * {@link GeoReference}. + * shape}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. @@ -2695,8 +2778,7 @@ public interface StringRedisConnection extends RedisConnection { /** * Query the members of a geo set which are within the borders of the area specified by a given {@link GeoShape shape} - * and store the result at {@code destKey}. The query's center point is provided by - * {@link GeoReference}. + * and store the result at {@code destKey}. The query's center point is provided by {@link GeoReference}. * * @param key must not be {@literal null}. * @param reference must not be {@literal null}. diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 3326a00d6..1223ab4c0 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -33,10 +33,8 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; -import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * Cluster {@link RedisHashCommands} implementation for Jedis. @@ -292,88 +290,71 @@ class JedisClusterHashCommands implements RedisHashCommands { }.open(); } - @Nullable @Override - public List expireHashField(byte[] key, Expiration expiration, FieldExpirationOptions options, + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpire(key, seconds, fields); + } + + return connection.getCluster().hexpire(key, seconds, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpire(key, millis, fields); + } + + return connection.getCluster().hpexpire(key, millis, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); + + try { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hexpireAt(key, unixTime, fields); + } + + return connection.getCluster().hexpireAt(key, unixTime, ExpiryOption.valueOf(condition.name()), fields); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + @Override + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, byte[]... fields) { - if (expiration.isPersistent()) { - return hPersist(key, fields); - } + Assert.notNull(key, "Key must not be null"); + Assert.notNull(fields, "Fields must not be null"); - if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); + try { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, fields); } - if (expiration.isUnixTimestamp()) { - return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); - } - return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); - } - ExpiryOption option = ExpiryOption.valueOf(options.getCondition().name()); - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.getCluster().hpexpireAt(key, expiration.getExpirationTimeInMilliseconds(), option, fields); - } - return connection.getCluster().hpexpire(key, expiration.getExpirationTimeInMilliseconds(), option, fields); - } - - if (expiration.isUnixTimestamp()) { - return connection.getCluster().hexpireAt(key, expiration.getExpirationTimeInSeconds(), option, fields); - } - return connection.getCluster().hexpire(key, expiration.getExpirationTimeInSeconds(), option, fields); - - } - - @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hexpire(key, seconds, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hpexpire(key, millis, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hexpireAt(key, unixTime, fields); - } catch (Exception ex) { - throw convertJedisAccessException(ex); - } - } - - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - Assert.notNull(key, "Key must not be null"); - Assert.notNull(fields, "Fields must not be null"); - - try { - return connection.getCluster().hpexpireAt(key, unixTimeInMillis, fields); + return connection.getCluster().hpexpireAt(key, unixTimeInMillis, ExpiryOption.valueOf(condition.name()), fields); } catch (Exception ex) { throw convertJedisAccessException(ex); } @@ -381,6 +362,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hPersist(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -393,6 +375,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hTtl(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -405,6 +388,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hTtl(byte[] key, TimeUnit timeUnit, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -418,6 +402,7 @@ class JedisClusterHashCommands implements RedisHashCommands { @Override public List hpTtl(byte[] key, byte[]... fields) { + Assert.notNull(key, "Key must not be null"); Assert.notNull(fields, "Fields must not be null"); @@ -439,7 +424,7 @@ class JedisClusterHashCommands implements RedisHashCommands { } private DataAccessException convertJedisAccessException(Exception ex) { - return connection.convertJedisAccessException(ex); } + } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index e8751e85c..2e83d8aba 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -39,7 +39,6 @@ import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * {@link RedisHashCommands} implementation for Jedis. @@ -257,62 +256,48 @@ class JedisHashCommands implements RedisHashCommands { } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields); + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, seconds, option, fields); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, millis, option, fields); } @Override - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { - if (expiration.isPersistent()) { - return hPersist(key, fields); + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields); } - if (ObjectUtils.nullSafeEquals(FieldExpirationOptions.none(), options)) { - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return hpExpireAt(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - return hpExpire(key, expiration.getExpirationTimeInMilliseconds(), fields); - } - if (expiration.isUnixTimestamp()) { - return hExpireAt(key, expiration.getExpirationTimeInSeconds(), fields); - } - return hExpire(key, expiration.getExpirationTimeInSeconds(), fields); - } - - ExpiryOption option = ExpiryOption.valueOf(options.getCondition().name()); - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, - expiration.getExpirationTimeInMilliseconds(), option, fields); - } - return connection.invoke().just(Jedis::hpexpire, PipelineBinaryCommands::hpexpire, key, - expiration.getExpirationTimeInMilliseconds(), option, fields); - } - - if (expiration.isUnixTimestamp()) { - return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, - expiration.getExpirationTimeInSeconds(), option, fields); - } - return connection.invoke().just(Jedis::hexpire, PipelineBinaryCommands::hexpire, key, - expiration.getExpirationTimeInSeconds(), option, fields); + ExpiryOption option = ExpiryOption.valueOf(condition.name()); + return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, option, fields); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return connection.invoke().just(Jedis::hexpireAt, PipelineBinaryCommands::hexpireAt, key, unixTime, fields); - } + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { + if (condition == FieldExpirationOptions.Condition.ALWAYS) { + return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, + fields); + } + + ExpiryOption option = ExpiryOption.valueOf(condition.name()); return connection.invoke().just(Jedis::hpexpireAt, PipelineBinaryCommands::hpexpireAt, key, unixTimeInMillis, fields); } 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 fc8460d51..fe646b3a1 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 @@ -1160,6 +1160,14 @@ public class LettuceConnection extends AbstractRedisConnection { COMMAND_OUTPUT_TYPE_MAPPING.put(PFMERGE, IntegerOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(PFADD, IntegerOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIRE, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPEXPIREAT, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPERSIST, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HTTL, IntegerListOutput.class); + COMMAND_OUTPUT_TYPE_MAPPING.put(HPTTL, IntegerListOutput.class); + // DOUBLE COMMAND_OUTPUT_TYPE_MAPPING.put(HINCRBYFLOAT, DoubleOutput.class); COMMAND_OUTPUT_TYPE_MAPPING.put(INCRBYFLOAT, DoubleOutput.class); diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 16564fd1e..032d6230d 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -215,63 +215,28 @@ class LettuceHashCommands implements RedisHashCommands { } @Override - public @Nullable List expireHashField(byte[] key, org.springframework.data.redis.core.types.Expiration expiration, - FieldExpirationOptions options, byte[]... fields) { - - if (expiration.isPersistent()) { - return hPersist(key, fields); - } - - ExpireArgs option = new ExpireArgs() { - @Override - public void build(CommandArgs args) { - - if(ObjectUtils.nullSafeEquals(options, FieldExpirationOptions.none())) { - return; - } - - args.add(options.getCondition().name()); - } - }; - - if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { - if (expiration.isUnixTimestamp()) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpireat, key, - expiration.getExpirationTimeInMilliseconds(), option, fields).toList(); - } - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hpexpire, key, expiration.getExpirationTimeInMilliseconds(), option, fields) - .toList(); - } - - if (expiration.isUnixTimestamp()) { - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hexpireat, key, expiration.getExpirationTimeInSeconds(), option, fields) - .toList(); - } - return connection.invoke() - .fromMany(RedisHashAsyncCommands::hexpire, key, expiration.getExpirationTimeInSeconds(), option, fields) + public List hExpire(byte[] key, long seconds, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, getExpireArgs(condition), fields) .toList(); } @Override - public List hExpire(byte[] key, long seconds, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hexpire, key, seconds, fields).toList(); + public List hpExpire(byte[] key, long millis, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, getExpireArgs(condition), fields) + .toList(); } @Override - public List hpExpire(byte[] key, long millis, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpire, key, millis, fields).toList(); + public List hExpireAt(byte[] key, long unixTime, FieldExpirationOptions.Condition condition, byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, getExpireArgs(condition), fields).toList(); } @Override - public List hExpireAt(byte[] key, long unixTime, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hexpireat, key, unixTime, fields).toList(); - } - - @Override - public List hpExpireAt(byte[] key, long unixTimeInMillis, byte[]... fields) { - return connection.invoke().fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, fields).toList(); + public List hpExpireAt(byte[] key, long unixTimeInMillis, FieldExpirationOptions.Condition condition, + byte[]... fields) { + return connection.invoke() + .fromMany(RedisHashAsyncCommands::hpexpireat, key, unixTimeInMillis, getExpireArgs(condition), fields).toList(); } @Override @@ -349,4 +314,19 @@ class LettuceHashCommands implements RedisHashCommands { return value.hasValue() ? Converters.entryOf(value.getKey(), value.getValue()) : null; } + private ExpireArgs getExpireArgs(FieldExpirationOptions.Condition condition) { + + return new ExpireArgs() { + @Override + public void build(CommandArgs args) { + + if (ObjectUtils.nullSafeEquals(condition, FieldExpirationOptions.Condition.ALWAYS)) { + return; + } + + args.add(condition.name()); + } + }; + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index 3cc7bfd9c..84dd2ca90 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -269,7 +269,8 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { } @Override - public Flux> expireHashField(Publisher commands) { + public Flux> applyExpiration(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -317,6 +318,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hPersist(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -329,6 +331,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hTtl(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); @@ -341,6 +344,7 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { @Override public Flux> hpTtl(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { Assert.notNull(command.getKey(), "Key must not be null"); diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java new file mode 100644 index 000000000..33e0ff82e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/BoundHashFieldExpirationOperations.java @@ -0,0 +1,126 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.TimeUnit; + +import org.springframework.data.redis.connection.Hash; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; + +/** + * Hash Field Expiration operations bound to a certain hash key and set of hash fields. + * + * @param type of the hash field names. + * @author Mark Paluch + * @since 3.5 + */ +public interface BoundHashFieldExpirationOperations { + + /** + * Apply {@link Expiration} to the hash without any additional constraints. + * + * @param expiration the expiration definition. + * @return changes to the hash fields. + */ + default ExpireChanges expire(Expiration expiration) { + return expire(expiration, Hash.FieldExpirationOptions.none()); + } + + /** + * Apply {@link Expiration} to the hash fields given {@link Hash.FieldExpirationOptions expiration options}. + * + * @param expiration the expiration definition. + * @param options expiration options. + * @return changes to the hash fields. + */ + ExpireChanges expire(Expiration expiration, Hash.FieldExpirationOptions options); + + /** + * Set time to live for given {@code hashKey}. + * + * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. + * @throws IllegalArgumentException if the timeout is {@literal null}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expire(Duration timeout); + + /** + * Set the expiration for given {@code hashKey} as a {@literal date} timestamp. + * + * @param expireAt must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable + ExpireChanges expireAt(Instant expireAt); + + /** + * Remove the expiration from given {@code hashKey} . + * + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: HPERSIST + * @since 3.5 + */ + @Nullable + ExpireChanges persist(); + + /** + * Get the time to live for {@code hashKey} in seconds. + * + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(); + + /** + * Get the time to live for {@code hashKey} and convert it to the given {@link TimeUnit}. + * + * @param timeUnit must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. + * @see Redis Documentation: HTTL + * @since 3.5 + */ + @Nullable + Expirations getTimeToLive(TimeUnit timeUnit); + +} diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 0503c3309..0d287e929 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -15,16 +15,12 @@ */ package org.springframework.data.redis.core; -import java.time.Duration; -import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.TimeUnit; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; /** @@ -96,7 +92,7 @@ public interface BoundHashOperations extends BoundKeyOperations { Double increment(HK key, double delta); /** - * Return a random key (aka field) from the hash stored at the bound key. + * Return a random key from the hash stored at the bound key. * * @return {@literal null} if the hash does not exist or when used in pipeline / transaction. * @since 2.6 @@ -116,10 +112,10 @@ public interface BoundHashOperations extends BoundKeyOperations { Map.Entry randomEntry(); /** - * Return a random keys (aka fields) from the hash stored at the bound key. If the provided {@code count} argument is - * positive, return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same key multiple times. In this case, the - * number of returned keys is the absolute value of the specified count. + * Return a random keys from the hash stored at the bound key. If the provided {@code count} argument is positive, + * return a list of distinct keys, capped either at {@code count} or the hash size. If {@code count} is negative, the + * behavior changes and the command is allowed to return the same key multiple times. In this case, the number of + * returned keys is the absolute value of the specified count. * * @param count number of keys to return. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -159,84 +155,6 @@ public interface BoundHashOperations extends BoundKeyOperations { @Nullable Long lengthOfValue(HK hashKey); - default ExpireChanges expire(Expiration expiration, Collection hashKeys) { - return expire(expiration, FieldExpirationOptions.none(), hashKeys); - } - - ExpireChanges expire(Expiration expiration, FieldExpirationOptions options, Collection hashKeys); - - /** - * Set time to live for given {@code hashKey} (aka field). - * - * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the timeout is {@literal null}. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ - @Nullable - ExpireChanges expire(Duration timeout, Collection hashKeys); - - /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. - * - * @param expireAt must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. - * @see Redis Documentation: HEXPIRE - * @since 3.5 - */ - @Nullable - ExpireChanges expireAt(Instant expireAt, Collection hashKeys); - - /** - * Remove the expiration from given {@code hashKey} (aka field). - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. - * @see Redis Documentation: HPERSIST - * @since 3.5 - */ - @Nullable - ExpireChanges persist(Collection hashKeys); - - /** - * Get the time to live for {@code hashKey} (aka field) in seconds. - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - @Nullable - Expirations getExpire(Collection hashKeys); - - /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. - * - * @param timeUnit must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - @Nullable - Expirations getExpire(TimeUnit timeUnit, Collection hashKeys); - /** * Get size of hash at the bound key. * @@ -297,8 +215,45 @@ public interface BoundHashOperations extends BoundKeyOperations { */ Cursor> scan(ScanOptions options); + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration + * operation. + * + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration() { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), this::keys); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(HK... hashFields) { + return expiration(Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(getOperations().opsForHash(), getKey(), () -> hashFields); + } + /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java new file mode 100644 index 000000000..8dbabe6dd --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashFieldExpirationOperations.java @@ -0,0 +1,93 @@ +/* + * Copyright 2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; +import java.util.concurrent.TimeUnit; +import java.util.function.Supplier; + +import org.springframework.data.redis.connection.Hash; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link BoundHashFieldExpirationOperations}. + * + * @author Mark Paluch + * @since 3.5 + */ +class DefaultBoundHashFieldExpirationOperations implements BoundHashFieldExpirationOperations { + + private final HashOperations operations; + private final H key; + private final Supplier> hashFields; + + public DefaultBoundHashFieldExpirationOperations(HashOperations operations, H key, + Supplier> hashFields) { + + this.operations = operations; + this.key = key; + this.hashFields = hashFields; + } + + @Override + public ExpireChanges expire(Expiration expiration, Hash.FieldExpirationOptions options) { + return operations.expire(key, expiration, options, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expire(Duration timeout) { + return operations.expire(key, timeout, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges expireAt(Instant expireAt) { + return operations.expireAt(key, expireAt, getHashKeys()); + } + + @Nullable + @Override + public ExpireChanges persist() { + return operations.persist(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive() { + return operations.getTimeToLive(key, getHashKeys()); + } + + @Nullable + @Override + public Expirations getTimeToLive(TimeUnit timeUnit) { + return operations.getTimeToLive(key, timeUnit, getHashKeys()); + } + + private Collection getHashKeys() { + + Collection hks = hashFields.get(); + + Assert.state(hks != null, "Hash keys must not be null"); + return hks; + } + +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index 2be7e0bd3..804617616 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -29,8 +29,9 @@ import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.convert.Converters; -import org.springframework.data.redis.core.Expirations.Timeouts; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -224,14 +225,11 @@ class DefaultHashOperations extends AbstractOperations imp byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - boolean splitSecond = TimeoutUtils.hasMillis(duration); + boolean hasMillis = TimeoutUtils.hasMillis(duration); - List raw = execute(connection -> { - if (splitSecond) { - return connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys); - } - return connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys); - }); + List raw = execute(connection -> TimeoutUtils.hasMillis(duration) + ? connection.hashCommands().hpExpire(rawKey, duration.toMillis(), rawHashKeys) + : connection.hashCommands().hExpire(rawKey, TimeoutUtils.toSeconds(duration), rawHashKeys)); return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; } @@ -243,8 +241,7 @@ class DefaultHashOperations extends AbstractOperations imp byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - - Long millis = instant.toEpochMilli(); + long millis = instant.toEpochMilli(); List raw = execute(connection -> TimeoutUtils.containsSplitSecond(millis) ? connection.hashCommands().hpExpireAt(rawKey, millis, rawHashKeys) @@ -257,10 +254,10 @@ class DefaultHashOperations extends AbstractOperations imp public ExpireChanges expire(K key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys) { List orderedKeys = List.copyOf(hashKeys); - byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); - List raw = execute(connection -> connection.hashCommands().expireHashField(rawKey, expiration, options, rawHashKeys)); + List raw = execute( + connection -> connection.hashCommands().applyExpiration(rawKey, expiration, options, rawHashKeys)); return raw != null ? ExpireChanges.of(orderedKeys, raw) : null; } @@ -269,7 +266,6 @@ class DefaultHashOperations extends AbstractOperations imp public ExpireChanges persist(K key, Collection hashKeys) { List orderedKeys = List.copyOf(hashKeys); - byte[] rawKey = rawKey(key); byte[][] rawHashKeys = rawHashKeys(orderedKeys.toArray()); @@ -279,7 +275,7 @@ class DefaultHashOperations extends AbstractOperations imp } @Override - public Expirations getExpire(K key, TimeUnit timeUnit, Collection hashKeys) { + public Expirations getTimeToLive(K key, TimeUnit timeUnit, Collection hashKeys) { if(timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java index d373a7f06..8c97c43fc 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -15,10 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.connection.ReactiveHashCommands.ExpireCommand; -import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; -import org.springframework.data.redis.core.types.Expiration; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -33,10 +29,16 @@ import java.util.concurrent.TimeUnit; import java.util.function.Function; import org.reactivestreams.Publisher; + import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.connection.ReactiveHashCommands.ExpireCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.convert.Converters; -import org.springframework.data.redis.core.Expirations.Timeouts; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; +import org.springframework.data.redis.core.types.Expirations.Timeouts; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -254,7 +256,9 @@ class DefaultReactiveHashOperations implements ReactiveHashOperations List rawHashKeys = orderedKeys.stream().map(this::rawHashKey).toList(); Mono> raw =createFlux(connection -> { - return connection.expireHashField(Mono.just(ExpireCommand.expire(rawHashKeys, expiration).from(rawKey).withOptions(options))).map(NumericResponse::getOutput); + return connection + .applyExpiration(Mono.just(ExpireCommand.expire(rawHashKeys, expiration).from(rawKey).withOptions(options))) + .map(NumericResponse::getOutput); }).collectList(); return raw.map(values -> ExpireChanges.of(orderedKeys, values)); @@ -288,7 +292,7 @@ class DefaultReactiveHashOperations implements ReactiveHashOperations @Nullable @Override - public Mono> getExpire(H key, TimeUnit timeUnit, Collection hashKeys) { + public Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys) { if (timeUnit.compareTo(TimeUnit.MILLISECONDS) < 0) { throw new IllegalArgumentException("%s precision is not supported must be >= MILLISECONDS".formatted(timeUnit)); diff --git a/src/main/java/org/springframework/data/redis/core/ExpireChanges.java b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java index b9486f639..029922d96 100644 --- a/src/main/java/org/springframework/data/redis/core/ExpireChanges.java +++ b/src/main/java/org/springframework/data/redis/core/ExpireChanges.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core; +import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -23,6 +24,7 @@ import java.util.Set; import java.util.function.Predicate; import java.util.stream.Collectors; +import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; /** @@ -35,7 +37,7 @@ import org.springframework.util.CollectionUtils; *
  • {@link #skipped()} returns keys for which the time to live has not been set because a precondition was not * met
  • * - * + * * @author Christoph Strobl * @since 3.5 */ @@ -50,23 +52,25 @@ public class ExpireChanges { /** * Factory Method to create {@link ExpireChanges} from raw sources. * - * @param keys the keys to associated with the raw values in states. Defines the actual order of entries within + * @param fields the fields to associated with the raw values in states. Defines the actual order of entries within * {@link ExpireChanges}. * @param states the raw Redis state change values. * @return new instance of {@link ExpireChanges}. * @param the key type used */ - public static ExpireChanges of(List keys, List states) { + public static ExpireChanges of(List fields, List states) { - if (keys.size() == 1) { - return new ExpireChanges<>(Map.of(keys.iterator().next(), stateFromValue(states.iterator().next()))); + Assert.isTrue(fields.size() == states.size(), "Keys and States must have the same number of elements"); + + if (fields.size() == 1) { + return new ExpireChanges<>(Map.of(fields.iterator().next(), stateFromValue(states.iterator().next()))); } - Map target = CollectionUtils.newLinkedHashMap(keys.size()); - for (int i = 0; i < keys.size(); i++) { - target.put(keys.get(i), stateFromValue(states.get(i))); + Map target = CollectionUtils.newLinkedHashMap(fields.size()); + for (int i = 0; i < fields.size(); i++) { + target.put(fields.get(i), stateFromValue(states.get(i))); } - return new ExpireChanges<>(target); + return new ExpireChanges<>(Collections.unmodifiableMap(target)); } /** diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index c32c33983..67c4ce0fa 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.time.Duration; import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Map; @@ -25,6 +26,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; import org.springframework.lang.Nullable; /** @@ -96,7 +98,7 @@ public interface HashOperations { Double increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return {@literal null} if key does not exist or when used in pipeline / transaction. @@ -118,10 +120,10 @@ public interface HashOperations { Map.Entry randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -228,15 +230,16 @@ public interface HashOperations { Cursor> scan(H key, ScanOptions options); /** - * Set time to live for given {@code hashKey} (aka field). + * Set time to live for given {@code hashKey} . * * @param key must not be {@literal null}. * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time + * is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition + * is not met); {@code -2} indicating there is no such field; {@literal null} when used in pipeline / + * transaction. * @throws IllegalArgumentException if the timeout is {@literal null}. * @see Redis Documentation: HEXPIRE * @since 3.5 @@ -245,15 +248,16 @@ public interface HashOperations { ExpireChanges expire(H key, Duration timeout, Collection hashKeys); /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. + * Set the expiration for given {@code hashKeys} as a {@literal date} timestamp. * * @param key must not be {@literal null}. * @param expireAt must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. * @see Redis Documentation: HEXPIRE * @since 3.5 @@ -261,16 +265,33 @@ public interface HashOperations { @Nullable ExpireChanges expireAt(H key, Instant expireAt, Collection hashKeys); + /** + * Apply the expiration for given {@code hashKeys} as a {@literal date} timestamp. + * + * @param key must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param options must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is + * deleted already due to expiration, or provided expiry interval is in the past; {@code 1} indicating + * expiration time is set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | + * GT | LT condition is not met); {@code -2} indicating there is no such field; {@literal null} when used in + * pipeline / transaction. + * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. + * @see Redis Documentation: HEXPIRE + * @since 3.5 + */ + @Nullable ExpireChanges expire(H key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys); /** - * Remove the expiration from given {@code hashKey} (aka field). + * Remove the expiration from given {@code hashKeys} . * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is + * removed; {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such + * field; {@literal null} when used in pipeline / transaction. * @see Redis Documentation: HPERSIST * @since 3.5 */ @@ -278,38 +299,77 @@ public interface HashOperations { ExpireChanges persist(H key, Collection hashKeys); /** - * Get the time to live for {@code hashKey} (aka field) in seconds. + * Get the time to live for {@code hashKeys} in seconds. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @Nullable - default Expirations getExpire(H key, Collection hashKeys) { - return getExpire(key, TimeUnit.SECONDS, hashKeys); + default Expirations getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); } /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. + * Get the time to live for {@code hashKeys} and convert it to the given {@link TimeUnit}. * * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. + * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative + * value to signal an error. The command returns {@code -1} if the key exists but has no associated expiration + * time. The command returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / + * transaction. * @see Redis Documentation: HTTL * @since 3.5 */ @Nullable - Expirations getExpire(H key, TimeUnit timeUnit, Collection hashKeys); + Expirations getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration operation. + * + * @param key must not be {@literal null}. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> keys(key)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, HK... hashFields) { + return expiration(key, Arrays.asList(hashFields)); + } + + /** + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key} for the given hash fields. + * + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. + * @since 3.5 + */ + default BoundHashFieldExpirationOperations expiration(H key, Collection hashFields) { + return new DefaultBoundHashFieldExpirationOperations<>(this, key, () -> hashFields); + } /** * @return never {@literal null}. */ RedisOperations getOperations(); + } diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java index 2d0cbcaf1..757dbcea7 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -15,8 +15,6 @@ */ package org.springframework.data.redis.core; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; -import org.springframework.data.redis.core.types.Expiration; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -28,6 +26,9 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.core.types.Expirations; import org.springframework.lang.Nullable; /** @@ -102,7 +103,7 @@ public interface ReactiveHashOperations { Mono increment(H key, HK hashKey, double delta); /** - * Return a random hash key (aka field) from the hash stored at {@code key}. + * Return a random hash key from the hash stored at {@code key}. * * @param key must not be {@literal null}. * @return @@ -122,10 +123,10 @@ public interface ReactiveHashOperations { Mono> randomEntry(H key); /** - * Return random hash keys (aka fields) from the hash stored at {@code key}. If the provided {@code count} argument is - * positive, return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is - * negative, the behavior changes and the command is allowed to return the same hash key multiple times. In this case, - * the number of returned fields is the absolute value of the specified count. + * Return random hash keys from the hash stored at {@code key}. If the provided {@code count} argument is positive, + * return a list of distinct hash keys, capped either at {@code count} or the hash size. If {@code count} is negative, + * the behavior changes and the command is allowed to return the same hash key multiple times. In this case, the + * number of returned fields is the absolute value of the specified count. * * @param key must not be {@literal null}. * @param count number of fields to return. @@ -242,7 +243,7 @@ public interface ReactiveHashOperations { Mono> expire(H key, Expiration expiration, FieldExpirationOptions options, Collection hashKeys); /** - * Set the expiration for given {@code hashKey} (aka field) as a {@literal date} timestamp. + * Set the expiration for given {@code hashKey} as a {@literal date} timestamp. * * @param key must not be {@literal null}. * @param expireAt must not be {@literal null}. @@ -260,7 +261,7 @@ public interface ReactiveHashOperations { Mono> expireAt(H key, Instant expireAt, Collection hashKeys); /** - * Remove the expiration from given {@code hashKey} (aka field). + * Remove the expiration from given {@code hashKey} . * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -274,7 +275,7 @@ public interface ReactiveHashOperations { Mono> persist(H key, Collection hashKeys); /** - * Get the time to live for {@code hashKey} (aka field) in seconds. + * Get the time to live for {@code hashKey} in seconds. * * @param key must not be {@literal null}. * @param hashKeys must not be {@literal null}. @@ -286,12 +287,12 @@ public interface ReactiveHashOperations { * @since 3.5 */ @Nullable - default Mono> getExpire(H key, Collection hashKeys) { - return getExpire(key, TimeUnit.SECONDS, hashKeys); + default Mono> getTimeToLive(H key, Collection hashKeys) { + return getTimeToLive(key, TimeUnit.SECONDS, hashKeys); } /** - * Get the time to live for {@code hashKey} (aka field) and convert it to the given {@link TimeUnit}. + * Get the time to live for {@code hashKey} and convert it to the given {@link TimeUnit}. * * @param key must not be {@literal null}. * @param timeUnit must not be {@literal null}. @@ -304,7 +305,7 @@ public interface ReactiveHashOperations { * @since 3.5 */ @Nullable - Mono> getExpire(H key, TimeUnit timeUnit, Collection hashKeys); + Mono> getTimeToLive(H key, TimeUnit timeUnit, Collection hashKeys); /** * Removes the given {@literal key}. diff --git a/src/main/java/org/springframework/data/redis/core/RedisCommand.java b/src/main/java/org/springframework/data/redis/core/RedisCommand.java index e9303233d..53dc940a9 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -107,6 +107,13 @@ public enum RedisCommand { HSET("w", 3, 3), // HSETNX("w", 3, 3), // HVALS("r", 1, 1), // + HEXPIRE("w", 5), // + HEXPIREAT("w", 5), // + HPEXPIRE("w", 5), // + HPEXPIREAT("w", 5), // + HPERSIST("w", 4), // + HTTL("r", 4), // + HPTTL("r", 4), // // -- I INCR("rw", 1), // INCRBYFLOAT("rw", 2, 2), // diff --git a/src/main/java/org/springframework/data/redis/core/RedisOperations.java b/src/main/java/org/springframework/data/redis/core/RedisOperations.java index 8c1ad67ad..def0dca04 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -376,6 +376,7 @@ public interface RedisOperations { @Nullable Boolean persist(K key); + // TODO: Add TimeToLive (getTimeToLive) /** * Get the time to live for {@code key} in seconds. * diff --git a/src/main/java/org/springframework/data/redis/core/Expirations.java b/src/main/java/org/springframework/data/redis/core/types/Expirations.java similarity index 62% rename from src/main/java/org/springframework/data/redis/core/Expirations.java rename to src/main/java/org/springframework/data/redis/core/types/Expirations.java index 958f90e3a..7101cb60f 100644 --- a/src/main/java/org/springframework/data/redis/core/Expirations.java +++ b/src/main/java/org/springframework/data/redis/core/types/Expirations.java @@ -13,9 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.redis.core; +package org.springframework.data.redis.core.types; import java.time.Duration; +import java.util.Collection; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; @@ -29,31 +30,32 @@ import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; /** - * Value Object linking a number of keys to their {@link Expiration} retaining the order of the original source. + * Value Object linking a number of keys to their {@link TimeToLive} retaining the order of the original source. * Dedicated higher level methods interpret raw expiration values retrieved from a Redis Client. *
      *
    1. {@link #persistent()} returns keys that do not have an associated time to live
    2. *
    3. {@link #missing()} returns keys that do not exist and therefore have no associated time to live
    4. - *
    5. {@link #expirations()} returns the ordered list of {@link Expiration expirations} based on the raw values
    6. + *
    7. {@link #ttl()} returns the ordered list of {@link TimeToLive expirations} based on the raw values
    8. *
    9. {@link #expiring()} returns the expiring keys along with their {@link Duration time to live}
    10. *
    - * + * * @author Christoph Strobl + * @author Mark Paluch * @since 3.5 */ -public class Expirations { // TODO: should we move this to let's say Hash.class or another place +public class Expirations { private final TimeUnit unit; - private final Map expirations; + private final Map expirations; - Expirations(TimeUnit unit, Map expirations) { + Expirations(TimeUnit unit, Map expirations) { this.unit = unit; this.expirations = expirations; } /** * Factory Method to create {@link Expirations} from raw sources provided in a given {@link TimeUnit}. - * + * * @param targetUnit the actual time unit of the raw timeToLive values. * @param keys the keys to associated with the raw values in timeToLive. Defines the actual order of entries within * {@link Expirations}. @@ -69,12 +71,12 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla } if (keys.size() == 1) { return new Expirations<>(targetUnit, - Map.of(keys.iterator().next(), Expiration.of(timeouts.raw().iterator().next(), timeouts.timeUnit()))); + Map.of(keys.iterator().next(), TimeToLive.of(timeouts.raw().iterator().next(), timeouts.timeUnit()))); } - Map target = CollectionUtils.newLinkedHashMap(keys.size()); + Map target = CollectionUtils.newLinkedHashMap(keys.size()); for (int i = 0; i < keys.size(); i++) { - target.put(keys.get(i), Expiration.of(timeouts.get(i), timeouts.timeUnit())); + target.put(keys.get(i), TimeToLive.of(timeouts.get(i), timeouts.timeUnit())); } return new Expirations<>(targetUnit, target); } @@ -83,87 +85,98 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla * @return an ordered set of keys that do not have a time to live. */ public Set persistent() { - return filterByState(Expiration.PERSISTENT); + return filterByState(TimeToLive.PERSISTENT); } /** - * @return an ordered set of keys that do not exists and therefore do not have a time to live. + * @return an ordered set of keys that do not exist and therefore do not have a time to live. */ public Set missing() { - return filterByState(Expiration.MISSING); + return filterByState(TimeToLive.MISSING); } /** - * @return an ordered set of all {@link Expirations expirations} where the {@link Expiration#value()} is using the - * {@link TimeUnit} defined in {@link #precision()}. + * @return an ordered set of all {@link Expirations expirations} where the {@link TimeToLive#value()} is using the + * {@link TimeUnit} defined in {@link #timeUnit()}. */ - public List expirations() { + public List ttl() { return expirations.values().stream().map(it -> it.convert(this.unit)).toList(); } /** - * @return the {@link TimeUnit} for {@link Expiration expirations} held by this instance. + * @return the {@link TimeUnit} for {@link TimeToLive expirations} held by this instance. */ - public TimeUnit precision() { + public TimeUnit timeUnit() { return unit; } /** * @return an ordered {@link List} of {@link java.util.Map.Entry entries} combining keys with their actual time to - * live. {@link Expiration#isMissing() Missing} and {@link Expiration#isPersistent() persistent} entries are + * live. {@link TimeToLive#isMissing() Missing} and {@link TimeToLive#isPersistent() persistent} entries are * skipped. */ public List> expiring() { + return expirations.entrySet().stream().filter(it -> !it.getValue().isMissing() && !it.getValue().isPersistent()) .map(it -> Map.entry(it.getKey(), toDuration(it.getValue()))).toList(); } + /** + * @return the ordered collection of keys that are associated with an expiration. + */ + public Collection keys() { + return expirations.keySet(); + } + /** * @param key - * @return the {@link Expirations expirations} where the {@link Expiration#value()} is using the {@link TimeUnit} - * defined in {@link #precision()} or {@literal null} if no entry could be found. + * @return the {@link Expirations expirations} where the {@link TimeToLive#value()} is using the {@link TimeUnit} + * defined in {@link #timeUnit()} or {@literal null} if no entry could be found. */ @Nullable - public Expiration expirationOf(K key) { + public TimeToLive expirationOf(K key) { - Expiration expiration = expirations.get(key); - if (expiration == null) { + TimeToLive timeToLive = expirations.get(key); + + if (timeToLive == null) { return null; } - return expiration.convert(this.unit); + return timeToLive.convert(this.unit); } /** * @param key * @return the time to live value of the requested key if it exists and the expiration is neither - * {@link Expiration#isMissing() missing} nor {@link Expiration#isPersistent() persistent}, {@literal null} + * {@link TimeToLive#isMissing() missing} nor {@link TimeToLive#isPersistent() persistent}, {@literal null} * otherwise. */ @Nullable public Duration ttlOf(K key) { - - Expiration expiration = expirationOf(key); - if (expiration == null) { - return null; - } - return toDuration(expiration); + return toDuration(expirationOf(key)); } - private Set filterByState(Expiration filter) { + private Set filterByState(TimeToLive filter) { return expirations.entrySet().stream().filter(entry -> entry.getValue().equals(filter)).map(Map.Entry::getKey) .collect(Collectors.toCollection(LinkedHashSet::new)); } @Nullable - static Duration toDuration(Expiration expiration) { + static Duration toDuration(@Nullable TimeToLive timeToLive) { - if (expiration.sourceUnit == null) { + if (timeToLive == null || timeToLive.sourceUnit == null) { return null; } - return Duration.of(expiration.raw(), expiration.sourceUnit.toChronoUnit()); + + return Duration.of(timeToLive.raw(), timeToLive.sourceUnit.toChronoUnit()); } + /** + * Collection of timeouts associated with a {@link TimeUnit}. + * + * @param timeUnit + * @param raw + */ public record Timeouts(TimeUnit timeUnit, List raw) { Long get(int index) { @@ -173,6 +186,7 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla public int size() { return raw.size(); } + } /** @@ -182,30 +196,57 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla * {@link #PERSISTENT} mark predefined states returned by Redis indicating a time to live value could not be retrieved * due to various reasons. */ - public static class Expiration { // TODO: is Expiry a better name for this type? + public static class TimeToLive { + /** + * Predefined {@link TimeToLive} for a key that does not exist and therefore does not have a time to live. + */ + public static TimeToLive MISSING = new TimeToLive(-2L); + + /** + * Predefined {@link TimeToLive} for a key that exists but does not expire. + */ + public static TimeToLive PERSISTENT = new TimeToLive(-1L); + + private final @Nullable TimeUnit sourceUnit; + private final @Nullable TimeUnit targetUnit; private final long raw; - @Nullable TimeUnit sourceUnit; - @Nullable TimeUnit targetUnit; - public Expiration(long value) { + TimeToLive(long value) { this(value, null); } - public Expiration(long value, @Nullable TimeUnit sourceUnit) { + TimeToLive(long value, @Nullable TimeUnit sourceUnit) { this(value, sourceUnit, null); } - public Expiration(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) { + TimeToLive(long value, @Nullable TimeUnit sourceUnit, @Nullable TimeUnit targetUnit) { this.raw = value; this.sourceUnit = sourceUnit; this.targetUnit = targetUnit; } + /** + * Factory method for creating {@link TimeToLive} instances, returning predefined ones if the value matches a known + * reserved state. + * + * @param value the TTL value. + * @param timeUnit time unit for the given value. + * @return the {@link TimeToLive} for the given raw value. + */ + public static TimeToLive of(Number value, TimeUnit timeUnit) { + + return switch (value.intValue()) { + case -2 -> MISSING; + case -1 -> PERSISTENT; + default -> new TimeToLive(value.longValue(), timeUnit); + }; + } + /** * The raw source value as returned by the Redis Client. * - * @return the raw data + * @return the raw data. */ public long raw() { return raw; @@ -219,58 +260,36 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla if (sourceUnit == null || targetUnit == null) { return raw; } + return targetUnit.convert(raw, sourceUnit); } /** * @param timeUnit must not be {@literal null}. - * @return the {@link Expiration} instance with new target {@link TimeUnit} set for obtaining the {@link #value() + * @return the {@link TimeToLive} instance with new target {@link TimeUnit} set for obtaining the {@link #value() * value}, or the same instance raw value cannot or must not be converted. */ - public Expiration convert(TimeUnit timeUnit) { + public TimeToLive convert(TimeUnit timeUnit) { if (sourceUnit == null || ObjectUtils.nullSafeEquals(sourceUnit, timeUnit)) { return this; } - return new Expiration(raw, sourceUnit, timeUnit); + + return new TimeToLive(raw, sourceUnit, timeUnit); } - /** - * Predefined {@link Expiration} for a key that does not exists and therefore does not have a time to live. - */ - public static Expiration MISSING = new Expiration(-2L); - - /** - * Predefined {@link Expiration} for a key that exists but does not expire. - */ - public static Expiration PERSISTENT = new Expiration(-1L); - /** * @return {@literal true} if key exists but does not expire. */ public boolean isPersistent() { - return PERSISTENT.equals(this); + return PERSISTENT.raw() == raw(); } /** - * @return {@literal true} if key does not exists and therefore does not have a time to live. + * @return {@literal true} if key does not exist and therefore does not have a time to live. */ public boolean isMissing() { - return MISSING.equals(this); - } - - /** - * Factory method for creating {@link Expiration} instances, returning predefined ones if the value matches a known - * reserved state. - * - * @return the {@link Expiration} for the given raw value. - */ - static Expiration of(Number value, TimeUnit timeUnit) { - return switch (value.intValue()) { - case -2 -> MISSING; - case -1 -> PERSISTENT; - default -> new Expiration(value.longValue(), timeUnit); - }; + return MISSING.raw() == raw(); } @Override @@ -280,7 +299,7 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla return true; } - if (!(o instanceof Expiration that)) { + if (!(o instanceof Expirations.TimeToLive that)) { return false; } @@ -299,5 +318,16 @@ public class Expirations { // TODO: should we move this to let's say Hash.cla public int hashCode() { return Objects.hash(raw); } + + @Override + public String toString() { + + return switch ((int) raw()) { + case -2 -> "MISSING"; + case -1 -> "PERSISTENT"; + default -> "%d %s".formatted(raw(), sourceUnit); + }; + } } + } diff --git a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java index ad22195ad..18c3c24e2 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/DefaultRedisMap.java @@ -15,19 +15,15 @@ */ package org.springframework.data.redis.support.collections; -import java.time.Duration; -import java.time.Instant; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.Map; -import java.util.Objects; import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisOperations; @@ -328,28 +324,13 @@ public class DefaultRedisMap implements RedisMap { } @Override - public ExpireChanges expire(Duration timeout, Collection hashKeys) { - return Objects.requireNonNull(hashOps.expire(timeout, hashKeys)); + public BoundHashFieldExpirationOperations expiration() { + return hashOps.expiration(); } @Override - public ExpireChanges expireAt(Instant expireAt, Collection hashKeys) { - return Objects.requireNonNull(hashOps.expireAt(expireAt, hashKeys)); - } - - @Override - public ExpireChanges persist(Collection hashKeys) { - return Objects.requireNonNull(hashOps.persist(hashKeys)); - } - - @Override - public Expirations getExpire(Collection hashKeys) { - return Objects.requireNonNull(hashOps.getExpire(hashKeys)); - } - - @Override - public Expirations getExpire(TimeUnit timeUnit, Collection hashKeys) { - return Objects.requireNonNull(hashOps.getExpire(timeUnit, hashKeys)); + public BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return hashOps.expiration(hashFields); } private void checkResult(@Nullable Object obj) { diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java index 54d002d54..955942eb2 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisMap.java @@ -15,16 +15,13 @@ */ package org.springframework.data.redis.support.collections; -import java.time.Duration; -import java.time.Instant; +import java.util.Arrays; import java.util.Collection; import java.util.Iterator; import java.util.Map; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.TimeUnit; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.lang.Nullable; /** @@ -33,6 +30,7 @@ import org.springframework.lang.Nullable; * @author Costin Leau * @author Christoph Strobl * @author Tihomi Mateev + * @author Mark Paluch */ public interface RedisMap extends RedisStore, ConcurrentMap { @@ -71,7 +69,7 @@ public interface RedisMap extends RedisStore, ConcurrentMap { * @since 2.6 */ @Nullable - Map.Entry randomEntry(); + Map.Entry randomEntry(); /** * @since 1.4 @@ -80,69 +78,34 @@ public interface RedisMap extends RedisStore, ConcurrentMap { Iterator> scan(); /** - * Set time to live for given {hash {@code key}. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at + * {@code key}. Operations on the expiration object obtain keys at the time of invoking any expiration operation. * - * @param timeout the amount of time after which the key will be expired, must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is 0; {@code 1} indicating expiration time is set/updated; - * {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); {@code -2} - * indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the timeout is {@literal null}. - * @see Redis Documentation: HEXPIRE + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges expire(Duration timeout, Collection hashKeys); + BoundHashFieldExpirationOperations expiration(); /** - * Set the expiration for given hash {@code key} as a {@literal date} timestamp. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. * - * @param expireAt must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 2} indicating the specific field is deleted - * already due to expiration, or provided expiry interval is in the past; {@code 1} indicating expiration time is - * set/updated; {@code 0} indicating the expiration time is not set (a provided NX | XX | GT | LT condition is not met); - * {@code -2} indicating there is no such field; {@literal null} when used in pipeline / transaction. - * @throws IllegalArgumentException if the instant is {@literal null} or too large to represent as a {@code Date}. - * @see Redis Documentation: HEXPIRE + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges expireAt(Instant expireAt, Collection hashKeys); + default BoundHashFieldExpirationOperations expiration(K... hashFields) { + return expiration(Arrays.asList(hashFields)); + } /** - * Remove the expiration from given hash {@code key}. + * Returns a bound operations object to perform operations on the hash field expiration for all hash fields at the + * bound {@code key} for the given hash fields. * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: {@code 1} indicating expiration time is removed; - * {@code -1} field has no expiration time to be removed; {@code -2} indicating there is no such field; {@literal null} when - * used in pipeline / transaction. - * @see Redis Documentation: HPERSIST + * @param hashFields collection of hash fields to operate on. + * @return the bound operations object to perform operations on the hash field expiration. * @since 3.5 */ - ExpireChanges persist(Collection hashKeys); + BoundHashFieldExpirationOperations expiration(Collection hashFields); - /** - * Get the time to live for hash {@code key} in seconds. - * - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - Expirations getExpire(Collection hashKeys); - - /** - * Get the time to live for hash {@code key} and convert it to the given {@link TimeUnit}. - * - * @param timeUnit must not be {@literal null}. - * @param hashKeys must not be {@literal null}. - * @return a list of {@link Long} values for each of the fields provided: the time to live in seconds; or a negative value - * to signal an error. The command returns {@code -1} if the key exists but has no associated expiration time. The command - * returns {@code -2} if the key does not exist; {@literal null} when used in pipeline / transaction. - * @see Redis Documentation: HTTL - * @since 3.5 - */ - Expirations getExpire(TimeUnit timeUnit, Collection hashKeys); } diff --git a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java index 54d7f0c9d..c6a5f0a45 100644 --- a/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java +++ b/src/main/java/org/springframework/data/redis/support/collections/RedisProperties.java @@ -17,15 +17,20 @@ package org.springframework.data.redis.support.collections; import java.io.IOException; import java.io.OutputStream; -import java.time.Duration; -import java.time.Instant; -import java.util.*; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.Enumeration; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Map; import java.util.Map.Entry; +import java.util.Properties; +import java.util.Set; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.connection.DataType; -import org.springframework.data.redis.core.Expirations; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisOperations; import org.springframework.lang.Nullable; @@ -298,42 +303,17 @@ public class RedisProperties extends Properties implements RedisMap> scan() { - throw new UnsupportedOperationException(); + return (Iterator) delegate.scan(); } @Override - public ExpireChanges expire(Duration timeout, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.expire(timeout, keys); + public BoundHashFieldExpirationOperations expiration() { + return (BoundHashFieldExpirationOperations) delegate.expiration(); } @Override - public ExpireChanges expireAt(Instant expireAt, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.expireAt(expireAt, keys); - } - - @Override - public ExpireChanges persist(Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (ExpireChanges) hashOps.persist(keys); - } - - @Override - public Expirations getExpire(Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (Expirations) hashOps.getExpire(keys); - } - - @Override - public Expirations getExpire(TimeUnit timeUnit, Collection hashKeys) { - - Collection keys = hashKeys.stream().map(key -> (String) key).toList(); - return (Expirations) hashOps.getExpire(timeUnit, keys); + public BoundHashFieldExpirationOperations expiration(Collection hashFields) { + return (BoundHashFieldExpirationOperations) delegate.expiration((Collection) hashFields); } } 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 ffa5bcd10..94b8db765 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -98,6 +98,7 @@ import org.springframework.data.redis.test.condition.LongRunningTest; import org.springframework.data.redis.test.condition.RedisDriver; import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.util.Streamable; +import org.springframework.util.ObjectUtils; /** * Base test class for AbstractConnection integration tests @@ -752,6 +753,45 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(stringSerializer.deserialize((byte[]) getResults().get(1))).isEqualTo("bar"); } + @Test + @EnabledOnCommand("HEXPIRE") + void testExecuteHashFieldExpiration() { + + actual.add(connection.hSet("foo", "bar", "field")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HEXPIRE", "foo", "100", "NX", "FIELDS", "1", "bar")); + actual.add(connection.execute("HPERSIST", "foo", "FIELDS", "1", "bar")); + actual.add(connection.execute("HTTL", "foo", "FIELDS", "1", "bar")); + + List results = getResults(); + + assertThat(deserializeList(results, 1, stringSerializer)).containsOnly(-1L); + assertThat(deserializeList(results, 2, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 3, stringSerializer)).containsOnly(1L); + assertThat(deserializeList(results, 4, stringSerializer)).containsOnly(-1L); + } + + List deserializeList(List objects, int index, RedisSerializer serializer) { + + List result = new ArrayList<>(); + Object o = objects.get(index); + if (o instanceof List ls) { + for (Object obj : ls) { + + if (obj instanceof byte[]) { + result.add(serializer.deserialize((byte[]) obj)); + } else { + result.add(obj); + } + } + + return result; + } + + throw new IllegalArgumentException( + "Object at index " + index + " is not a list but " + ObjectUtils.nullSafeToString(o)); + } + @Test void testExecuteNoArgs() { @@ -3436,7 +3476,7 @@ public abstract class AbstractConnectionIntegrationTests { @Test @EnabledOnCommand("HEXPIRE") public void hExpireReturnsSuccessAndSetsTTL() { - + actual.add(connection.hSet("hash-hexpire", "key-2", "value-2")); actual.add(connection.hExpire("hash-hexpire", 5L, "key-2")); actual.add(connection.hTtl("hash-hexpire", "key-2")); diff --git a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java index 4abd23dac..bbb84a76d 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsIntegrationTests.java @@ -15,9 +15,8 @@ */ package org.springframework.data.redis.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assumptions.assumeThat; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; import java.io.IOException; import java.time.Duration; @@ -30,15 +29,15 @@ import java.util.concurrent.TimeUnit; import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; + import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; import org.springframework.data.redis.connection.jedis.extension.JedisConnectionFactoryExtension; -import org.springframework.data.redis.core.Expirations.Expiration; import org.springframework.data.redis.core.ExpireChanges.ExpiryChangeState; +import org.springframework.data.redis.core.types.Expirations.TimeToLive; import org.springframework.data.redis.test.condition.EnabledOnCommand; import org.springframework.data.redis.test.extension.RedisStanalone; import org.springframework.data.redis.test.extension.parametrized.MethodSource; @@ -228,11 +227,11 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().expire(key, Duration.ofMillis(500), List.of(key1))) .satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1))).satisfies(expirations -> { + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1))).satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.SECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 1L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofSeconds(1)); }); @@ -259,16 +258,48 @@ public class DefaultHashOperationsIntegrationTests { assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); }); - assertThat(redisTemplate.opsForHash().getExpire(key, TimeUnit.SECONDS, List.of(key1, key2))) + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2))) .satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.SECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 5L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); }); } + @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") + void testBoundExpireAndGetExpireSeconds() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + BoundHashOperations hashOps = redisTemplate.boundHashOps(key); + BoundHashFieldExpirationOperations exp = hashOps.expiration(key1, key2); + + assertThat(exp.expire(Duration.ofSeconds(5))).satisfies(changes -> { + assertThat(changes.allOk()).isTrue(); + assertThat(changes.stateOf(key1)).isEqualTo(ExpiryChangeState.OK); + assertThat(changes.ok()).containsExactlyInAnyOrder(key1, key2); + assertThat(changes.missed()).isEmpty(); + assertThat(changes.stateChanges()).map(ExpiryChangeState::value).containsExactly(1L, 1L); + }); + + assertThat(exp.getTimeToLive(TimeUnit.SECONDS)).satisfies(expirations -> { + assertThat(expirations.missing()).isEmpty(); + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.SECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) + .isBetween(0L, 5L); + assertThat(expirations.ttlOf(key1)).isBetween(Duration.ofSeconds(1), Duration.ofSeconds(5)); + }); + } + @ParameterizedRedisTest @EnabledOnCommand("HEXPIRE") void testExpireAtAndGetExpireMillis() { @@ -284,27 +315,29 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().expireAt(key, Instant.now().plusMillis(500), List.of(key1, key2))) .satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, TimeUnit.MILLISECONDS, List.of(key1, key2))) + assertThat(redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.MILLISECONDS, List.of(key1, key2))) .satisfies(expirations -> { assertThat(expirations.missing()).isEmpty(); - assertThat(expirations.precision()).isEqualTo(TimeUnit.MILLISECONDS); - assertThat(expirations.expirationOf(key1)).extracting(Expiration::raw, InstanceOfAssertFactories.LONG) + assertThat(expirations.timeUnit()).isEqualTo(TimeUnit.MILLISECONDS); + assertThat(expirations.expirationOf(key1)).extracting(TimeToLive::raw, InstanceOfAssertFactories.LONG) .isBetween(0L, 500L); assertThat(expirations.ttlOf(key1)).isBetween(Duration.ZERO, Duration.ofMillis(500)); }); } @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") void expireThrowsErrorOfNanoPrecision() { K key = keyFactory.instance(); HK key1 = hashKeyFactory.instance(); assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> redisTemplate.opsForHash().getExpire(key, TimeUnit.NANOSECONDS, List.of(key1))); + .isThrownBy(() -> redisTemplate.opsForHash().getTimeToLive(key, TimeUnit.NANOSECONDS, List.of(key1))); } @ParameterizedRedisTest + @EnabledOnCommand("HEXPIRE") void testExpireWithOptionsNone() { K key = keyFactory.instance(); @@ -360,7 +393,7 @@ public class DefaultHashOperationsIntegrationTests { assertThat(redisTemplate.opsForHash().persist(key, List.of(key2))).satisfies(ExpireChanges::allOk); - assertThat(redisTemplate.opsForHash().getExpire(key, List.of(key1, key2))).satisfies(expirations -> { + assertThat(redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2))).satisfies(expirations -> { assertThat(expirations.expirationOf(key1).isPersistent()).isFalse(); assertThat(expirations.expirationOf(key2).isPersistent()).isTrue(); }); diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java index a128a2929..2d574ee12 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java @@ -15,11 +15,10 @@ */ package org.springframework.data.redis.core; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assumptions.assumeThat; -import static org.junit.jupiter.api.condition.OS.MAC; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assumptions.*; +import static org.junit.jupiter.api.condition.OS.*; -import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import reactor.test.StepVerifier; import java.time.Duration; @@ -34,10 +33,12 @@ import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.condition.DisabledOnOs; + import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.Hash.FieldExpirationOptions; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.connection.convert.Converters; @@ -523,7 +524,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - hashOperations.getExpire(key, List.of(key1)) // + hashOperations.getTimeToLive(key, List.of(key1)) // .as(StepVerifier::create) // .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); @@ -576,7 +577,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - hashOperations.getExpire(key, TimeUnit.SECONDS, List.of(key1, key2)) // + hashOperations.getTimeToLive(key, TimeUnit.SECONDS, List.of(key1, key2)) // .as(StepVerifier::create) // .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 5L); @@ -603,7 +604,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)).as(StepVerifier::create)// + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// .assertNext(it -> { assertThat(it.expirationOf(key1).raw()).isBetween(0L, 2L); assertThat(it.expirationOf(key2).raw()).isBetween(0L, 2L); @@ -633,7 +634,7 @@ public class DefaultReactiveHashOperationsIntegrationTests { assertThat(changes.allOk()).isTrue(); }).verifyComplete(); - redisTemplate.opsForHash().getExpire(key, List.of(key1, key2)).as(StepVerifier::create)// + redisTemplate.opsForHash().getTimeToLive(key, List.of(key1, key2)).as(StepVerifier::create)// .assertNext(expirations -> { assertThat(expirations.persistent()).contains(key1, key2); }).verifyComplete(); diff --git a/src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java similarity index 80% rename from src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java rename to src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java index 5fc1953d3..db5ba9860 100644 --- a/src/test/java/org/springframework/data/redis/core/ExpirationsUnitTest.java +++ b/src/test/java/org/springframework/data/redis/core/types/ExpirationsUnitTest.java @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.springframework.data.redis.core; +package org.springframework.data.redis.core.types; -import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.util.List; @@ -26,11 +26,14 @@ import java.util.stream.IntStream; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.EnumSource; -import org.springframework.data.redis.core.Expirations.Timeouts; + +import org.springframework.data.redis.core.types.Expirations.Timeouts; /** + * Unit test for {@link Expirations} + * * @author Christoph Strobl - * @since 2025/02 + * @author Mark Paluch */ class ExpirationsUnitTest { @@ -38,19 +41,19 @@ class ExpirationsUnitTest { static final String KEY_2 = "key-2"; static final String KEY_3 = "key-3"; - @ParameterizedTest + @ParameterizedTest // GH-3054 @EnumSource(TimeUnit.class) void expirationMemorizesSourceUnit(TimeUnit targetUnit) { Expirations exp = Expirations.of(targetUnit, List.of(KEY_1), new Timeouts(TimeUnit.SECONDS, List.of(120L))); - assertThat(exp.expirations().get(0)).satisfies(expiration -> { + assertThat(exp.ttl().get(0)).satisfies(expiration -> { assertThat(expiration.raw()).isEqualTo(120L); assertThat(expiration.value()).isEqualTo(targetUnit.convert(120, TimeUnit.SECONDS)); }); } - @Test + @Test // GH-3054 void expirationsCategorizesElements() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -60,7 +63,7 @@ class ExpirationsUnitTest { assertThat(exp.expiring()).containsExactly(Map.entry(KEY_3, Duration.ofMinutes(2))); } - @Test + @Test // GH-3054 void returnsNullForMissingElements() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -69,7 +72,7 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf("missing")).isNull(); } - @Test + @Test // GH-3054 void ttlReturnsDurationForEntriesWithTimeout() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -77,7 +80,7 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf(KEY_3)).isEqualTo(Duration.ofMinutes(2)); } - @Test + @Test // GH-3054 void ttlReturnsNullForPersistentAndMissingEntries() { Expirations exp = createExpirations(new Timeouts(TimeUnit.SECONDS, List.of(-2L, -1L, 120L))); @@ -86,6 +89,14 @@ class ExpirationsUnitTest { assertThat(exp.ttlOf(KEY_2)).isNull(); } + @Test // GH-3054 + void shouldRenderToString() { + + assertThat(Expirations.TimeToLive.PERSISTENT).hasToString("PERSISTENT"); + assertThat(Expirations.TimeToLive.MISSING).hasToString("MISSING"); + assertThat(Expirations.TimeToLive.of(1, TimeUnit.SECONDS)).hasToString("1 SECONDS"); + } + static Expirations createExpirations(Timeouts timeouts) { List keys = IntStream.range(1, timeouts.raw().size() + 1).mapToObj("key-%s"::formatted).toList(); diff --git a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java index 5ce8e5441..0a03b7340 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/AbstractRedisMapIntegrationTests.java @@ -15,9 +15,8 @@ */ package org.springframework.data.redis.support.collections; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assumptions.assumeThat; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.*; +import static org.assertj.core.api.Assumptions.*; import java.io.IOException; import java.text.DecimalFormat; @@ -36,14 +35,16 @@ import java.util.concurrent.TimeUnit; import org.assertj.core.api.Assumptions; import org.junit.jupiter.api.BeforeEach; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.data.redis.DoubleAsStringObjectFactory; import org.springframework.data.redis.LongAsStringObjectFactory; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.RedisSystemException; -import org.springframework.data.redis.core.ExpireChanges; +import org.springframework.data.redis.core.BoundHashFieldExpirationOperations; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ExpireChanges; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.RedisOperations; import org.springframework.data.redis.core.RedisTemplate; @@ -204,15 +205,15 @@ public abstract class AbstractRedisMapIntegrationTests { V v1 = getValue(); assertThat(map.put(k1, v1)).isEqualTo(null); - Collection keys = Collections.singletonList(k1); - assertThat(map.expire(Duration.ofSeconds(5), keys)).satisfies(ExpireChanges::allOk); - assertThat(map.getExpire(keys)).satisfies(expiration -> { + BoundHashFieldExpirationOperations ops = map.expiration(Collections.singletonList(k1)); + assertThat(ops.expire(Duration.ofSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); }); - assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> { + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); }); - assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); } @ParameterizedRedisTest @@ -223,15 +224,15 @@ public abstract class AbstractRedisMapIntegrationTests { V v1 = getValue(); assertThat(map.put(k1, v1)).isEqualTo(null); - Collection keys = Collections.singletonList(k1); - assertThat(map.expireAt(Instant.now().plusSeconds(5), keys)).satisfies(ExpireChanges::allOk); - assertThat(map.getExpire(keys)).satisfies(expiration -> { + BoundHashFieldExpirationOperations ops = map.expiration(Collections.singletonList(k1)); + assertThat(ops.expireAt(Instant.now().plusSeconds(5))).satisfies(ExpireChanges::allOk); + assertThat(ops.getTimeToLive()).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1L, 5L); }); - assertThat(map.getExpire(TimeUnit.MILLISECONDS, keys)).satisfies(expiration -> { + assertThat(ops.getTimeToLive(TimeUnit.MILLISECONDS)).satisfies(expiration -> { assertThat(expiration.expirationOf(k1).raw()).isBetween(1000L, 5000L); }); - assertThat(map.persist(keys)).satisfies(ExpireChanges::allOk); + assertThat(ops.persist()).satisfies(ExpireChanges::allOk); } @ParameterizedRedisTest diff --git a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java index 17e63c311..be3e627a2 100644 --- a/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/support/collections/RedisPropertiesIntegrationTests.java @@ -185,12 +185,6 @@ public class RedisPropertiesIntegrationTests extends RedisMapIntegrationTests { assertThat(keys.contains(key3)).isTrue(); } - @ParameterizedRedisTest - @Override - public void testScanWorksCorrectly() { - assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> super.testScanWorksCorrectly()); - } - // DATAREDIS-241 public static Collection testParams() {