diff --git a/src/main/asciidoc/appendix/appendix-command-reference.adoc b/src/main/asciidoc/appendix/appendix-command-reference.adoc index ddf2c9335..8c3bdc1b7 100644 --- a/src/main/asciidoc/appendix/appendix-command-reference.adoc +++ b/src/main/asciidoc/appendix/appendix-command-reference.adoc @@ -13,6 +13,7 @@ |BGREWRITEAOF |X |BGSAVE |X |BITCOUNT |X +|BITFIELD |X |BITOP |X |BLPOP |X |BRPOP |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 7016d2cc1..bf1196cdd 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -3657,6 +3657,29 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return convertedResults; } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitfield(byte[], BitfieldCommand) + */ + @Override + public List bitfield(byte[] key, BitfieldCommand operation) { + return delegate.bitfield(key, operation); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#bitfield(byte[], BitfieldCommand) + */ + @Override + public List bitfield(String key, BitfieldCommand operation) { + + List results = delegate.bitfield(serialize(key), operation); + if (isFutureConversion()) { + addResultConverter(identityConverter); + } + return results; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.DecoratedRedisConnection#getDelegate() 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 b75aab1f2..f777b786e 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -401,6 +401,13 @@ public interface DefaultedRedisConnection extends RedisConnection { return stringCommands().bitCount(key, start, end); } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ + @Override + @Deprecated + default List bitfield(byte[] key, BitfieldCommand operation) { + return stringCommands().bitfield(key, operation); + } + /** @deprecated in favor of {@link RedisConnection#stringCommands()}}. */ @Override @Deprecated 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 1c479e6b7..0550681fa 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -15,11 +15,15 @@ */ package org.springframework.data.redis.connection; +import java.util.ArrayList; +import java.util.Collections; 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.util.Assert; +import org.springframework.util.ObjectUtils; import org.springframework.lang.Nullable; /** @@ -312,7 +316,7 @@ public interface RedisStringCommands { * 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. @@ -334,6 +338,18 @@ public interface RedisStringCommands { @Nullable Long strLen(byte[] key); + /** + * 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 operation must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + */ + @Nullable + List bitfield(byte[] key, BitfieldCommand operation); + /** * {@code SET} command arguments for {@code NX}, {@code XX}. * @@ -390,4 +406,581 @@ public interface RedisStringCommands { return SET_IF_ABSENT; } } + + /** + * The actual {@code BITFIELD} command representation holding several {@link BitfieldSubCommand}s to execute. + * + * @author Christoph Strobl + * @since 1.8 + */ + class BitfieldCommand { + + private final List subCommands; + + private BitfieldCommand(List subCommands) { + + this.subCommands = new ArrayList( + subCommands != null ? subCommands : Collections. emptyList()); + } + + private BitfieldCommand(List subCommands, BitfieldSubCommand subCommand) { + + this(subCommands); + + Assert.notNull(subCommand, "SubCommand must not be null!"); + this.subCommands.add(subCommand); + } + + /** + * Creates a new {@link BitfieldCommand}. + * + * @return + */ + public static BitfieldCommand newBitfieldCommand() { + return new BitfieldCommand(Collections. emptyList()); + } + + /** + * Obtain a new {@link BitfieldGetBuilder} for creating and adding a {@link BitfieldGet} sub command. + * + * @param type must not be {@literal null}. + * @return + */ + public BitfieldGetBuilder get(BitfieldType type) { + return new BitfieldGetBuilder(this).forType(type); + } + + /** + * Create new {@link BitfieldCommand} adding given {@link BitfieldGet} to the sub commands. + * + * @param get must not be {@literal null}. + * @return + */ + protected BitfieldCommand get(BitfieldGet get) { + return new BitfieldCommand(subCommands, get); + } + + /** + * Obtain a new {@link BitfieldSetBuilder} for creating and adding a {@link BitfieldSet} sub command. + * + * @param type must not be {@literal null}. + * @return + */ + public BitfieldSetBuilder set(BitfieldType type) { + return new BitfieldSetBuilder(this).forType(type); + } + + /** + * Create new {@link BitfieldCommand} adding given {@link BitfieldSet} to the sub commands. + * + * @param get must not be {@literal null}. + * @return + */ + protected BitfieldCommand set(BitfieldSet set) { + return new BitfieldCommand(subCommands, set); + } + + /** + * Obtain a new {@link BitfieldIncrByBuilder} for creating and adding a {@link BitfieldIncrby} sub command. + * + * @param type must not be {@literal null}. + * @return + */ + public BitfieldIncrByBuilder incr(BitfieldType type) { + return new BitfieldIncrByBuilder(this).forType(type); + } + + /** + * Create new {@link BitfieldCommand} adding given {@link BitfieldIncrBy} to the sub commands. + * + * @param get must not be {@literal null}. + * @return + */ + protected BitfieldCommand incr(BitfieldIncrBy incrBy) { + return new BitfieldCommand(subCommands, incrBy); + } + + /** + * Get the {@link List} of sub commands. + * + * @return never {@literal null}. + */ + public List getSubCommands() { + return subCommands; + } + + /** + * @author Christoph Strobl + */ + public class BitfieldSetBuilder { + + private BitfieldCommand ref; + + BitfieldSet set = new BitfieldSet(); + + private BitfieldSetBuilder(BitfieldCommand ref) { + this.ref = ref; + } + + public BitfieldSetBuilder forType(BitfieldType type) { + this.set.type = type; + return this; + } + + /** + * Set the zero based bit {@literal offset}. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldSetBuilder valueAt(Long offset) { + return valueAt(Offset.offset(offset)); + } + + /** + * Set the bit offset. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldSetBuilder valueAt(Offset offset) { + + Assert.notNull(offset, "Offset must not be null!"); + + this.set.offset = offset; + return this; + } + + /** + * Set the value. + * + * @param value must not be {@literal null}. + * @return + */ + public BitfieldCommand to(Long value) { + + Assert.notNull(value, "Value must not be null!"); + + this.set.value = value; + return ref.set(this.set); + } + } + + /** + * @author Christoph Strobl + */ + public class BitfieldGetBuilder { + + private BitfieldCommand ref; + + BitfieldGet get = new BitfieldGet(); + + private BitfieldGetBuilder(BitfieldCommand ref) { + this.ref = ref; + } + + public BitfieldGetBuilder forType(BitfieldType type) { + this.get.type = type; + return this; + } + + /** + * Set the zero based bit {@literal offset}. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldCommand valueAt(Long offset) { + return valueAt(Offset.offset(offset)); + } + + /** + * Set the bit offset. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldCommand valueAt(Offset offset) { + + Assert.notNull(offset, "Offset must not be null!"); + + this.get.offset = offset; + return ref.get(this.get); + } + } + + /** + * @author Christoph Strobl + */ + public class BitfieldIncrByBuilder { + + private BitfieldCommand ref; + + BitfieldIncrBy incrBy = new BitfieldIncrBy(); + + private BitfieldIncrByBuilder(BitfieldCommand ref) { + this.ref = ref; + } + + public BitfieldIncrByBuilder forType(BitfieldType type) { + this.incrBy.type = type; + return this; + } + + /** + * Set the zero based bit {@literal offset}. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldIncrByBuilder valueAt(Long offset) { + return valueAt(Offset.offset(offset)); + } + + /** + * Set the bit offset. + * + * @param offset must not be {@literal null}. + * @return + */ + public BitfieldIncrByBuilder valueAt(Offset offset) { + + Assert.notNull(offset, "Offset must not be null!"); + this.incrBy.offset = offset; + return this; + } + + /** + * Set the {@link BitfieldIncrBy.Overflow} to be used for this and any subsequent {@link BitfieldIncrBy} commands. + * + * @param overflow + * @return + */ + public BitfieldIncrByBuilder overflow(BitfieldIncrBy.Overflow overflow) { + this.incrBy.overflow = overflow; + return this; + } + + /** + * Set the value used for increasing. + * + * @param value + * @return + */ + public BitfieldCommand by(Long value) { + + Assert.notNull(value, "Value must not be null!"); + + this.incrBy.value = value; + return ref.incr(this.incrBy); + } + } + } + + /** + * Sub command to be used as part of {@link BitfieldCommand}. + * + * @author Christoph Strobl + * @since 1.8 + */ + interface BitfieldSubCommand { + + /** + * The actual sub command + * + * @return never {@literal null}. + */ + String getCommand(); + + /** + * The {@link BitfieldType} to apply for the command. + * + * @return never {@literal null}. + */ + BitfieldType getType(); + + /** + * The bit offset to apply for the command. + * + * @return never {@literal null}. + */ + Offset getOffset(); + } + + /** + * Offset used inside a {@link BitfieldSubCommand}. Can be zero or type based. See + * Bits and positional offsets in the + * Redis reference. + * + * @author Christoph Strobl + * @since 1.8 + */ + class Offset { + + private final Long offset; + private final Boolean zeroBased; + + private Offset(Long offset, Boolean zeroBased) { + + Assert.notNull(offset, "Offset must not be null!"); + Assert.notNull(zeroBased, "ZeroBased must not be null!"); + + this.offset = offset; + this.zeroBased = zeroBased; + } + + /** + * Creates new zero based offset.
+ * NOTE: change to type based offset by calling {@link #multipliedByTypeLength()}. + * + * @param offset must not be {@literal null}. + * @return + */ + public static Offset offset(Long offset) { + return new Offset(offset, Boolean.TRUE); + } + + /** + * Creates new type based offset. + * + * @return + */ + public Offset multipliedByTypeLength() { + return new Offset(offset, Boolean.FALSE); + } + + /** + * @return true if offset starts at 0 and is not multiplied by the type length. + */ + public Boolean isZeroBased() { + return zeroBased; + } + + /** + * @return the actual offset value + */ + public Long getValue() { + return offset; + } + + /** + * @return the Redis Command representation + */ + public String asString() { + return (isZeroBased() ? "" : "#") + getValue(); + } + } + + /** + * The actual Redis bitfield type representation for signed and unsigned integers used with + * {@link BitfieldSubCommand}. + * + * @author Christoph Strobl + * @since 1.8 + */ + class BitfieldType { + + /** 8 bit signed Integer */ + public static final BitfieldType INT_8 = new BitfieldType(Boolean.TRUE, 8); + + /** 16 bit signed Integer */ + public static final BitfieldType INT_16 = new BitfieldType(Boolean.TRUE, 16); + + /** 32 bit signed Integer */ + public static final BitfieldType INT_32 = new BitfieldType(Boolean.TRUE, 32); + + /** 64 bit signed Integer */ + public static final BitfieldType INT_64 = new BitfieldType(Boolean.TRUE, 64); + + /** 8 bit unsigned Integer */ + public static final BitfieldType UINT_8 = new BitfieldType(Boolean.FALSE, 8); + + /** 16 bit unsigned Integer */ + public static final BitfieldType UINT_16 = new BitfieldType(Boolean.FALSE, 16); + + /** 32 bit unsigned Integer */ + public static final BitfieldType UINT_32 = new BitfieldType(Boolean.FALSE, 32); + + /** 64 bit unsigned Integer */ + public static final BitfieldType UINT_64 = new BitfieldType(Boolean.FALSE, 64); + + private final Boolean signed; + private final Integer bits; + + private BitfieldType(Boolean signed, Integer bits) { + + Assert.notNull(signed, "Signed must not be null!"); + Assert.notNull(bits, "Bits must not be null!"); + + this.signed = signed; + this.bits = bits; + } + + /** + * Create new signed {@link BitfieldType}. + * + * @param bits must not be {@literal null}. + * @return + */ + public static BitfieldType signed(Integer bits) { + return new BitfieldType(Boolean.TRUE, bits); + } + + /** + * Create new unsigned {@link BitfieldType}. + * + * @param bits must not be {@literal null}. + * @return + */ + public static BitfieldType unsigned(Integer bits) { + return new BitfieldType(Boolean.FALSE, bits); + } + + /** + * @return true if {@link BitfieldType} is signed. + */ + public boolean isSigned() { + return ObjectUtils.nullSafeEquals(signed, Boolean.TRUE); + } + + /** + * Get the actual bits of the type. + * + * @return never {@literal null}. + */ + public Integer getBits() { + return bits; + } + + /** + * Get the Redis Command representation. + * + * @return + */ + public String asString() { + return (isSigned() ? "i" : "u") + getBits(); + } + } + + /** + * @author Christoph Strobl + */ + abstract class AbstractBitfieldSubCommand implements BitfieldSubCommand { + + BitfieldType type; + Offset offset; + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection.BitfieldSubCommand#getType() + */ + @Override + public BitfieldType getType() { + return type; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection.BitfieldSubCommand#getOffset() + */ + @Override + public Offset getOffset() { + return offset; + } + } + + /** + * The {@code SET} sub command used with {@link BitfieldCommand}. + * + * @author Christoph Strobl + * @since 1.8 + */ + class BitfieldSet extends AbstractBitfieldSubCommand { + + private Long value; + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection.BitfieldSubCommand#getCommand() + */ + @Override + public String getCommand() { + return "SET"; + } + + /** + * Get the value to set. + * + * @return never {@literal null}. + */ + public Long getValue() { + return value; + } + + } + + /** + * The {@code GET} sub command used with {@link BitfieldCommand}. + * + * @author Christoph Strobl + * @since 1.8 + */ + class BitfieldGet extends AbstractBitfieldSubCommand { + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection.BitfieldSubCommand#getCommand() + */ + @Override + public String getCommand() { + return "GET"; + } + + } + + /** + * The {@code INCRBY} sub command used with {@link BitfieldCommand}. + * + * @author Christoph Strobl + * @since 1.8 + */ + class BitfieldIncrBy extends AbstractBitfieldSubCommand { + + private Long value; + private Overflow overflow; + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection.BitfieldSubCommand#getCommand() + */ + @Override + public String getCommand() { + return "INCRBY"; + } + + /** + * Get the increment value. + * + * @return never {@literal null}. + */ + public Long getValue() { + return value; + } + + /** + * Get the overflow to apply. Can be {@literal null} to use redis defaults. + * + * @return can be {@literal null}. + */ + public Overflow getOverflow() { + return overflow; + } + + /** + * @author Christoph Strobl + */ + public enum Overflow { + SAT, FAIL, WRAP + } + } } 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 348c57d3e..ff71f4ff9 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1945,4 +1945,14 @@ public interface StringRedisConnection extends RedisConnection { */ List getClientList(); + + /** + * 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 + */ + List bitfield(String key, BitfieldCommand command); } 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 1fc33b169..62e5cf695 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 @@ -469,6 +469,27 @@ class JedisClusterStringCommands implements RedisStringCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitfield(byte[], BitfieldCommand) + */ + @Override + public List bitfield(byte[] key, BitfieldCommand command) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(command, "Command must not be null!"); + + try { + + List untypedListToAvoidClassCastErrorsSinceThisOneDeclaresListOfByteArrayButReturnsListOfLong = connection.getCluster() + .bitfield(key, JedisConverters.toBitfieldCommandArguments(command)); + return (List) untypedListToAvoidClassCastErrorsSinceThisOneDeclaresListOfByteArrayButReturnsListOfLong; + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#bitOp(org.springframework.data.redis.connection.RedisStringCommands.BitOperation, byte[], byte[][]) diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index ad1bc3618..74e006dce 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -103,6 +103,8 @@ abstract public class JedisConverters extends Converters { private static final ListConverter LIST_GEO_COORDINATE_TO_POINT_CONVERTER; private static final Converter BYTES_TO_STRING_CONVERTER; private static final ListConverter BYTES_LIST_TO_STRING_LIST_CONVERTER; + private static final ListConverter BYTES_LIST_TO_LONG_LIST_CONVERTER; + private static final Converter> BITFIELD_COMMAND_ARGUMENT_CONVERTER; public static final byte[] PLUS_BYTES; public static final byte[] MINUS_BYTES; @@ -191,6 +193,50 @@ abstract public class JedisConverters extends Converters { GEO_COORDINATE_TO_POINT_CONVERTER = geoCoordinate -> geoCoordinate != null ? new Point(geoCoordinate.getLongitude(), geoCoordinate.getLatitude()) : null; LIST_GEO_COORDINATE_TO_POINT_CONVERTER = new ListConverter<>(GEO_COORDINATE_TO_POINT_CONVERTER); + + BYTES_LIST_TO_LONG_LIST_CONVERTER = new ListConverter(new Converter() { + @Override + public Long convert(byte[] source) { + return Long.valueOf(JedisConverters.toString(source)); + } + }); + + BITFIELD_COMMAND_ARGUMENT_CONVERTER = new Converter>() { + @Override + public List convert(RedisStringCommands.BitfieldCommand source) { + + if (source == null) { + return Collections.emptyList(); + } + + List args = new ArrayList(source.getSubCommands().size() * 4); + + for (RedisStringCommands.BitfieldSubCommand command : source.getSubCommands()) { + + if (command instanceof RedisStringCommands.BitfieldIncrBy) { + + RedisStringCommands.BitfieldIncrBy.Overflow overflow = ((RedisStringCommands.BitfieldIncrBy) command) + .getOverflow(); + if (overflow != null) { + args.add(JedisConverters.toBytes("OVERFLOW")); + args.add(JedisConverters.toBytes(overflow.name())); + } + } + + args.add(JedisConverters.toBytes(command.getCommand())); + args.add(JedisConverters.toBytes(command.getType().asString())); + args.add(JedisConverters.toBytes(command.getOffset().asString())); + + if (command instanceof RedisStringCommands.BitfieldSet) { + args.add(JedisConverters.toBytes(((RedisStringCommands.BitfieldSet) command).getValue())); + } else if (command instanceof RedisStringCommands.BitfieldIncrBy) { + args.add(JedisConverters.toBytes(((RedisStringCommands.BitfieldIncrBy) command).getValue())); + } + } + + return args; + } + }; } public static Converter stringToBytes() { @@ -548,6 +594,10 @@ abstract public class JedisConverters extends Converters { return BYTES_LIST_TO_STRING_LIST_CONVERTER; } + public static ListConverter getBytesListToLongListConverter() { + return BYTES_LIST_TO_LONG_LIST_CONVERTER; + } + /** * @return * @since 1.8 @@ -638,6 +688,20 @@ abstract public class JedisConverters extends Converters { return param; } + /** + * Convert given {@link org.springframework.data.redis.connection.RedisStringCommands.BitfieldCommand} into argument + * array. + * + * @param bitfieldOperation + * @return never {@literal null}. + * @since 1.8 + */ + public static byte[][] toBitfieldCommandArguments(RedisStringCommands.BitfieldCommand bitfieldOperation) { + + List tmp = BITFIELD_COMMAND_ARGUMENT_CONVERTER.convert(bitfieldOperation); + return tmp.toArray(new byte[tmp.size()][]); + } + /** * @author Christoph Strobl * @since 1.8 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 336ef9ddd..280a13eeb 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 @@ -685,6 +685,40 @@ class JedisStringCommands implements RedisStringCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitfield(byte[], BitfieldCommand) + */ + @Override + public List bitfield(byte[] key, BitfieldCommand command) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(command, "Command must not be null!"); + + byte[][] args = JedisConverters.toBitfieldCommandArguments(command); + + try { + if (isPipelined()) { + pipeline(new JedisResult(connection.getRequiredPipeline().bitfield(key, args))); + return null; + } + if (isQueueing()) { + transaction(new JedisResult(connection.getRequiredTransaction().bitfield(key, args))); + return null; + } + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + + // TODO: fix type declaration once https://github.com/xetorthio/jedis/issues/1413 is resolved + // Jedis return type declaration is wrong. it indicates List but return List causing Class cast errors + // when trying to convert that stuff. So we hacked it here to make it work. + + List untypedListToAvoidClassCastErrorsSinceThisOneDeclaresListOfByteArrayButReturnsListOfLong = connection.getJedis().bitfield(key, + args); + return (List) untypedListToAvoidClassCastErrorsSinceThisOneDeclaresListOfByteArrayButReturnsListOfLong; + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#bitOp(org.springframework.data.redis.connection.RedisStringCommands.BitOperation, byte[], byte[][]) 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 46c571187..af9267790 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,8 @@ */ package org.springframework.data.redis.connection.lettuce; +import io.lettuce.core.BitFieldArgs; +import io.lettuce.core.BitFieldArgs.Offset; import io.lettuce.core.RedisFuture; import io.lettuce.core.cluster.api.async.RedisClusterAsyncCommands; import io.lettuce.core.cluster.api.sync.RedisClusterCommands; @@ -603,6 +605,78 @@ class LettuceStringCommands implements RedisStringCommands { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#bitfield(byte[], BitfieldCommand) + */ + @Override + public List bitfield(byte[] key, BitfieldCommand command) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(command, "Command must not be null!"); + + BitFieldArgs args = new BitFieldArgs(); + + for (BitfieldSubCommand subCommand : command.getSubCommands()) { + + BitFieldArgs.BitFieldType bft = subCommand.getType().isSigned() ? BitFieldArgs.signed(subCommand.getType().getBits()) + : BitFieldArgs.unsigned(subCommand.getType().getBits()); + + BitFieldArgs.Offset offset; + if (!subCommand.getOffset().isZeroBased()) { + offset = BitFieldArgs.offset(subCommand.getOffset().getValue().intValue()); + }else{ + offset = BitFieldArgs.typeWidthBasedOffset(subCommand.getOffset().getValue().intValue()); + } + + if (subCommand instanceof BitfieldGet) { + args = args.get(bft, offset); + } else if (subCommand instanceof BitfieldSet) { + args = args.set(bft, offset, ((BitfieldSet) subCommand).getValue()); + } else if (subCommand instanceof BitfieldIncrBy) { + + BitfieldIncrBy.Overflow overflow = ((BitfieldIncrBy) subCommand).getOverflow(); + if (overflow != null) { + + BitFieldArgs.OverflowType type; + + switch (overflow) { + case SAT: + type = BitFieldArgs.OverflowType.SAT; + break; + case FAIL: + type = BitFieldArgs.OverflowType.FAIL; + break; + case WRAP: + type = BitFieldArgs.OverflowType.WRAP; + break; + default: + throw new IllegalArgumentException( + String.format("o_O invalid OVERFLOW. Expected one the following %s but got %s.", + BitfieldIncrBy.Overflow.values(), overflow)); + } + args = args.overflow(type); + } + + args = args.incrBy(bft, subCommand.getOffset().getValue().intValue(), ((BitfieldIncrBy) subCommand).getValue()); + } + } + + try { + if (isPipelined()) { + pipeline(connection.newLettuceResult(getAsyncConnection().bitfield(key, args))); + return null; + } + if (isQueueing()) { + transaction(connection.newLettuceResult(getAsyncConnection().bitfield(key, args))); + return null; + } + return getConnection().bitfield(key, args); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#bitOp(org.springframework.data.redis.connection.RedisStringCommands.BitOperation, byte[], byte[][]) diff --git a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java index 4f904ab4c..84028d46c 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultValueOperations.java @@ -24,6 +24,7 @@ import java.util.concurrent.TimeUnit; import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.core.types.Expiration; import org.springframework.lang.Nullable; @@ -386,4 +387,20 @@ class DefaultValueOperations extends AbstractOperations implements V byte[] rawKey = rawKey(key); return execute(connection -> connection.getBit(rawKey, offset), true); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ValueOperations#bitfield(Object, RedisStringCommands.BitfieldCommand) + */ + @Override + public List bitfield(K key, final RedisStringCommands.BitfieldCommand command) { + + final byte[] rawKey = rawKey(key); + return execute(new RedisCallback>() { + + public List doInRedis(RedisConnection connection) { + return connection.bitfield(rawKey, command); + } + }, true); + } } diff --git a/src/main/java/org/springframework/data/redis/core/ValueOperations.java b/src/main/java/org/springframework/data/redis/core/ValueOperations.java index 7c3ffdef9..c27cb7520 100644 --- a/src/main/java/org/springframework/data/redis/core/ValueOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ValueOperations.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.connection.RedisStringCommands; + import org.springframework.lang.Nullable; import org.springframework.util.Assert; @@ -347,6 +349,16 @@ public interface ValueOperations { @Nullable Boolean getBit(K key, long offset); - RedisOperations getOperations(); + /** + * 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 + */ + List bitfield(K key, RedisStringCommands.BitfieldCommand command); + RedisOperations getOperations(); } 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 f5e0ee5a6..3acbb7021 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.collection.IsCollectionWithSize.*; import static org.hamcrest.collection.IsEmptyCollection.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.*; import static org.hamcrest.core.Is.is; import static org.hamcrest.number.IsCloseTo.*; import static org.junit.Assert.*; @@ -25,6 +26,10 @@ import static org.junit.Assume.*; import static org.springframework.data.redis.SpinBarrier.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldIncrBy.Overflow.FAIL; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldCommand.newBitfieldCommand; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldType.*; +import static org.springframework.data.redis.connection.RedisStringCommands.Offset.*; import static org.springframework.data.redis.core.ScanOptions.*; import java.time.Duration; @@ -2902,6 +2907,116 @@ public abstract class AbstractConnectionIntegrationTests { verifyResults(Arrays.asList(new Object[] { null })); } + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitFieldSetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add(connection.bitfield(key, newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(10L))); + actual.add(connection.bitfield(key, newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(20L))); + + List results = getResults(); + assertThat((List) results.get(0), contains(0L)); + assertThat((List) results.get(1), contains(10L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitFieldGetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add(connection.bitfield(key, newBitfieldCommand().get(INT_8).valueAt(offset(0L)))); + + List results = getResults(); + assertThat((List) results.get(0), contains(0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitFieldIncrByShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add(connection.bitfield(key, newBitfieldCommand().incr(INT_8).valueAt(offset(100L)).by(1L))); + + List results = getResults(); + assertThat((List) results.get(0), contains(1L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add( + connection.bitfield(key, newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))); + actual.add( + connection.bitfield(key, newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))); + actual.add( + connection.bitfield(key, newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))); + actual.add( + connection.bitfield(key, newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L))); + + List results = getResults(); + assertThat((List) results.get(0), contains(1L)); + assertThat((List) results.get(1), contains(2L)); + assertThat((List) results.get(2), contains(3L)); + assertThat(((List) results.get(3)).get(0), is(nullValue())); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitfieldShouldAllowMultipleSubcommands() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add(connection.bitfield(key, + newBitfieldCommand().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L))); + + assertThat((List) getResults().get(0), contains(1L, 0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + + String key = "bitfield-" + UUID.randomUUID(); + + actual.add(connection.bitfield(key, newBitfieldCommand().set(INT_8).valueAt(offset(0L).multipliedByTypeLength()).to(100L).set(INT_8).valueAt(offset(1L).multipliedByTypeLength()).to(200L))); + actual.add(connection.bitfield(key, newBitfieldCommand().get(INT_8).valueAt(offset(0L).multipliedByTypeLength()).get(INT_8).valueAt(offset(1L).multipliedByTypeLength()))); + + List results = getResults(); + assertThat((List) results.get(0), contains(0L, 0L)); + assertThat((List) results.get(1), contains(100L, -56L)); + } + protected void verifyResults(List expected) { assertEquals(expected, getResults()); } diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index c2a9c2ff6..9906ea007 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -981,5 +981,10 @@ public class RedisConnectionUnitTests { public Boolean set(byte[] key, byte[] value, Expiration expiration, SetOption options) { return delegate.set(key, value, expiration, options); } + + @Override + public List bitfield(byte[] key, BitfieldCommand operation) { + return delegate.bitfield(key, operation); + } } } 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 9052d393c..8b3f897a8 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 @@ -20,6 +20,10 @@ import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldCommand.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldIncrBy.Overflow.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldType.*; +import static org.springframework.data.redis.connection.RedisStringCommands.Offset.*; import static org.springframework.data.redis.core.ScanOptions.*; import redis.clients.jedis.HostAndPort; @@ -30,6 +34,7 @@ import java.io.IOException; import java.nio.charset.Charset; import java.time.Duration; import java.util.*; +import java.util.UUID; import java.util.concurrent.TimeUnit; import org.junit.After; @@ -2333,4 +2338,97 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { public void refcountReturnsNullWhenKeyDoesNotExist() { assertThat(clusterConnection.keyCommands().refcount(KEY_3_BYTES), is(nullValue())); } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldSetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(10L)), contains(0L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(20L)), contains(10L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldGetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().get(INT_8).valueAt(offset(0L))), + contains(0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldIncrByShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(INT_8).valueAt(offset(100L)).by(1L)), contains(1L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(1L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(2L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(3L)); + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)).get(0), + is(nullValue())); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitfieldShouldAllowMultipleSubcommands() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L)), + contains(1L, 0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().set(INT_8).valueAt(offset(0L).multipliedByTypeLength()) + .to(100L).set(INT_8).valueAt(offset(1L).multipliedByTypeLength()).to(200L)), contains(0L, 0L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().get(INT_8).valueAt(offset(0L).multipliedByTypeLength()) + .get(INT_8).valueAt(offset(1L).multipliedByTypeLength())), contains(100L, -56L)); + } } 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 4d03b6947..6b0a98cf4 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 @@ -20,6 +20,10 @@ import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldCommand.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldIncrBy.Overflow.*; +import static org.springframework.data.redis.connection.RedisStringCommands.BitfieldType.*; +import static org.springframework.data.redis.connection.RedisStringCommands.Offset.*; import static org.springframework.data.redis.core.ScanOptions.*; import io.lettuce.core.RedisURI.Builder; @@ -31,11 +35,13 @@ import io.lettuce.core.codec.ByteArrayCodec; import java.nio.charset.Charset; import java.time.Duration; import java.util.*; +import java.util.UUID; import java.util.concurrent.TimeUnit; import org.junit.After; import org.junit.Before; import org.junit.ClassRule; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.springframework.dao.DataAccessException; @@ -2358,4 +2364,99 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { public void refcountReturnsNullWhenKeyDoesNotExist() { assertThat(clusterConnection.keyCommands().refcount(KEY_3_BYTES), is(nullValue())); } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldSetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(10L)), contains(0L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().set(INT_8).valueAt(offset(0L)).to(20L)), contains(10L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldGetShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().get(INT_8).valueAt(offset(0L))), + contains(0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldIncrByShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(INT_8).valueAt(offset(100L)).by(1L)), contains(1L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(1L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(2L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)), contains(3L)); + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(unsigned(2)).valueAt(offset(102L)).overflow(FAIL).by(1L)).get(0), + is(nullValue())); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitfieldShouldAllowMultipleSubcommands() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat( + clusterConnection.bitfield(JedisConverters.toBytes(key), + newBitfieldCommand().incr(signed(5)).valueAt(offset(100L)).by(1L).get(unsigned(4)).valueAt(0L)), + contains(1L, 0L)); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore + @IfProfileValue(name = "redisVersion", value = "3.2+") + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + + String key = "bitfield-" + UUID.randomUUID(); + + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().set(INT_8).valueAt(offset(0L).multipliedByTypeLength()) + .to(100L).set(INT_8).valueAt(offset(1L).multipliedByTypeLength()).to(200L)), contains(0L, 0L)); + assertThat(clusterConnection.bitfield(JedisConverters.toBytes(key), newBitfieldCommand().get(INT_8).valueAt(offset(0L).multipliedByTypeLength()) + .get(INT_8).valueAt(offset(1L).multipliedByTypeLength())), contains(100L, -56L)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java index b0d21b8d6..4cd34cfa8 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionIntegrationTests.java @@ -31,6 +31,7 @@ import java.util.concurrent.atomic.AtomicBoolean; import org.hamcrest.core.AllOf; import org.hamcrest.core.IsCollectionContaining; import org.hamcrest.core.IsInstanceOf; +import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -337,4 +338,24 @@ public class LettuceConnectionIntegrationTests extends AbstractConnectionIntegra new RedisSentinelConfiguration().master("mymaster").sentinel("127.0.0.1", 26379).sentinel("127.0.0.1", 26380)); assertThat(connection.getSentinelConnection(), notNullValue()); } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + super.bitFieldIncrByWithOverflowShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + super.bitfieldShouldWorkUsingNonZeroBasedOffset(); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java index 4318aef0a..d2417cb87 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionPipelineIntegrationTests.java @@ -22,6 +22,7 @@ import static org.springframework.data.redis.SpinBarrier.*; import java.util.Arrays; import java.util.concurrent.atomic.AtomicBoolean; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.dao.DataAccessException; @@ -110,4 +111,23 @@ public class LettuceConnectionPipelineIntegrationTests extends AbstractConnectio super.testListClientsContainsAtLeastOneElement(); } + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + super.bitFieldIncrByWithOverflowShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + super.bitfieldShouldWorkUsingNonZeroBasedOffset(); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java index 747441df5..1835b5cb4 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionTransactionIntegrationTests.java @@ -19,6 +19,7 @@ import static org.junit.Assert.*; import java.util.Arrays; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.data.redis.connection.AbstractConnectionTransactionIntegrationTests; @@ -68,4 +69,64 @@ public class LettuceConnectionTransactionIntegrationTests extends AbstractConnec public void testSelect() { super.testSelect(); } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldIncrByWithOverflowShouldWorkCorrectly() { + super.bitFieldIncrByWithOverflowShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldGetShouldWorkCorrectly() { + super.bitFieldGetShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldIncrByShouldWorkCorrectly() { + super.bitFieldIncrByShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitFieldSetShouldWorkCorrectly() { + super.bitFieldSetShouldWorkCorrectly(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitfieldShouldAllowMultipleSubcommands() { + super.bitfieldShouldAllowMultipleSubcommands(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + @Ignore("Lettuce Bug") + @Override + public void bitfieldShouldWorkUsingNonZeroBasedOffset() { + super.bitfieldShouldWorkUsingNonZeroBasedOffset(); + } } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java new file mode 100644 index 000000000..ac4c85c3e --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultValueOperationsUnitTests.java @@ -0,0 +1,73 @@ +/* + * Copyright 2016. 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.core; + +import static org.mockito.Mockito.*; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.runners.MockitoJUnitRunner; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisStringCommands; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * @author Christoph Strobl + */ +@RunWith(MockitoJUnitRunner.class) +public class DefaultValueOperationsUnitTests { + + @Mock RedisConnectionFactory connectionFactoryMock; + @Mock RedisConnection connectionMock; + RedisSerializer serializer; + RedisTemplate template; + ValueOperations valueOps; + + @Before + public void setUp() { + + when(connectionFactoryMock.getConnection()).thenReturn(connectionMock); + + serializer = new StringRedisSerializer(); + + template = new RedisTemplate(); + template.setKeySerializer(serializer); + template.setConnectionFactory(connectionFactoryMock); + template.afterPropertiesSet(); + + this.valueOps = template.opsForValue(); + } + + /** + * @see DATAREDIS-562 + */ + @Test + public void bitfieldShouldBeDelegatedCorrectly() { + + RedisStringCommands.BitfieldCommand command = RedisStringCommands.BitfieldCommand.newBitfieldCommand() + .get(RedisStringCommands.BitfieldType.INT_8).valueAt(0L); + + valueOps.bitfield("key", command); + + verify(connectionMock, times(1)).bitfield(eq(serializer.serialize("key")), eq(command)); + } + +}