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 5e559d72fa..dd71397a77 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -289,6 +289,11 @@ public Boolean copy(byte[] sourceKey, byte[] targetKey, boolean replace) { return convertAndReturn(delegate.copy(sourceKey, targetKey, replace), Converters.identityConverter()); } + @Override + public @Nullable String digest(byte @NonNull [] key) { + return convertAndReturn(delegate.digest(key), Converters.identityConverter()); + } + @Override public Long dbSize() { return convertAndReturn(delegate.dbSize(), Converters.identityConverter()); @@ -1451,6 +1456,11 @@ public Boolean copy(String sourceKey, String targetKey, boolean replace) { return copy(serialize(sourceKey), serialize(targetKey), replace); } + @Override + public @Nullable String digest(@NonNull String key) { + return digest(serialize(key)); + } + @Override public Long decr(String key) { return decr(serialize(key)); 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 6605b22ef9..6d8c5e3b6b 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -84,6 +84,12 @@ default Boolean copy(byte[] sourceKey, byte[] targetKey, boolean replace) { return keyCommands().copy(sourceKey, targetKey, replace); } + @Override + @Deprecated + default String digest(byte[] key) { + return keyCommands().digest(key); + } + /** @deprecated in favor of {@link RedisConnection#keyCommands()}. */ @Override @Deprecated diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index 10b37e3d92..84cff3cc94 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -164,6 +164,33 @@ default Mono copy(ByteBuffer sourceKey, ByteBuffer targetKey, boolean r */ Flux> copy(Publisher commands); + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param key must not be {@literal null}. + * @return {@link Mono} emitting the digest string. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + default Mono digest(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null"); + + return digest(Mono.just(new KeyCommand(key))).next().map(CommandResponse::getOutput); + } + + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param keys must not be {@literal null}. + * @return {@link Flux} of {@link CommandResponse} holding the {@literal key} along with the digest. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + Flux> digest(Publisher keys); + /** * Determine if given {@literal key} exists. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java index 32a3c870ba..3bd0a66ba3 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisKeyCommands.java @@ -54,6 +54,17 @@ public interface RedisKeyCommands { */ Boolean copy(byte @NonNull [] sourceKey, byte @NonNull [] targetKey, boolean replace); + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + @Nullable String digest(byte @NonNull [] key); + /** * Determine if given {@code key} exists. * 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 f8609b2cf1..47c160fc36 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -168,6 +168,17 @@ interface StringTuple extends Tuple { */ Boolean copy(@NonNull String sourceKey, @NonNull String targetKey, boolean replace); + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param key must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + @Nullable String digest(@NonNull String key); + /** * Unlink the {@code keys} from the keyspace. Unlike with {@link #del(String...)} the actual memory reclaiming here * happens asynchronously. diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java index dcafdb621c..e39517a1c2 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterKeyCommands.java @@ -82,6 +82,14 @@ public Boolean copy(byte @NonNull [] sourceKey, byte @NonNull [] targetKey, bool return connection.getCluster().copy(sourceKey, targetKey, replace); } + @Override + public @Nullable String digest(byte @NonNull [] key) { + + Assert.notNull(key, "Key must not be null"); + + return JedisConverters.toString(connection.getCluster().digestKey(key)); + } + @Override public Long del(byte @NonNull [] @NonNull... keys) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java index 3f497aec6e..8459fdb55c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisKeyCommands.java @@ -112,6 +112,15 @@ public Boolean copy(byte @NonNull [] sourceKey, byte @NonNull [] targetKey, bool replace); } + @Override + public @Nullable String digest(byte @NonNull [] key) { + + Assert.notNull(key, "Key must not be null"); + + return connection.invoke().from(JedisBinaryCommands::digestKey, PipelineBinaryCommands::digestKey, key) + .get(JedisConverters::toString); + } + @Override public Long unlink(byte @NonNull [] @NonNull... keys) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java index 628052c50b..7db4edb947 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceKeyCommands.java @@ -72,6 +72,14 @@ public Boolean copy(byte @NonNull [] sourceKey, byte @NonNull [] targetKey, bool CopyArgs.Builder.replace(replace)); } + @Override + public @Nullable String digest(byte @NonNull [] key) { + + Assert.notNull(key, "Key must not be null"); + + return connection.invoke().just(RedisKeyAsyncCommands::digestKey, key); + } + @Override public Boolean exists(byte @NonNull [] key) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java index 02e6523459..0f6a02a0c7 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java @@ -84,6 +84,17 @@ public Flux> copy(Publisher commands) })); } + @Override + public Flux> digest(Publisher keys) { + + return connection.execute(cmd -> Flux.from(keys).concatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null"); + + return cmd.digestKey(command.getKey()).map((value) -> new CommandResponse<>(command, value)); + })); + } + @Override public Flux> exists(Publisher commands) { diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java index 6fba55daed..7707ff07f3 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java @@ -236,6 +236,17 @@ default Mono>> listenToPatternLater(String... */ Mono copy(K sourceKey, K targetKey, boolean replace); + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param key must not be {@literal null}. + * @return string of the hash digest. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + Mono getDigest(K key); + /** * Determine if given {@code key} exists. * diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java index d7091e448c..4482b05bab 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java @@ -323,6 +323,14 @@ public Mono copy(K sourceKey, K targetKey, boolean replace) { return doCreateMono(connection -> connection.keyCommands().copy(rawKey(sourceKey), rawKey(targetKey), replace)); } + @Override + public Mono getDigest(K key) { + + Assert.notNull(key, "Key must not be null"); + + return doCreateMono(connection -> connection.keyCommands().digest(rawKey(key))); + } + @Override public Mono hasKey(K 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 06c11acc30..6e94ec5874 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -88,6 +88,7 @@ public enum RedisCommand { DECRBY("w", 2, 2), // DEL("rw", 1), // DELEX("w", 3), // + DIGEST("r", 1, 1), // DISCARD("rw", 0, 0), // DUMP("r", 1, 1), // 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 e1ebb6eb29..8330161496 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisOperations.java +++ b/src/main/java/org/springframework/data/redis/core/RedisOperations.java @@ -196,6 +196,17 @@ public interface RedisOperations { */ Boolean copy(@NonNull K sourceKey, @NonNull K targetKey, boolean replace); + /** + * Get the hash digest for the value stored in the specified key as a hexadecimal string. This command is intended to + * be used with string values only. + * + * @param key must not be {@literal null}. + * @return the digest of the key. {@literal null} when used in pipeline / transaction. + * @see Redis Documentation: DIGEST + * @since 4.1 + */ + String getDigest(@NonNull K key); + /** * Determine if given {@code key} exists. * diff --git a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java index 10664e18a5..2d00eb67df 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisTemplate.java +++ b/src/main/java/org/springframework/data/redis/core/RedisTemplate.java @@ -576,6 +576,14 @@ public Boolean copy(K source, K target, boolean replace) { return doWithKeys(connection -> connection.copy(sourceKey, targetKey, replace)); } + @Override + public @Nullable String getDigest(@NonNull K key) { + + byte[] rawKey = rawKey(key); + + return doWithKeys(connection -> connection.digest(rawKey)); + } + @Override public Boolean hasKey(K key) { diff --git a/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt b/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt index 86cfb79d88..04f6536eda 100644 --- a/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt +++ b/src/main/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensions.kt @@ -253,3 +253,11 @@ suspend fun ReactiveRedisOperations.moveAndAwait(key: K * @since 2.2 */ suspend fun ReactiveRedisOperations.getExpireAndAwait(key: K): Duration? = getExpire(key).awaitFirstOrNull() + +/** + * Coroutines variant of [ReactiveRedisOperations.getDigest]. + * + * @author Yordan Tsintsov + * @since 4.1 + */ +suspend fun ReactiveRedisOperations.getDigestAndAwait(key: K): String? = getDigest(key).awaitFirstOrNull() 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 6ed5f0e30a..c27a48f8eb 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -629,6 +629,46 @@ void testCopy() { assertThat(connection.exists("foo")).isTrue(); } + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnDigestForExistingKey() { + + String key = "digest-" + UUID.randomUUID(); + actual.add(connection.set(key, "bar")); + actual.add(connection.digest(key)); + + List results = getResults(); + assertThat(results.get(0)).isEqualTo(Boolean.TRUE); + assertThat(results.get(1)).isNotNull(); + assertThat(results.get(1)).isInstanceOf(String.class); + assertThat(((String) results.get(1))).hasSize(16); + } + + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnNullForNonExistingKey() { + + actual.add(connection.digest("nonexistent")); + + List results = getResults(); + assertThat(results.get(0)).isNull(); + } + + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnConsistentValueForSameContent() { + + String key1 = "digest-1-" + UUID.randomUUID(); + String key2 = "digest-2-" + UUID.randomUUID(); + actual.add(connection.set(key1, "same-value")); + actual.add(connection.set(key2, "same-value")); + actual.add(connection.digest(key1)); + actual.add(connection.digest(key2)); + + List results = getResults(); + assertThat(results.get(2)).isEqualTo(results.get(3)); + } + @Test public void testInfo() { diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java index 6ef137629c..cbbf48566c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsIntegrationTests.java @@ -92,6 +92,45 @@ void existsKeyReturnsZeroWhenKeysDoNotExist() { .expectNext(0L).verifyComplete(); } + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnDigestForExistingKey() { + + nativeCommands.set(KEY_1, VALUE_1); + + connection.keyCommands().digest(KEY_1_BBUFFER).as(StepVerifier::create) + .assertNext(digest -> { + assertThat(digest).isNotNull(); + assertThat(digest).isInstanceOf(String.class); + assertThat(digest).hasSize(16); + }) + .verifyComplete(); + } + + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnEmptyForNonExistingKey() { + + connection.keyCommands().digest(KEY_1_BBUFFER).as(StepVerifier::create) + .verifyComplete(); + } + + @Test + @EnabledOnCommand("DIGEST") + void digestShouldReturnSameValueForSameContent() { + + nativeCommands.set(KEY_1, "same-value"); + nativeCommands.set(KEY_2, "same-value"); + + Mono digest1 = connection.keyCommands().digest(KEY_1_BBUFFER); + Mono digest2 = connection.keyCommands().digest(KEY_2_BBUFFER); + + Mono.zip(digest1, digest2) + .as(StepVerifier::create) + .assertNext(tuple -> assertThat(tuple.getT1()).isEqualTo(tuple.getT2())) + .verifyComplete(); + } + @Test // DATAREDIS-525 void typeShouldReturnTypeCorrectly() { diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java index 8556a3deea..cf36a5bc74 100644 --- a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java @@ -122,6 +122,23 @@ void copy() { redisTemplate.opsForValue().get(targetKey).as(StepVerifier::create).expectNext(nextValue).verifyComplete(); } + @Test + @EnabledOnCommand("DIGEST") + void digest() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + redisTemplate.opsForValue().set(key, value).as(StepVerifier::create).expectNext(true).verifyComplete(); + redisTemplate.getDigest(key).as(StepVerifier::create).assertNext(digest -> { + assertThat(digest).isNotNull(); + assertThat(digest).hasSize(16); + }).verifyComplete(); + + K nonExistingKey = keyFactory.instance(); + redisTemplate.getDigest(nonExistingKey).as(StepVerifier::create).verifyComplete(); + } + @Test // DATAREDIS-602 void exists() { diff --git a/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java index e23e898b89..efe4d92f09 100644 --- a/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/RedisTemplateIntegrationTests.java @@ -515,6 +515,28 @@ void testCopy() { assertThat(redisTemplate.opsForValue().get(key2)).isEqualTo(value2); } + @Test + @EnabledOnCommand("DIGEST") + void testDigest() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + V sameValue = valueFactory.instance(); + + redisTemplate.opsForValue().set(key1, sameValue); + redisTemplate.opsForValue().set(key2, sameValue); + + String digest1 = redisTemplate.getDigest(key1); + String digest2 = redisTemplate.getDigest(key2); + + assertThat(digest1).isNotNull(); + assertThat(digest1).hasSize(16); + assertThat(digest2).isEqualTo(digest1); + + K nonExistingKey = keyFactory.instance(); + assertThat(redisTemplate.getDigest(nonExistingKey)).isNull(); + } + @Test // DATAREDIS-688 void testDeleteMultiple() { diff --git a/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt b/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt index f9a8a870ab..761f7f199d 100644 --- a/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt +++ b/src/test/kotlin/org/springframework/data/redis/core/ReactiveRedisOperationsExtensionsUnitTests.kt @@ -451,4 +451,19 @@ class ReactiveRedisOperationsExtensionsUnitTests { operations.getExpire("foo") } } + + @Test + fun digest() { + + val operations = mockk>() + every { operations.getDigest(any()) } returns Mono.just("1234") + + runBlocking { + assertThat(operations.getDigestAndAwait("foo")).isEqualTo("1234") + } + + verify { + operations.getDigest("foo") + } + } }