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 3dbef2de5..37d1f1049 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -1160,6 +1160,16 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return convertAndReturn(delegate.bitOp(op, destination, keys), identityConverter); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#bitPos(byte[], boolean, org.springframework.data.domain.Range) + */ + @Nullable + @Override + public Long bitPos(byte[] key, boolean bit, org.springframework.data.domain.Range range) { + return convertAndReturn(delegate.bitPos(key, bit, range), identityConverter); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisPubSubCommands#subscribe(org.springframework.data.redis.connection.MessageListener, byte[][]) @@ -2470,6 +2480,16 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return bitOp(op, serialize(destination), serializeMulti(keys)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#bitPos(java.lang.String, boolean, org.springframework.data.domain.Range) + */ + @Nullable + @Override + public Long bitPos(String key, boolean bit, org.springframework.data.domain.Range range) { + return bitPos(serialize(key), bit, range); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#subscribe(org.springframework.data.redis.connection.MessageListener, java.lang.String[]) diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java index 110836632..d40766594 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -22,6 +22,7 @@ import java.util.Properties; import java.util.Set; import java.util.concurrent.TimeUnit; +import org.springframework.data.domain.Range; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -385,6 +386,13 @@ public interface DefaultedRedisConnection extends RedisConnection { return stringCommands().bitOp(op, destination, keys); } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default Long bitPos(byte[] key, boolean bit, org.springframework.data.domain.Range range) { + return stringCommands().bitPos(key, bit, range); + } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ @Override @Deprecated 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 2f3be93a6..8587143b6 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -1014,6 +1014,80 @@ public interface ReactiveStringCommands { */ Flux> bitOp(Publisher commands); + /** + * @author Christoph Strobl + * @since 2.1 + */ + class BitPosCommand extends KeyCommand { + + private boolean bit; + private Range range; + + private BitPosCommand(@Nullable ByteBuffer key, boolean bit, Range range) { + super(key); + this.bit = bit; + this.range = range; + } + + static BitPosCommand positionOf(boolean bit) { + return new BitPosCommand(null, bit, Range.unbounded()); + } + + public BitPosCommand in(ByteBuffer key) { + return new BitPosCommand(key, bit, range); + } + + public BitPosCommand within(Range range) { + return new BitPosCommand(getKey(), bit, range); + } + + public boolean getBit() { + return bit; + } + + public Range getRange() { + return range; + } + } + + /** + * Return the position of the first bit set to given {@code bit} in a string. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @return {@link Mono} emitting result when ready. + * @since 2.1 + */ + default Mono bitPos(ByteBuffer key, boolean bit) { + return bitPos(key, bit, Range.unbounded()); + } + + /** + * Return the position of the first bit set to given {@code bit} in a string. {@link Range} start and end can contain + * negative values in order to index bytes starting from the end of the string, where {@literal -1} + * is the last byte, {@literal -2} is the penultimate. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @param range must not be {@literal null}. Use {@link Range#unbounded()} to not limit search. + * @return {@link Mono} emitting result when ready. + * @since 2.1 + */ + default Mono bitPos(ByteBuffer key, boolean bit, Range range) { + return bitPos(Mono.just(BitPosCommand.positionOf(bit).in(key).within(range))).next() + .map(NumericResponse::getOutput); + } + + /** + * Emmit the the position of the first bit set to given {@code bit} in a string. Get the length of the value stored at + * {@literal key}. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} emitting results when ready. + * @since 2.1 + */ + Flux> bitPos(Publisher commands); + /** * Get the length of the value stored at {@literal key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index 2a800a9d8..1c479e6b7 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection; import java.util.List; import java.util.Map; +import org.springframework.data.domain.Range; import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; @@ -292,6 +293,37 @@ public interface RedisStringCommands { @Nullable Long bitOp(BitOperation op, byte[] destination, byte[]... keys); + /** + * Return the position of the first bit set to given {@code bit} in a string. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @return {@literal null} when used in pipeline / transaction. The position of the first bit set to 1 or 0 according + * to the request. + * @see Redis Documentation: BITPOS + * @since 2.1 + */ + @Nullable + default Long bitPos(byte[] key, boolean bit) { + return bitPos(key, bit, Range.unbounded()); + } + + /** + * Return the position of the first bit set to given {@code bit} in a string. {@link Range} start and end can contain + * negative values in order to index bytes starting from the end of the string, where {@literal -1} + * is the last byte, {@literal -2} is the penultimate. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @param range must not be {@literal null}. Use {@link Range#unbounded()} to not limit search. + * @return {@literal null} when used in pipeline / transaction. The position of the first bit set to 1 or 0 according + * to the request. + * @see Redis Documentation: BITPOS + * @since 2.1 + */ + @Nullable + Long bitPos(byte[] key, boolean bit, Range range); + /** * Get the length of the value stored at {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java index 9fe8a59bc..505716521 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -580,6 +580,37 @@ public interface StringRedisConnection extends RedisConnection { */ Long bitOp(BitOperation op, String destination, String... keys); + /** + * Return the position of the first bit set to given {@code bit} in a string. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @return {@literal null} when used in pipeline / transaction. The position of the first bit set to 1 or 0 according + * to the request. + * @see Redis Documentation: BITPOS + * @since 2.1 + */ + default Long bitPos(String key, boolean bit) { + return bitPos(key, bit, org.springframework.data.domain.Range.unbounded()); + } + + /** + * Return the position of the first bit set to given {@code bit} in a string. + * {@link org.springframework.data.domain.Range} start and end can contain negative values in order to index + * bytes starting from the end of the string, where {@literal -1} is the last byte, {@literal -2} is + * the penultimate. + * + * @param key the key holding the actual String. + * @param bit the bit value to look for. + * @param range must not be {@literal null}. Use {@link Range#unbounded()} to not limit search. + * @return {@literal null} when used in pipeline / transaction. The position of the first bit set to 1 or 0 according + * to the request. + * @see Redis Documentation: BITPOS + * @since 2.1 + */ + @Nullable + Long bitPos(String key, boolean bit, org.springframework.data.domain.Range range); + /** * Get the length of the value stored at {@code key}. * diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java index 8052a6da3..1fc33b169 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterStringCommands.java @@ -19,6 +19,7 @@ import lombok.NonNull; import lombok.RequiredArgsConstructor; import redis.clients.jedis.BinaryJedis; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.Map; @@ -26,11 +27,13 @@ import java.util.concurrent.TimeUnit; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.ClusterSlotHashUtil; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisClusterCommandCallback; import org.springframework.data.redis.connection.jedis.JedisClusterConnection.JedisMultiKeyClusterCommandCallback; +import org.springframework.data.redis.connection.lettuce.LettuceConverters; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.util.ByteUtils; import org.springframework.util.Assert; @@ -489,6 +492,29 @@ class JedisClusterStringCommands implements RedisStringCommands { throw new InvalidDataAccessApiUsageException("BITOP is only supported for same slot keys in cluster mode."); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitPos(byte[], boolean, org.springframework.data.Range) + */ + @Override + public Long bitPos(byte[] key, boolean bit, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null! Use Range.unbounded() instead."); + + List args = new ArrayList<>(3); + args.add(LettuceConverters.toBit(bit)); + + if (range.getLowerBound().isBounded()) { + args.add(range.getLowerBound().getValue().map(LettuceConverters::toBytes).get()); + } + if (range.getUpperBound().isBounded()) { + args.add(range.getUpperBound().getValue().map(LettuceConverters::toBytes).get()); + } + + return Long.class.cast(connection.execute("BITPOS", key, args)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#strLen(byte[]) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java index 976565102..336ef9ddd 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisStringCommands.java @@ -17,14 +17,17 @@ package org.springframework.data.redis.connection.jedis; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import redis.clients.jedis.BitPosParams; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -713,6 +716,43 @@ class JedisStringCommands implements RedisStringCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitOp(byte[], boolean, org.springframework.data.domain.Range) + */ + @Nullable + @Override + public Long bitPos(byte[] key, boolean bit, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null! Use Range.unbounded() instead."); + + BitPosParams params = null; + if (range.getLowerBound().isBounded()) { + params = range.getUpperBound().isBounded() + ? new BitPosParams(range.getLowerBound().getValue().get(), range.getUpperBound().getValue().get()) + : new BitPosParams(range.getLowerBound().getValue().get()); + } + + try { + if (isPipelined()) { + + pipeline(connection.newJedisResult(params != null ? connection.getRequiredPipeline().bitpos(key, bit, params) + : connection.getRequiredPipeline().bitpos(key, bit))); + return null; + } + if (isQueueing()) { + transaction( + connection.newJedisResult(params != null ? connection.getRequiredTransaction().bitpos(key, bit, params) + : connection.getRequiredTransaction().bitpos(key, bit))); + return null; + } + return params != null ? connection.getJedis().bitpos(key, bit, params) : connection.getJedis().bitpos(key, bit); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#strLen(byte[]) 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 451ad56ff..9e4fefe61 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 @@ -16,6 +16,10 @@ package org.springframework.data.redis.connection.lettuce; import io.lettuce.core.SetArgs; +import io.lettuce.core.codec.ByteArrayCodec; +import io.lettuce.core.output.IntegerOutput; +import io.lettuce.core.protocol.CommandArgs; +import io.lettuce.core.protocol.CommandType; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,6 +36,8 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiVa import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; import org.springframework.data.redis.connection.ReactiveStringCommands; +import org.springframework.data.redis.connection.convert.Converters; +import org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection.ByteBufferCodec; import org.springframework.util.Assert; /** @@ -320,9 +326,9 @@ class LettuceReactiveStringCommands implements ReactiveStringCommands { Range range = command.getRange(); - return (!Range.unbounded().equals(range) ? cmd.bitcount(command.getKey(), range.getLowerBound().getValue().orElse(null), - range.getUpperBound().getValue().orElse(null)) : cmd.bitcount(command.getKey())) - .map(responseValue -> new NumericResponse<>(command, responseValue)); + return (!Range.unbounded().equals(range) ? cmd.bitcount(command.getKey(), + range.getLowerBound().getValue().orElse(null), range.getUpperBound().getValue().orElse(null)) + : cmd.bitcount(command.getKey())).map(responseValue -> new NumericResponse<>(command, responseValue)); })); } @@ -366,6 +372,33 @@ class LettuceReactiveStringCommands implements ReactiveStringCommands { })); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#bitPos(org.reactivestreams.Publisher) + */ + @Override + public Flux> bitPos(Publisher commands) { + + return connection.execute(cmd -> { + + return Flux.from(commands).flatMap(command -> { + + Mono result = cmd.bitpos(command.getKey(), command.getBit()); + + if (command.getRange().getLowerBound().isBounded()) { + + result = cmd.bitpos(command.getKey(), command.getBit(), command.getRange().getLowerBound().getValue().get()); + + if (command.getRange().getUpperBound().isBounded()) { + result = cmd.bitpos(command.getKey(), command.getBit(), command.getRange().getLowerBound().getValue().get(), + command.getRange().getUpperBound().getValue().get()); + } + } + return result.map(respValue -> new NumericResponse<>(command, respValue)); + }); + }); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#strLen(org.reactivestreams.Publisher) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java index b22cc236c..c92b86367 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceStringCommands.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.RedisFuture; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; import lombok.NonNull; @@ -25,9 +26,11 @@ import java.util.Map; import java.util.concurrent.Future; import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Range; import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -666,6 +669,57 @@ class LettuceStringCommands implements RedisStringCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitPos(byte[], boolean, org.springframework.data.domain.Range) + */ + @Nullable + @Override + public Long bitPos(byte[] key, boolean bit, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null! Use Range.unbounded() instead."); + + try { + if (isPipelined() || isQueueing()) { + + RedisFuture futureResult; + if (range.getLowerBound().isBounded()) { + if (range.getUpperBound().isBounded()) { + futureResult = getAsyncConnection().bitpos(key, bit, range.getLowerBound().getValue().get(), + range.getUpperBound().getValue().get()); + } else { + + futureResult = getAsyncConnection().bitpos(key, bit, range.getLowerBound().getValue().get()); + } + + } else { + futureResult = getAsyncConnection().bitpos(key, bit); + } + + if (isPipelined()) { + pipeline(connection.newLettuceResult(futureResult)); + } + else if (isQueueing()) { + transaction(connection.newLettuceResult(futureResult)); + } + return null; + } + + if (range.getLowerBound().isBounded()) { + if (range.getUpperBound().isBounded()) { + return getConnection().bitpos(key, bit, range.getLowerBound().getValue().get(), + range.getUpperBound().getValue().get()); + } + return getConnection().bitpos(key, bit, range.getLowerBound().getValue().get()); + } + + return getConnection().bitpos(key, bit); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#strLen(byte[]) 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 6c51c596b..987891798 100644 --- a/src/main/java/org/springframework/data/redis/core/RedisCommand.java +++ b/src/main/java/org/springframework/data/redis/core/RedisCommand.java @@ -41,7 +41,7 @@ public enum RedisCommand { BGSAVE("r", 0, 0), // BITCOUNT("r", 1, 3), // BITOP("rw", 3), // - BITPOS("r", 2, 4), // + BITPOS("r", 2), // BLPOP("rw", 2), // BRPOP("rw", 2), // BRPOPLPUSH("rw", 3), // 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 a51279cf0..df4d7f11f 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -44,6 +44,7 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -70,6 +71,8 @@ import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.redis.test.util.RedisClientRule; import org.springframework.data.redis.test.util.RedisDriver; import org.springframework.data.redis.test.util.WithRedisDriver; @@ -2822,6 +2825,27 @@ public abstract class AbstractConnectionIntegrationTests { verifyResults(Arrays.asList(new Object[] { 0L })); } + @Test // DATAREDIS-697 + @IfProfileValue(name = "redisVersion", value = "2.8.7+") + public void bitPosShouldReturnPositionCorrectly() { + + actual.add(connection.set("bitpos-1".getBytes(), HexStringUtils.hexToBytes("fff000"))); + actual.add(connection.bitPos("bitpos-1", false)); + + verifyResults(Arrays.asList(new Object[] { true, 12L })); + } + + @Test // DATAREDIS-697 + @IfProfileValue(name = "redisVersion", value = "2.8.7+") + public void bitPosShouldReturnPositionInRangeCorrectly() { + + actual.add(connection.set("bitpos-1".getBytes(), HexStringUtils.hexToBytes("fff0f0"))); + actual.add(connection.bitPos("bitpos-1", true, + org.springframework.data.domain.Range.of(Bound.inclusive(2L), Bound.unbounded()))); + + verifyResults(Arrays.asList(new Object[] { true, 16L })); + } + protected void verifyResults(List expected) { assertEquals(expected, getResults()); } @@ -2845,4 +2869,5 @@ public abstract class AbstractConnectionIntegrationTests { return (!connection.exists(key)); } } + } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java index 1f87272f5..58aa0bce8 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisClusterConnectionTests.java @@ -38,6 +38,7 @@ import org.junit.Rule; import org.junit.Test; import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -59,6 +60,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.redis.test.util.MinimumRedisVersionRule; import org.springframework.data.redis.test.util.RedisClusterRule; import org.springframework.test.annotation.IfProfileValue; @@ -2271,4 +2273,23 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { assertThat(clusterConnection.keyCommands().unlink(KEY_1_BYTES), is(0L)); } + @Test // DATAREDIS-697 + @IfProfileValue(name = "redisVersion", value = "2.8.7+") + public void bitPosShouldReturnPositionCorrectly() { + + nativeConnection.set(KEY_1_BYTES, HexStringUtils.hexToBytes("fff000")); + + assertThat(clusterConnection.stringCommands().bitPos(KEY_1_BYTES, false), is(12L)); + } + + @Test // DATAREDIS-697 + @IfProfileValue(name = "redisVersion", value = "2.8.7+") + public void bitPosShouldReturnPositionInRangeCorrectly() { + + nativeConnection.set(KEY_1_BYTES, HexStringUtils.hexToBytes("fff0f0")); + + assertThat(clusterConnection.stringCommands().bitPos(KEY_1_BYTES, true, + org.springframework.data.domain.Range.of(Bound.inclusive(2L), Bound.unbounded())), is(16L)); + } + } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java index 231c62d7d..8507abed8 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceClusterConnectionTests.java @@ -26,6 +26,7 @@ import io.lettuce.core.RedisURI.Builder; import io.lettuce.core.api.sync.RedisHLLCommands; import io.lettuce.core.cluster.RedisClusterClient; import io.lettuce.core.cluster.api.sync.RedisAdvancedClusterCommands; +import io.lettuce.core.codec.ByteArrayCodec; import java.nio.charset.Charset; import java.time.Duration; @@ -38,6 +39,7 @@ import org.junit.ClassRule; import org.junit.Rule; import org.junit.Test; import org.springframework.dao.DataAccessException; +import org.springframework.data.domain.Range.Bound; import org.springframework.data.geo.Circle; import org.springframework.data.geo.Distance; import org.springframework.data.geo.GeoResults; @@ -54,6 +56,7 @@ import org.springframework.data.redis.connection.jedis.JedisConverters; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.test.util.HexStringUtils; import org.springframework.data.redis.test.util.MinimumRedisVersionRule; import org.springframework.data.redis.test.util.RedisClusterRule; import org.springframework.test.annotation.IfProfileValue; @@ -89,6 +92,7 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { RedisClusterClient client; RedisAdvancedClusterCommands nativeConnection; + RedisAdvancedClusterCommands binaryConnection; LettuceClusterConnection clusterConnection; public static @ClassRule RedisClusterRule clusterAvailable = new RedisClusterRule(); @@ -104,6 +108,7 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { client = RedisClusterClient.create(LettuceTestClientResources.getSharedClientResources(), Builder.redis(CLUSTER_HOST, MASTER_NODE_1_PORT).withTimeout(500, TimeUnit.MILLISECONDS).build()); nativeConnection = client.connect().sync(); + binaryConnection = client.connect(ByteArrayCodec.INSTANCE).sync(); clusterConnection = new LettuceClusterConnection(client); } @@ -112,6 +117,7 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { clusterConnection.serverCommands().flushDb(); nativeConnection.getStatefulConnection().close(); + binaryConnection.getStatefulConnection().close(); clusterConnection.close(); client.shutdown(0, 0, TimeUnit.MILLISECONDS); } @@ -2294,4 +2300,21 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { public void unlinkReturnsZeroIfNoKeysTouched() { assertThat(clusterConnection.keyCommands().unlink(KEY_1_BYTES), is(0L)); } + + @Test // DATAREDIS-697 + public void bitPosShouldReturnPositionCorrectly() { + + binaryConnection.set(KEY_1_BYTES, HexStringUtils.hexToBytes("fff000")); + + assertThat(clusterConnection.stringCommands().bitPos(KEY_1_BYTES, false), is(12L)); + } + + @Test // DATAREDIS-697 + public void bitPosShouldReturnPositionInRangeCorrectly() { + + binaryConnection.set(KEY_1_BYTES, HexStringUtils.hexToBytes("fff0f0")); + + assertThat(clusterConnection.stringCommands().bitPos(KEY_1_BYTES, true, + org.springframework.data.domain.Range.of(Bound.inclusive(2L), Bound.unbounded())), is(16L)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java index e39670e38..30e7c71c4 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java @@ -31,6 +31,7 @@ import org.junit.After; import org.junit.Before; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import org.springframework.data.redis.connection.lettuce.LettuceReactiveRedisConnection.ByteBufferCodec; import org.springframework.data.redis.test.util.LettuceRedisClientProvider; import org.springframework.data.redis.test.util.LettuceRedisClusterClientProvider; @@ -75,10 +76,12 @@ public abstract class LettuceReactiveCommandsTestsBase { @Parameterized.Parameter(value = 0) public LettuceConnectionProvider connectionProvider; @Parameterized.Parameter(value = 1) public LettuceConnectionProvider nativeConnectionProvider; - @Parameterized.Parameter(value = 2) public Object displayName; + @Parameterized.Parameter(value = 2) public LettuceConnectionProvider nativeBinaryConnectionProvider; + @Parameterized.Parameter(value = 3) public Object displayName; LettuceReactiveRedisConnection connection; RedisClusterCommands nativeCommands; + RedisClusterCommands nativeBinaryCommands; @Parameterized.Parameters(name = "{2}") public static List parameters() { @@ -92,11 +95,14 @@ public abstract class LettuceReactiveCommandsTestsBase { LettuceReactiveRedisConnection.CODEC); StandaloneConnectionProvider nativeConnectionProvider = new StandaloneConnectionProvider(standalone.getClient(), StringCodec.UTF8); + StandaloneConnectionProvider nativeBinaryConnectionProvider = new StandaloneConnectionProvider( + standalone.getClient(), ByteBufferCodec.INSTANCE); - parameters.add(new Object[] { standaloneProvider, nativeConnectionProvider, "Standalone" }); + parameters.add( + new Object[] { standaloneProvider, nativeConnectionProvider, nativeBinaryConnectionProvider, "Standalone" }); parameters.add(new Object[] { new LettucePoolingConnectionProvider(standaloneProvider, LettucePoolingClientConfiguration.builder().build()), - nativeConnectionProvider, "Standalone/Pooled" }); + nativeConnectionProvider, nativeBinaryConnectionProvider, "Standalone/Pooled" }); if (cluster.test()) { @@ -104,8 +110,11 @@ public abstract class LettuceReactiveCommandsTestsBase { LettuceReactiveRedisConnection.CODEC); ClusterConnectionProvider nativeClusterConnectionProvider = new ClusterConnectionProvider(cluster.getClient(), StringCodec.UTF8); + ClusterConnectionProvider nativeBinaryClusterConnectionProvider = new ClusterConnectionProvider( + cluster.getClient(), ByteBufferCodec.INSTANCE); - parameters.add(new Object[] { clusterProvider, nativeClusterConnectionProvider, "Cluster" }); + parameters.add(new Object[] { clusterProvider, nativeClusterConnectionProvider, + nativeBinaryClusterConnectionProvider, "Cluster" }); } return parameters; @@ -116,11 +125,13 @@ public abstract class LettuceReactiveCommandsTestsBase { if (nativeConnectionProvider instanceof StandaloneConnectionProvider) { nativeCommands = nativeConnectionProvider.getConnection(StatefulRedisConnection.class).sync(); + nativeBinaryCommands = nativeBinaryConnectionProvider.getConnection(StatefulRedisConnection.class).sync(); this.connection = new LettuceReactiveRedisConnection(connectionProvider); } else { ClusterConnectionProvider clusterConnectionProvider = (ClusterConnectionProvider) nativeConnectionProvider; nativeCommands = nativeConnectionProvider.getConnection(StatefulRedisClusterConnection.class).sync(); + nativeBinaryCommands = nativeBinaryConnectionProvider.getConnection(StatefulRedisClusterConnection.class).sync(); this.connection = new LettuceReactiveRedisClusterConnection(connectionProvider, clusterConnectionProvider.getRedisClient()); } 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 58d0e1d93..954deef7d 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 @@ -22,6 +22,7 @@ import static org.junit.Assume.*; 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; @@ -36,6 +37,7 @@ import java.util.stream.Collectors; import java.util.stream.Stream; import org.junit.Test; +import org.springframework.data.domain.Range.Bound; import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; @@ -43,7 +45,7 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiVa import org.springframework.data.redis.connection.ReactiveStringCommands.SetCommand; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; import org.springframework.data.redis.core.types.Expiration; -import org.springframework.data.redis.test.util.LettuceRedisClientProvider; +import org.springframework.data.redis.test.util.HexStringUtils; /** * @author Christoph Strobl @@ -399,4 +401,22 @@ public class LettuceReactiveStringCommandsTests extends LettuceReactiveCommandsT .expectNext(7L) // .verifyComplete(); } + + @Test // DATAREDIS-697 + public void bitPosShouldReturnPositionCorrectly() { + + nativeBinaryCommands.set(KEY_1_BBUFFER, ByteBuffer.wrap(HexStringUtils.hexToBytes("fff000"))); + + StepVerifier.create(connection.stringCommands().bitPos(KEY_1_BBUFFER, false)).expectNext(12L).verifyComplete(); + } + + @Test // DATAREDIS-697 + public void bitPosShouldReturnPositionInRangeCorrectly() { + + nativeBinaryCommands.set(KEY_1_BBUFFER, ByteBuffer.wrap(HexStringUtils.hexToBytes("fff0f0"))); + + StepVerifier.create(connection.stringCommands().bitPos(KEY_1_BBUFFER, true, + org.springframework.data.domain.Range.of(Bound.inclusive(2L), Bound.unbounded()))).expectNext(16L).verifyComplete(); + } + } diff --git a/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java b/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java new file mode 100644 index 000000000..bd841d5d0 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/HexStringUtils.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 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 + * + * http://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.test.util; + +import org.springframework.util.Assert; + +/** + * Utils for working with Hex Stings. + * + * @author Christoph Strobl + * @currentRead Beyong the Shadows - Brent Weeks + */ +public class HexStringUtils { + + /** + * Convert a given HEX {@link String} to its byte representation. + * + * @param source must not be {@literal null}. + * @return + */ + public static byte[] hexToBytes(String source) { + + Assert.notNull(source, "Source must not be null!"); + int len = source.length(); + + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(source.charAt(i), 16) << 4) + Character.digit(source.charAt(i + 1), 16)); + } + return data; + } +}