From d2871ec032d90631fedeec0487a0799a4b72be50 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 3 May 2018 14:52:16 +0200 Subject: [PATCH] DATAREDIS-562 - Add reactive BitField support. Original pull request: #227. --- .../connection/ReactiveStringCommands.java | 80 +++++++++++++++++++ .../LettuceReactiveStringCommands.java | 17 +++- .../core/DefaultReactiveValueOperations.java | 13 +++ .../redis/core/ReactiveValueOperations.java | 13 +++ .../LettuceReactiveStringCommandsTests.java | 66 ++++++++++++++- ...activeValueOperationsIntegrationTests.java | 24 ++++++ 6 files changed, 208 insertions(+), 5 deletions(-) diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java index 8587143b6..4d7f2c37b 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -902,6 +902,86 @@ public interface ReactiveStringCommands { */ Flux> bitCount(Publisher commands); + /** + * {@code BITFIELD} command parameters. + * + * @author Mark Paluch + * @see Redis Documentation: BITFIELD + * @since 2.1 + */ + class BitFieldCommand extends KeyCommand { + + private @Nullable BitFieldSubCommands subcommands; + + private BitFieldCommand(ByteBuffer key, @Nullable BitFieldSubCommands subcommands) { + + super(key); + + this.subcommands = subcommands; + } + + /** + * Creates a new {@link BitFieldCommand} given a {@literal key}. + * + * @param key must not be {@literal null}. + * @return a new {@link BitFieldCommand} for a {@literal key}. + */ + public static BitFieldCommand bitField(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new BitFieldCommand(key, null); + } + + /** + * Applies the {@link BitFieldSubCommands}. Constructs a new command instance with all previously configured + * properties. + * + * @param commands must not be {@literal null}. + * @return a new {@link BitFieldSubCommands} with {@link BitFieldSubCommands} applied. + */ + public BitFieldCommand commands(BitFieldSubCommands commands) { + + Assert.notNull(commands, "BitFieldCommands must not be null!"); + + return new BitFieldCommand(getKey(), commands); + } + + public BitFieldSubCommands getSubCommands() { + return subcommands; + } + } + + /** + * Get / Manipulate specific integer fields of varying bit widths and arbitrary non (necessary) aligned offset stored + * at a given {@code key}. + * + * @param key must not be {@literal null}. + * @param subCommands + * @return + * @see Redis Documentation: BITFIELD + * @since 2.1 + */ + default Mono> bitField(ByteBuffer key, BitFieldSubCommands subCommands) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(subCommands, "BitFieldSubCommands must not be null!"); + + return bitField(Mono.just(BitFieldCommand.bitField(key).commands(subCommands))).map(CommandResponse::getOutput) + .next(); + } + + /** + * Get / Manipulate specific integer fields of varying bit widths and arbitrary non (necessary) aligned offset stored + * at a given {@code key}. + * + * @param commands must not be {@literal null}. + * @return + * @see Redis Documentation: BITFIELD + * @since 2.1 + */ + Flux> bitField(Publisher commands); + /** * {@code BITOP} command parameters. * diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java index 5dbe010e0..312db4ad5 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java @@ -15,12 +15,14 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.BitFieldArgs; import io.lettuce.core.SetArgs; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; import java.util.List; +import java.util.stream.Collectors; import org.reactivestreams.Publisher; import org.springframework.data.domain.Range; @@ -328,11 +330,20 @@ class LettuceReactiveStringCommands implements ReactiveStringCommands { /* * (non-Javadoc) - * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#bitCount(org.reactivestreams.Publisher) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#bitField(org.reactivestreams.Publisher) */ @Override - public Flux> bitField(Publisher commands) { - return null; + public Flux> bitField(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).concatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + BitFieldArgs args = LettuceConverters.toBitFieldArgs(command.getSubCommands()); + + return cmd.bitfield(command.getKey(), args).collectList().map(value -> new MultiValueResponse<>(command, + value.stream().map(v -> v.getValueOrElse(null)).collect(Collectors.toList()))); + })); } /* diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java index f45cb615d..2429b0b76 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -29,6 +29,7 @@ import java.util.Map; import java.util.function.Function; import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.BitFieldSubCommands; import org.springframework.data.redis.connection.ReactiveStringCommands; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; @@ -318,6 +319,18 @@ class DefaultReactiveValueOperations implements ReactiveValueOperations connection.getBit(rawKey(key), offset)); } + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#bitField(java.lang.Object, org.springframework.data.redis.connection.BitFieldSubCommands) + */ + @Override + public Mono> bitField(K key, BitFieldSubCommands subCommands) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(subCommands, "BitFieldSubCommands must not be null!"); + + return createMono(connection -> connection.bitField(rawKey(key), subCommands)); + } + /* (non-Javadoc) * @see org.springframework.data.redis.core.ReactiveValueOperations#delete(java.lang.Object) */ diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java index a99fde609..70c4b86ad 100644 --- a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -22,6 +22,8 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import org.springframework.data.redis.connection.BitFieldSubCommands; + /** * Reactive Redis operations for simple (or in Redis terminology 'string') values. * @@ -235,6 +237,17 @@ public interface ReactiveValueOperations { */ Mono getBit(K key, long offset); + /** + * Get / Manipulate specific integer fields of varying bit widths and arbitrary non (necessary) aligned offset stored + * at a given {@code key}. + * + * @param key must not be {@literal null}. + * @param command must not be {@literal null}. + * @return + * @since 2.1 + */ + Mono> bitField(K key, BitFieldSubCommands command); + /** * Removes the given {@literal key}. * diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java index 954deef7d..a32c0bce9 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java @@ -16,13 +16,17 @@ package org.springframework.data.redis.connection.lettuce; import static org.hamcrest.Matchers.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.contains; +import static org.hamcrest.core.Is.is; import static org.junit.Assert.*; import static org.junit.Assume.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldType.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.Offset.*; -import org.springframework.data.redis.util.ByteUtils; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import reactor.core.publisher.MonoOperator; import reactor.test.StepVerifier; import java.nio.ByteBuffer; @@ -46,6 +50,7 @@ import org.springframework.data.redis.connection.ReactiveStringCommands.SetComma import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.test.util.HexStringUtils; +import org.springframework.data.redis.util.ByteUtils; /** * @author Christoph Strobl @@ -346,6 +351,63 @@ public class LettuceReactiveStringCommandsTests extends LettuceReactiveCommandsT .verifyComplete(); } + @Test // DATAREDIS-562 + public void bitFieldSetShouldWorkCorrectly() { + + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, create().set(INT_8).valueAt(offset(0L)).to(10L))) + .expectNext(Collections.singletonList(0L)).verifyComplete(); + + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, create().set(INT_8).valueAt(offset(0L)).to(20L))) + .expectNext(Collections.singletonList(10L)).verifyComplete(); + } + + @Test // DATAREDIS-562 + public void bitFieldGetShouldWorkCorrectly() { + + StepVerifier.create(connection.stringCommands().bitField(KEY_1_BBUFFER, create().get(INT_8).valueAt(offset(0L)))) + .expectNext(Collections.singletonList(0L)).verifyComplete(); + } + + @Test // DATAREDIS-562 + public void bitFieldIncrByShouldWorkCorrectly() { + + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, create().incr(INT_8).valueAt(offset(100L)).by(1L))) + .expectNext(Collections.singletonList(1L)).verifyComplete(); + } + + @Test // DATAREDIS-562 + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, + create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(1L)).verifyComplete(); + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, + create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(2L)).verifyComplete(); + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, + create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(3L)).verifyComplete(); + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, + create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(null)).verifyComplete(); + } + + @Test // DATAREDIS-562 + public void bitfieldShouldAllowMultipleSubcommands() { + + StepVerifier + .create(connection.stringCommands().bitField(KEY_1_BBUFFER, + create().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L))) + .expectNext(Arrays.asList(1L, 0L)).verifyComplete(); + } + @Test // DATAREDIS-525 public void bitOpAndShouldWorkAsExpected() { diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java index 669d383eb..930eb6ac2 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java @@ -17,6 +17,10 @@ package org.springframework.data.redis.core; import static org.assertj.core.api.Assertions.*; import static org.junit.Assume.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldType.*; +import static org.springframework.data.redis.connection.BitFieldSubCommands.Offset.offset; import reactor.test.StepVerifier; @@ -24,6 +28,7 @@ import java.nio.ByteBuffer; import java.time.Duration; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.Map; @@ -372,6 +377,25 @@ public class DefaultReactiveValueOperationsIntegrationTests { StepVerifier.create(valueOperations.getBit(key, 1)).expectNext(false).expectComplete(); } + @Test // DATAREDIS-562 + public void bitField() { + + K key = keyFactory.instance(); + + StepVerifier + .create(valueOperations.bitField(key, create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(1L)).verifyComplete(); + StepVerifier + .create(valueOperations.bitField(key, create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(2L)).verifyComplete(); + StepVerifier + .create(valueOperations.bitField(key, create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(3L)).verifyComplete(); + StepVerifier + .create(valueOperations.bitField(key, create().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))) + .expectNext(Collections.singletonList(null)).verifyComplete(); + } + @Test // DATAREDIS-602 public void delete() {