From c939c8a9ffd5237ff452f11db6926d8b1daf8b04 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 2 Jun 2014 21:35:05 +0200 Subject: [PATCH] DATAREDIS-306 - Add support for ZSCAN. ZSCAN is directly supported by jedis and can be emulated for lettuce via eval. SRP and JRedis will throw UnsupportedOperationException. Original pull request: #80. --- .../DefaultStringRedisConnection.java | 50 ++++++++------ .../redis/connection/RedisZSetCommands.java | 13 ++++ .../connection/StringRedisConnection.java | 12 +++- .../connection/jedis/JedisConnection.java | 38 +++++++++++ .../connection/jedis/JedisConverters.java | 25 +++++-- .../connection/jredis/JredisConnection.java | 9 +++ .../connection/lettuce/LettuceConnection.java | 67 +++++++++++++++---- .../connection/lettuce/LettuceConverters.java | 24 +++++++ .../redis/connection/srp/SrpConnection.java | 5 ++ .../data/redis/core/AbstractOperations.java | 21 +++--- .../data/redis/core/BoundZSetOperations.java | 8 +++ .../core/DefaultBoundZSetOperations.java | 10 +++ .../redis/core/DefaultZSetOperations.java | 28 ++++++++ .../data/redis/core/ZSetOperations.java | 9 +++ .../AbstractConnectionIntegrationTests.java | 36 ++++++++++ .../core/DefaultZSetOperationsTests.java | 42 +++++++++++- 16 files changed, 346 insertions(+), 51 deletions(-) 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 a1095e88e..5c29f4773 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -2228,6 +2228,14 @@ public class DefaultStringRedisConnection implements StringRedisConnection { return this.delegate.scan(options); } + /* + * + */ + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + return this.delegate.zScan(key, options); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisSetCommands#scan(byte[], org.springframework.data.redis.core.ScanOptions) @@ -2320,24 +2328,6 @@ public class DefaultStringRedisConnection implements StringRedisConnection { return this.delegate.getClientName(); } - /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.StringRedisConnection#sScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) - */ - @Override - public Cursor sScan(String key, ScanOptions options) { - - return new ConvertingCursor(this.delegate.sScan(this.serialize(key), options), - new Converter() { - - @Override - public String convert(byte[] source) { - return serializer.deserialize(source); - } - }); - - } - /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#hScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) @@ -2354,12 +2344,12 @@ public class DefaultStringRedisConnection implements StringRedisConnection { @Override public String getKey() { - return DefaultStringRedisConnection.this.serializer.deserialize(source.getKey()); + return bytesToString.convert(source.getKey()); } @Override public String getValue() { - return DefaultStringRedisConnection.this.serializer.deserialize(source.getValue()); + return bytesToString.convert(source.getValue()); } @Override @@ -2370,4 +2360,24 @@ public class DefaultStringRedisConnection implements StringRedisConnection { } }); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#sScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor sScan(String key, ScanOptions options) { + return new ConvertingCursor(this.delegate.sScan(this.serialize(key), options), bytesToString); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#zScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor zScan(String key, ScanOptions options) { + return new ConvertingCursor(delegate.zScan(this.serialize(key), options), + new TupleConverter()); + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java index 8b143a317..90f22c72c 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisZSetCommands.java @@ -17,6 +17,9 @@ package org.springframework.data.redis.connection; import java.util.Set; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; + /** * ZSet(SortedSet)-specific commands supported by Redis. * @@ -332,4 +335,14 @@ public interface RedisZSetCommands { * @return */ Long zInterStore(byte[] destKey, Aggregate aggregate, int[] weights, byte[]... sets); + + /** + * Use a {@link Cursor} to iterate over elements in sorted set at {@code key}. + * + * @param key + * @param options + * @return + * @since 1.4 + */ + Cursor zScan(byte[] key, ScanOptions options); } 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 e58c58877..20e7644a5 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -319,6 +319,15 @@ public interface StringRedisConnection extends RedisConnection { */ List getClientList(); + /** + * @see RedisHashCommands#hScan(byte[], ScanOptions) + * @param key + * @param options + * @return + * @since 1.4 + */ + Cursor> hScan(String key, ScanOptions options); + /** * @see RedisSetCommands#sScan(byte[], ScanOptions) * @param key @@ -329,10 +338,11 @@ public interface StringRedisConnection extends RedisConnection { Cursor sScan(String key, ScanOptions options); /** + * @see RedisZSetCommands#zScan(byte[], ScanOptions) * @param key * @param options * @return * @since 1.4 */ - Cursor> hScan(String key, ScanOptions options); + Cursor zScan(String key, ScanOptions options); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java index a1340e8e7..2727d7ac3 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnection.java @@ -38,6 +38,7 @@ import org.springframework.data.redis.connection.MessageListener; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisPipelineException; import org.springframework.data.redis.connection.RedisSubscribedConnectionException; +import org.springframework.data.redis.connection.RedisZSetCommands; import org.springframework.data.redis.connection.ReturnType; import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; @@ -2949,6 +2950,43 @@ public class JedisConnection implements RedisConnection { } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zScan(byte[], org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + return zScan(key, 0L, options); + } + + /** + * @param key + * @param cursorId + * @param options + * @return + * @since 1.4 + */ + public Cursor zScan(byte[] key, Long cursorId, ScanOptions options) { + + return new KeyBoundCursor(key, cursorId, options) { + + @Override + protected ScanIteration doScan(byte[] key, long cursorId, ScanOptions options) { + + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'ZSCAN' cannot be called in pipeline / transaction mode."); + } + + ScanParams params = prepareScanParams(options); + + ScanResult result = jedis.zscan(key, JedisConverters.toBytes(cursorId), params); + return new ScanIteration(Long.valueOf(result.getStringCursor()), JedisConverters + .tuplesToTuples().convert(result.getResult())); + } + + }.open(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisSetCommands#sScan(byte[], org.springframework.data.redis.core.ScanOptions) 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 90116bb19..749f33688 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 @@ -59,6 +59,8 @@ abstract public class JedisConverters extends Converters { private static final SetConverter TUPLE_SET_TO_TUPLE_SET; private static final Converter EXCEPTION_CONVERTER = new JedisExceptionConverter(); private static final Converter> STRING_TO_CLIENT_INFO_CONVERTER = new StringToRedisClientInfoConverter(); + private static final Converter TUPLE_CONVERTER; + private static final ListConverter TUPLE_LIST_TO_TUPLE_LIST_CONVERTER; static { STRING_TO_BYTES = new Converter() { @@ -69,19 +71,30 @@ abstract public class JedisConverters extends Converters { STRING_LIST_TO_BYTE_LIST = new ListConverter(STRING_TO_BYTES); STRING_SET_TO_BYTE_SET = new SetConverter(STRING_TO_BYTES); STRING_MAP_TO_BYTE_MAP = new MapConverter(STRING_TO_BYTES); - TUPLE_SET_TO_TUPLE_SET = new SetConverter( - new Converter() { - public Tuple convert(redis.clients.jedis.Tuple source) { - return source != null ? new DefaultTuple(source.getBinaryElement(), source.getScore()) : null; - } + TUPLE_CONVERTER = new Converter() { + public Tuple convert(redis.clients.jedis.Tuple source) { + return source != null ? new DefaultTuple(source.getBinaryElement(), source.getScore()) : null; + } - }); + }; + TUPLE_SET_TO_TUPLE_SET = new SetConverter(TUPLE_CONVERTER); + TUPLE_LIST_TO_TUPLE_LIST_CONVERTER = new ListConverter(TUPLE_CONVERTER); } public static Converter stringToBytes() { return STRING_TO_BYTES; } + /** + * {@link ListConverter} converting jedis {@link redis.clients.jedis.Tuple} to {@link Tuple}. + * + * @return + * @since 1.4 + */ + public static ListConverter tuplesToTuples() { + return TUPLE_LIST_TO_TUPLE_LIST_CONVERTER; + } + public static ListConverter stringListToByteList() { return STRING_LIST_TO_BYTE_LIST; } diff --git a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java index d6e85c4b8..25926ac63 100644 --- a/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jredis/JredisConnection.java @@ -1249,6 +1249,15 @@ public class JredisConnection implements RedisConnection { throw new UnsupportedOperationException("'SCAN' command is not supported for jredis."); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zScan(byte[], org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + throw new UnsupportedOperationException("'ZSCAN' command is not supported for jredis."); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisSetCommands#sScan(byte[], org.springframework.data.redis.core.ScanOptions) diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java index 9cab349de..d4cd9cd5a 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnection.java @@ -3069,12 +3069,52 @@ public class LettuceConnection implements RedisConnection { List result = eval(script.getBytes(), ReturnType.MULTI, 0); String nextCursorId = LettuceConverters.bytesToString().convert((byte[]) result.get(0)); - return new ScanIteration(Long.valueOf(nextCursorId), ((ArrayList) result.get(1))); + return new ScanIteration(Long.valueOf(nextCursorId), ((List) result.get(1))); } }.open(); } + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hScan(byte[], org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor> hScan(byte[] key, ScanOptions options) { + return hscan(key, 0, options); + } + + /** + * @param key + * @param cursorId + * @param options + * @return + * @since 1.4 + */ + public Cursor> hscan(byte[] key, long cursorId, ScanOptions options) { + + return new KeyBoundCursor>(key, cursorId, options) { + + @Override + protected ScanIteration> doScan(byte[] key, long cursorId, ScanOptions options) { + + if (isQueueing() || isPipelined()) { + throw new UnsupportedOperationException("'HSCAN' cannot be called in pipeline / transaction mode."); + } + + String params = " ,'" + LettuceConverters.bytesToString().convert(key) + "', " + cursorId + + options.toOptionString(); + String script = "return redis.call('HSCAN'" + params + ")"; + + List result = eval(script.getBytes(), ReturnType.MULTI, 0); + String nextCursorId = LettuceConverters.bytesToString().convert((byte[]) result.get(0)); + + @SuppressWarnings("unchecked") + Map values = LettuceConverters.toMap((List) result.get(1)); + return new ScanIteration>(Long.valueOf(nextCursorId), values.entrySet()); + } + }.open(); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisSetCommands#sScan(byte[], org.springframework.data.redis.core.ScanOptions) @@ -3115,12 +3155,13 @@ public class LettuceConnection implements RedisConnection { }.open(); } - /* (non-Javadoc) - * @see org.springframework.data.redis.connection.RedisHashCommands#hScan(byte[], org.springframework.data.redis.core.ScanOptions) + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisZSetCommands#zScan(byte[], org.springframework.data.redis.core.ScanOptions) */ @Override - public Cursor> hScan(byte[] key, ScanOptions options) { - return hscan(key, 0, options); + public Cursor zScan(byte[] key, ScanOptions options) { + return zScan(key, 0L, options); } /** @@ -3130,27 +3171,27 @@ public class LettuceConnection implements RedisConnection { * @return * @since 1.4 */ - public Cursor> hscan(byte[] key, long cursorId, ScanOptions options) { + public Cursor zScan(byte[] key, long cursorId, ScanOptions options) { - return new KeyBoundCursor>(key, cursorId, options) { + return new KeyBoundCursor(key, cursorId, options) { + @SuppressWarnings("unchecked") @Override - protected ScanIteration> doScan(byte[] key, long cursorId, ScanOptions options) { + protected ScanIteration doScan(byte[] key, long cursorId, ScanOptions options) { if (isQueueing() || isPipelined()) { - throw new UnsupportedOperationException("'HSCAN' cannot be called in pipeline / transaction mode."); + throw new UnsupportedOperationException("'ZSCAN' cannot be called in pipeline / transaction mode."); } String params = " ,'" + LettuceConverters.bytesToString().convert(key) + "', " + cursorId + options.toOptionString(); - String script = "return redis.call('HSCAN'" + params + ")"; + String script = "return redis.call('ZSCAN'" + params + ")"; List result = eval(script.getBytes(), ReturnType.MULTI, 0); String nextCursorId = LettuceConverters.bytesToString().convert((byte[]) result.get(0)); - @SuppressWarnings("unchecked") - Map values = LettuceConverters.toMap((List) result.get(1)); - return new ScanIteration>(Long.valueOf(nextCursorId), values.entrySet()); + return new ScanIteration(Long.valueOf(nextCursorId), LettuceConverters.toTuple((List) result + .get(1))); } }.open(); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index cd2103fda..6882d60e9 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -67,6 +67,7 @@ abstract public class LettuceConverters extends Converters { private static final Converter EXCEPTION_CONVERTER = new LettuceExceptionConverter(); private static final Converter LONG_TO_BOOLEAN = new LongToBooleanConverter(); private static final Converter, Map> BYTES_LIST_TO_MAP; + private static final Converter, List> BYTES_LIST_TO_TUPLE_LIST_CONVERTER; private static final Converter> STRING_TO_LIST_OF_CLIENT_INFO = new StringToRedisClientInfoConverter(); @@ -148,7 +149,29 @@ abstract public class LettuceConverters extends Converters { } }; + BYTES_LIST_TO_TUPLE_LIST_CONVERTER = new Converter, List>() { + @Override + public List convert(List source) { + + if (CollectionUtils.isEmpty(source)) { + return Collections.emptyList(); + } + + List tuples = new ArrayList(); + Iterator it = source.iterator(); + while (it.hasNext()) { + tuples.add(new DefaultTuple(it.next(), it.hasNext() ? Double.valueOf(LettuceConverters.toString(it.next())) + : null)); + } + return tuples; + } + }; + + } + + public static List toTuple(List list) { + return BYTES_LIST_TO_TUPLE_LIST_CONVERTER.convert(list); } public static Converter> stringToRedisClientListConverter() { @@ -301,4 +324,5 @@ abstract public class LettuceConverters extends Converters { public static List toListOfRedisClientInformation(String clientList) { return stringToRedisClientListConverter().convert(clientList); } + } diff --git a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java index 89423f4b1..df6b05504 100644 --- a/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/srp/SrpConnection.java @@ -2343,6 +2343,11 @@ public class SrpConnection implements RedisConnection { throw new UnsupportedOperationException("'SCAN' command is not supported for Srp."); } + @Override + public Cursor zScan(byte[] key, ScanOptions options) { + throw new UnsupportedOperationException("'ZSCAN' command is not supported for Srp."); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisSetCommands#sScan(byte[], org.springframework.data.redis.core.ScanOptions) diff --git a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java index df4f93e3c..0aa0ebda3 100644 --- a/src/main/java/org/springframework/data/redis/core/AbstractOperations.java +++ b/src/main/java/org/springframework/data/redis/core/AbstractOperations.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2013 the original author or authors. + * Copyright 2011-2014 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. @@ -35,6 +35,7 @@ import org.springframework.util.Assert; * * @author Costin Leau * @author Jennifer Hickey + * @author Christoph Strobl */ abstract class AbstractOperations { @@ -181,22 +182,26 @@ abstract class AbstractOperations { return SerializationUtils.deserialize(rawValues, valueSerializer()); } - @SuppressWarnings({ "unchecked", "rawtypes" }) - Set> deserializeTupleValues(Set rawValues) { + Set> deserializeTupleValues(Collection rawValues) { if (rawValues == null) { return null; } Set> set = new LinkedHashSet>(rawValues.size()); for (Tuple rawValue : rawValues) { - Object value = rawValue.getValue(); - if (valueSerializer() != null) { - value = valueSerializer().deserialize(rawValue.getValue()); - } - set.add(new DefaultTypedTuple(value, rawValue.getScore())); + set.add(deserializeTuple(rawValue)); } return set; } + @SuppressWarnings({ "unchecked", "rawtypes" }) + TypedTuple deserializeTuple(Tuple tuple) { + Object value = tuple.getValue(); + if (valueSerializer() != null) { + value = valueSerializer().deserialize(tuple.getValue()); + } + return new DefaultTypedTuple(value, tuple.getScore()); + } + @SuppressWarnings("unchecked") Set rawTupleValues(Set> values) { if (values == null) { diff --git a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java index 521177e1d..5cc36ebf5 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundZSetOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.Set; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; @@ -90,4 +91,11 @@ public interface BoundZSetOperations extends BoundKeyOperations { Long zCard(); Double score(Object o); + + /** + * @param options + * @return + * @since 1.4 + */ + Iterator> scan(ScanOptions options); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java index bb7aa4d56..4e286a7be 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundZSetOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.Set; import org.springframework.data.redis.connection.DataType; @@ -156,4 +157,13 @@ class DefaultBoundZSetOperations extends DefaultBoundKeyOperations impl public DataType getType() { return DataType.ZSET; } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundZSetOperations#scan(org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Iterator> scan(ScanOptions options) { + return ops.scan(getKey(), options); + } } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java index 3d4b25cd4..794965201 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultZSetOperations.java @@ -17,8 +17,11 @@ package org.springframework.data.redis.core; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.Set; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; @@ -365,4 +368,29 @@ class DefaultZSetOperations extends AbstractOperations implements ZS } }, true); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.ZSetOperations#scan(java.lang.Object, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Iterator> scan(K key, final ScanOptions options) { + + final byte[] rawKey = rawKey(key); + Cursor cursor = execute(new RedisCallback>() { + + @Override + public Cursor doInRedis(RedisConnection connection) throws DataAccessException { + return connection.zScan(rawKey, options); + } + }, true); + + return new ConvertingCursor>(cursor, new Converter>() { + + @Override + public TypedTuple convert(Tuple source) { + return deserializeTuple(source); + } + }); + } } diff --git a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java index 66d9d7f18..85e3ffd80 100644 --- a/src/main/java/org/springframework/data/redis/core/ZSetOperations.java +++ b/src/main/java/org/springframework/data/redis/core/ZSetOperations.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.Set; /** @@ -107,4 +108,12 @@ public interface ZSetOperations { Long zCard(K key); RedisOperations getOperations(); + + /** + * @param key + * @param options + * @return + * @since 1.4 + */ + Iterator> scan(K key, ScanOptions options); } 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 a74dfd3c4..b54beca9c 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -56,6 +56,7 @@ import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.SortParameters.Order; import org.springframework.data.redis.connection.StringRedisConnection.StringTuple; import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; @@ -1950,6 +1951,41 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(i, is(itemCount)); } + /** + * @see DATAREDIS-306 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "2.8+") + public void zScanShouldReadEntireValueRange() { + + if (!ConnectionUtils.isJedis(connectionFactory) && !ConnectionUtils.isLettuce(connectionFactory)) { + throw new AssumptionViolatedException("ZSCAN is only available for jedis and lettuce"); + } + + if (connection.isPipelined() || connection.isQueueing()) { + throw new AssumptionViolatedException("ZSCAN is only available in non pipeline | queue mode."); + } + + connection.zAdd("myset", 2, "Bob"); + connection.zAdd("myset", 1, "James"); + connection.zAdd("myset", 4, "Joe"); + + Cursor tuples = connection.zScan("myset", ScanOptions.NONE); + + int count = 0; + while (tuples.hasNext()) { + + StringTuple tuple = tuples.next(); + + assertThat(tuple.getValueAsString(), anyOf(equalTo("Bob"), equalTo("James"), equalTo("Joe"))); + assertThat(tuple.getScore(), anyOf(equalTo(1D), equalTo(2D), equalTo(4D))); + + count++; + } + + assertThat(count, equalTo(3)); + } + /** * @see DATAREDIS-304 */ diff --git a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsTests.java b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsTests.java index ad2c47509..df8903e0e 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultZSetOperationsTests.java @@ -15,16 +15,18 @@ */ package org.springframework.data.redis.core; +import static org.hamcrest.core.AnyOf.*; +import static org.hamcrest.core.IsEqual.*; import static org.junit.Assert.*; import static org.springframework.data.redis.matcher.RedisTestMatchers.*; import java.util.Collection; import java.util.Collections; import java.util.HashSet; +import java.util.Iterator; import java.util.LinkedHashSet; import java.util.Set; -import org.hamcrest.core.IsEqual; import org.junit.After; import org.junit.Before; import org.junit.Test; @@ -34,6 +36,7 @@ import org.junit.runners.Parameterized.Parameters; import org.springframework.data.redis.ObjectFactory; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.test.annotation.IfProfileValue; /** * Integration test of {@link DefaultZSetOperations} @@ -207,7 +210,7 @@ public class DefaultZSetOperationsTests { values.add(new DefaultTypedTuple(value3, 0.8)); zSetOps.add(key, values); - assertThat(zSetOps.zCard(key), IsEqual.equalTo(3L)); + assertThat(zSetOps.zCard(key), equalTo(3L)); } @Test @@ -224,6 +227,39 @@ public class DefaultZSetOperationsTests { values.add(new DefaultTypedTuple(value3, 0.8)); zSetOps.add(key, values); - assertThat(zSetOps.size(key), IsEqual.equalTo(3L)); + assertThat(zSetOps.size(key), equalTo(3L)); + } + + /** + * @see DATAREDIS-306 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "2.8+") + public void testZScanShouldReadEntireValueRange() { + + K key = keyFactory.instance(); + + final TypedTuple tuple1 = new DefaultTypedTuple(valueFactory.instance(), 1.7); + final TypedTuple tuple2 = new DefaultTypedTuple(valueFactory.instance(), 3.2); + final TypedTuple tuple3 = new DefaultTypedTuple(valueFactory.instance(), 0.8); + + Set> values = new HashSet>() { + { + add(tuple1); + add(tuple2); + add(tuple3); + } + }; + + zSetOps.add(key, values); + + int count = 0; + Iterator> it = zSetOps.scan(key, ScanOptions.scanOptions().count(2).build()); + while (it.hasNext()) { + assertThat(it.next(), anyOf(equalTo(tuple1), equalTo(tuple2), equalTo(tuple3))); + count++; + } + + assertThat(count, equalTo(3)); } }