From ba8824918864de40a55d6082640d8b8f1098f217 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Mon, 2 Jun 2014 10:39:48 +0200 Subject: [PATCH] DATAREDIS-305 - Add support for HSCAN. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HSCAN is directly supported by jedis and can be emulated for lettuce via eval. SRP and JRedis will throw UnsupportedOperationException. We have modified the ScanCursor implementation to take Collection instead of List. Currently naming for scan commands is not consistent for scan, sscan and hscan. There’s a clean up task for those commands when done with all scan related commands. Original pull request: #78. --- .../DefaultStringRedisConnection.java | 42 +++++++++++++ .../redis/connection/RedisHashCommands.java | 13 ++++ .../connection/StringRedisConnection.java | 8 +++ .../connection/jedis/JedisConnection.java | 27 ++++++++ .../connection/jredis/JredisConnection.java | 10 +++ .../connection/lettuce/LettuceConnection.java | 52 +++++++++++---- .../connection/lettuce/LettuceConverters.java | 29 +++++++++ .../redis/connection/srp/SrpConnection.java | 10 +++ .../data/redis/core/BoundHashOperations.java | 11 +++- .../core/DefaultBoundHashOperations.java | 14 ++++- .../redis/core/DefaultHashOperations.java | 52 ++++++++++++++- .../data/redis/core/HashOperations.java | 9 +++ .../data/redis/core/ScanIteration.java | 8 +-- .../data/redis/core/ScanOptions.java | 25 +++++++- .../AbstractConnectionIntegrationTests.java | 38 +++++++++++ .../core/DefaultHashOperationsTests.java | 63 ++++++++++++++----- 16 files changed, 372 insertions(+), 39 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 f9a86b09c..a1095e88e 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -21,6 +21,7 @@ import java.util.LinkedHashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Queue; import java.util.Set; @@ -2236,6 +2237,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection { return this.delegate.sScan(key, options); } + /* + * (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 this.delegate.hScan(key, options); + } + /** * Specifies if pipelined and tx results should be deserialized to Strings. If false, results of * {@link #closePipeline()} and {@link #exec()} will be of the type returned by the underlying connection @@ -2328,4 +2338,36 @@ public class DefaultStringRedisConnection implements StringRedisConnection { } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Cursor> hScan(String key, ScanOptions options) { + + return new ConvertingCursor, Map.Entry>(this.delegate.hScan( + this.serialize(key), options), new Converter, Map.Entry>() { + + @Override + public Entry convert(final Entry source) { + return new Map.Entry() { + + @Override + public String getKey() { + return DefaultStringRedisConnection.this.serializer.deserialize(source.getKey()); + } + + @Override + public String getValue() { + return DefaultStringRedisConnection.this.serializer.deserialize(source.getValue()); + } + + @Override + public String setValue(String value) { + throw new UnsupportedOperationException("Cannot set value for entry in cursor"); + } + }; + } + }); + } } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java index 3a0bbfc7d..b419615e3 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -19,6 +19,9 @@ import java.util.List; import java.util.Map; import java.util.Set; +import org.springframework.data.redis.core.Cursor; +import org.springframework.data.redis.core.ScanOptions; + /** * Hash-specific commands supported by Redis. * @@ -155,4 +158,14 @@ public interface RedisHashCommands { * @return */ Map hGetAll(byte[] key); + + /** + * Use a {@link Cursor} to iterate over entries in hash at {@code key}. + * + * @param key + * @param options + * @return + * @since 1.4 + */ + Cursor> hScan(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 b357a1a84..e58c58877 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -327,4 +327,12 @@ public interface StringRedisConnection extends RedisConnection { * @since 1.4 */ Cursor sScan(String key, ScanOptions options); + + /** + * @param key + * @param options + * @return + * @since 1.4 + */ + Cursor> hScan(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 de00f447b..a1340e8e7 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 @@ -23,6 +23,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Queue; import java.util.Set; @@ -65,6 +66,7 @@ import redis.clients.jedis.Protocol.Command; import redis.clients.jedis.Queable; import redis.clients.jedis.Response; import redis.clients.jedis.ScanParams; +import redis.clients.jedis.ScanResult; import redis.clients.jedis.SortingParams; import redis.clients.jedis.Transaction; import redis.clients.jedis.ZParams; @@ -2925,6 +2927,7 @@ public class JedisConnection implements RedisConnection { * @param options * @return */ + @SuppressWarnings("resource") public Cursor scan(long cursorId, ScanOptions options) { return new ScanCursor(cursorId, options) { @@ -2981,6 +2984,30 @@ public class JedisConnection implements RedisConnection { }.open(); } + @Override + public Cursor> hScan(byte[] key, ScanOptions options) { + return hscan(key, 0, options); + } + + 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."); + } + + ScanParams params = prepareScanParams(options); + + ScanResult> result = jedis.hscan(key, JedisConverters.toBytes(cursorId), params); + return new ScanIteration>(Long.valueOf(result.getStringCursor()), result.getResult()); + } + }.open(); + } + private ScanParams prepareScanParams(ScanOptions options) { ScanParams sp = new ScanParams(); if (!options.equals(ScanOptions.NONE)) { 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 ae0c7a4a1..d6e85c4b8 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 @@ -22,6 +22,7 @@ import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Set; @@ -1256,4 +1257,13 @@ public class JredisConnection implements RedisConnection { public Cursor sScan(byte[] key, ScanOptions options) { throw new UnsupportedOperationException("'SSCAN' command is not uspported for jredis"); } + + /* + * (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) { + throw new UnsupportedOperationException("'HSCAN' command is not uspported for jredis"); + } } 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 7ebb394ab..9cab349de 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 @@ -25,6 +25,7 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Queue; import java.util.Set; @@ -59,7 +60,6 @@ import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.StringUtils; import com.lambdaworks.redis.RedisAsyncConnection; import com.lambdaworks.redis.RedisClient; @@ -3063,7 +3063,7 @@ public class LettuceConnection implements RedisConnection { throw new UnsupportedOperationException("'SCAN' cannot be called in pipeline / transaction mode."); } - String params = " ," + cursorId + prepareScanParams(options); + String params = " ," + cursorId + options.toOptionString(); String script = "return redis.call('SCAN'" + params + ")"; List result = eval(script.getBytes(), ReturnType.MULTI, 0); @@ -3104,7 +3104,7 @@ public class LettuceConnection implements RedisConnection { } String params = " ,'" + LettuceConverters.bytesToString().convert(key) + "', " + cursorId - + prepareScanParams(options); + + options.toOptionString(); String script = "return redis.call('SSCAN'" + params + ")"; List result = eval(script.getBytes(), ReturnType.MULTI, 0); @@ -3115,18 +3115,44 @@ public class LettuceConnection implements RedisConnection { }.open(); } - private String prepareScanParams(ScanOptions options) { + /* (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); + } - String params = ""; - if (!options.equals(ScanOptions.NONE)) { - if (options.getCount() != null) { - params += (", 'count', " + options.getCount()); + /** + * @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()); } - if (StringUtils.hasText(options.getPattern())) { - params += (", 'match' , '" + options.getPattern() + "'"); - } - } - return params; + }.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 720ea95e2..cd2103fda 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 @@ -19,8 +19,11 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; +import java.util.Iterator; +import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; import java.util.Set; import org.springframework.core.convert.converter.Converter; @@ -36,6 +39,7 @@ import org.springframework.data.redis.connection.convert.LongToBooleanConverter; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; +import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import com.lambdaworks.redis.KeyValue; @@ -62,6 +66,7 @@ abstract public class LettuceConverters extends Converters { private static final Converter, Tuple> SCORED_VALUE_TO_TUPLE; 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> STRING_TO_LIST_OF_CLIENT_INFO = new StringToRedisClientInfoConverter(); @@ -105,6 +110,26 @@ abstract public class LettuceConverters extends Converters { return list; } }; + BYTES_LIST_TO_MAP = new Converter, Map>() { + + @Override + public Map convert(final List source) { + + if (CollectionUtils.isEmpty(source)) { + Collections.emptyMap(); + } + + Map target = new LinkedHashMap(); + + Iterator kv = source.iterator(); + while (kv.hasNext()) { + target.put(kv.next(), kv.hasNext() ? kv.next() : null); + } + + return target; + } + + }; SCORED_VALUES_TO_TUPLE_SET = new Converter>, Set>() { public Set convert(List> source) { if (source == null) { @@ -238,6 +263,10 @@ abstract public class LettuceConverters extends Converters { return (value ? 1 : 0); } + public static Map toMap(List source) { + return BYTES_LIST_TO_MAP.convert(source); + } + public static SortArgs toSortArgs(SortParameters params) { SortArgs args = new SortArgs(); if (params == null) { 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 cf55276e5..89423f4b1 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 @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Properties; import java.util.Queue; import java.util.Set; @@ -2351,6 +2352,15 @@ public class SrpConnection implements RedisConnection { throw new UnsupportedOperationException("'SSCAN' command is not supported for Srp."); } + /* + * (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) { + throw new UnsupportedOperationException("'HSCAN' command is not supported for Srp."); + } + private List closeTransaction() { List results = Collections.emptyList(); if (txTracker != null) { diff --git a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java index 54e0a3648..219208b10 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.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. @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -24,6 +25,7 @@ import java.util.Set; * Hash operations bound to a certain key. * * @author Costin Leau + * @author Christoph Strobl */ public interface BoundHashOperations extends BoundKeyOperations { @@ -54,4 +56,11 @@ public interface BoundHashOperations extends BoundKeyOperations { void delete(Object... keys); Map entries(); + + /** + * @param key + * @since 1.4 + * @return + */ + Iterator> scan(H key, ScanOptions options); } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java index 382b46569..00ca107a0 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.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. @@ -16,8 +16,10 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; import org.springframework.data.redis.connection.DataType; @@ -26,6 +28,7 @@ import org.springframework.data.redis.connection.DataType; * Default implementation for {@link HashOperations}. * * @author Costin Leau + * @author Christoph Strobl */ class DefaultBoundHashOperations extends DefaultBoundKeyOperations implements BoundHashOperations { @@ -102,4 +105,13 @@ class DefaultBoundHashOperations extends DefaultBoundKeyOperations public DataType getType() { return DataType.HASH; } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#hscan(java.lang.Object) + */ + @Override + public Iterator> scan(H key, ScanOptions options) { + return ops.scan(key, options); + } } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java index bb54463f4..3984296e9 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.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. @@ -17,17 +17,22 @@ package org.springframework.data.redis.core; import java.util.Collection; import java.util.Collections; +import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Map.Entry; import java.util.Set; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisConnection; /** * Default implementation of {@link HashOperations}. * * @author Costin Leau + * @author Christoph Strobl */ class DefaultHashOperations extends AbstractOperations implements HashOperations { @@ -222,4 +227,49 @@ class DefaultHashOperations extends AbstractOperations imp return deserializeHashMap(entries); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#hscan(java.lang.Object, org.springframework.data.redis.core.ScanOptions) + */ + @Override + public Iterator> scan(K key, final ScanOptions options) { + + final byte[] rawKey = rawKey(key); + return execute(new RedisCallback>>() { + + @Override + public Cursor> doInRedis(RedisConnection connection) throws DataAccessException { + + return new ConvertingCursor, Map.Entry>(connection.hScan(rawKey, options), + new Converter, Map.Entry>() { + + @Override + public Entry convert(final Entry source) { + + return new Map.Entry() { + + @Override + public HK getKey() { + return deserializeHashKey(source.getKey()); + } + + @Override + public HV getValue() { + return deserializeHashValue(source.getValue()); + } + + @Override + public HV setValue(HV value) { + throw new UnsupportedOperationException("Values cannot be set when scanning through entries."); + } + }; + + } + }); + } + + }, true); + + } } diff --git a/src/main/java/org/springframework/data/redis/core/HashOperations.java b/src/main/java/org/springframework/data/redis/core/HashOperations.java index ad3bc9506..20493af95 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.core; import java.util.Collection; +import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; @@ -54,4 +55,12 @@ public interface HashOperations { Map entries(H key); RedisOperations getOperations(); + + /** + * @param key + * @since 1.4 + * @param options + * @return + */ + Iterator> scan(H key, ScanOptions options); } diff --git a/src/main/java/org/springframework/data/redis/core/ScanIteration.java b/src/main/java/org/springframework/data/redis/core/ScanIteration.java index 77b6edd5e..edbc7e6ca 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanIteration.java +++ b/src/main/java/org/springframework/data/redis/core/ScanIteration.java @@ -16,9 +16,9 @@ package org.springframework.data.redis.core; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.Iterator; -import java.util.List; /** * {@link ScanIteration} holds the values contained in Redis {@literal Multibulk reply} on exectuting {@literal SCAN} @@ -30,13 +30,13 @@ import java.util.List; public class ScanIteration implements Iterable { private final long cursorId; - private final List items; + private final Collection items; /** * @param cursorId * @param items */ - public ScanIteration(long cursorId, List items) { + public ScanIteration(long cursorId, Collection items) { this.cursorId = cursorId; this.items = (items != null ? new ArrayList(items) : Collections. emptyList()); @@ -56,7 +56,7 @@ public class ScanIteration implements Iterable { * * @return */ - public List getItems() { + public Collection getItems() { return items; } diff --git a/src/main/java/org/springframework/data/redis/core/ScanOptions.java b/src/main/java/org/springframework/data/redis/core/ScanOptions.java index 357f429cf..83bc1a8ef 100644 --- a/src/main/java/org/springframework/data/redis/core/ScanOptions.java +++ b/src/main/java/org/springframework/data/redis/core/ScanOptions.java @@ -15,6 +15,8 @@ */ package org.springframework.data.redis.core; +import org.springframework.util.StringUtils; + /** * Options to be used for with {@literal SCAN} command. * @@ -29,15 +31,14 @@ public class ScanOptions { private Long count; private String pattern; - private ScanOptions() { - } + private ScanOptions() {} /** * Static factory method that returns a new {@link ScanOptionsBuilder}. * * @return */ - public static ScanOptionsBuilder scanOptions(){ + public static ScanOptionsBuilder scanOptions() { return new ScanOptionsBuilder(); } @@ -49,6 +50,24 @@ public class ScanOptions { return pattern; } + public String toOptionString() { + + if (this.equals(ScanOptions.NONE)) { + return ""; + } + + String params = ""; + + if (this.count != null) { + params += (", 'count', " + count); + } + if (StringUtils.hasText(this.pattern)) { + params += (", 'match' , '" + this.pattern + "'"); + } + + return params; + } + /** * @author Christoph Strobl * @since 1.4 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 b139d19b6..a74dfd3c4 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1979,6 +1979,44 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(i, is(6)); } + /** + * @see DATAREDIS-305 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "2.8+") + public void hScanShouldReadEntireValueRange() { + + if (!ConnectionUtils.isJedis(connectionFactory) && !ConnectionUtils.isLettuce(connectionFactory)) { + throw new AssumptionViolatedException("HSCAN is only available for jedis and lettuce"); + } + + if (connection.isPipelined() || connection.isQueueing()) { + throw new AssumptionViolatedException("HSCAN is only available in non pipeline | queue mode."); + } + + connection.hSet("hscankey", "bar", "foobar"); + + connection.hSet("hscankey", "foo-1", "v-1"); + connection.hSet("hscankey", "foo-2", "v-2"); + connection.hSet("hscankey", "foo-3", "v-3"); + + Cursor> cursor = connection + .hScan("hscankey", scanOptions().count(2).match("fo*").build()); + + int i = 0; + while (cursor.hasNext()) { + + String key = cursor.next().getKey(); + + assertThat(key, not(containsString("bar"))); + assertThat(key, containsString("foo")); + + i++; + } + + assertThat(i, is(3)); + } + protected void verifyResults(List expected) { assertEquals(expected, getResults()); } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java index e1d4b8fff..0f92b36ce 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2013 the original author or authors. + * Copyright 2013-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. @@ -15,13 +15,12 @@ */ package org.springframework.data.redis.core; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; -import static org.springframework.data.redis.matcher.RedisTestMatchers.isEqual; +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; import java.util.Arrays; import java.util.Collection; -import java.util.LinkedHashMap; +import java.util.Iterator; import java.util.Map; import org.junit.After; @@ -35,12 +34,14 @@ import org.springframework.data.redis.RawObjectFactory; import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.StringObjectFactory; import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.srp.SrpConnectionFactory; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.test.annotation.IfProfileValue; /** * Integration test of {@link DefaultHashOperations} * * @author Jennifer Hickey + * @author Christoph Strobl * @param Key type * @param Hash key type * @param Hash value type @@ -70,17 +71,17 @@ public class DefaultHashOperationsTests { ObjectFactory stringFactory = new StringObjectFactory(); ObjectFactory rawFactory = new RawObjectFactory(); - SrpConnectionFactory srConnFactory = new SrpConnectionFactory(); - srConnFactory.setPort(SettingsUtils.getPort()); - srConnFactory.setHostName(SettingsUtils.getHost()); - srConnFactory.afterPropertiesSet(); + JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(); + jedisConnectionFactory.setPort(SettingsUtils.getPort()); + jedisConnectionFactory.setHostName(SettingsUtils.getHost()); + jedisConnectionFactory.afterPropertiesSet(); RedisTemplate stringTemplate = new StringRedisTemplate(); - stringTemplate.setConnectionFactory(srConnFactory); + stringTemplate.setConnectionFactory(jedisConnectionFactory); stringTemplate.afterPropertiesSet(); RedisTemplate rawTemplate = new RedisTemplate(); - rawTemplate.setConnectionFactory(srConnFactory); + rawTemplate.setConnectionFactory(jedisConnectionFactory); rawTemplate.setEnableDefaultSerializer(false); rawTemplate.afterPropertiesSet(); @@ -112,10 +113,11 @@ public class DefaultHashOperationsTests { HV val2 = hashValueFactory.instance(); hashOps.put(key, key1, val1); hashOps.put(key, key2, val2); - Map expected = new LinkedHashMap(); - expected.put(key1, val1); - expected.put(key2, val2); - assertThat(hashOps.entries(key), isEqual(expected)); + + for (Map.Entry entry : hashOps.entries(key).entrySet()) { + assertThat(entry.getKey(), anyOf(equalTo(key1), equalTo(key2))); + assertThat(entry.getValue(), anyOf(equalTo(val1), equalTo(val2))); + } } @Test @@ -130,4 +132,33 @@ public class DefaultHashOperationsTests { hashOps.delete(key, key1, key2); assertTrue(hashOps.keys(key).isEmpty()); } + + /** + * @see DATAREDIS-305 + */ + @Test + @IfProfileValue(name = "redisVersion", value = "2.8+") + public void testHScanReadsValuesFully() { + + K key = keyFactory.instance(); + HK key1 = hashKeyFactory.instance(); + HV val1 = hashValueFactory.instance(); + HK key2 = hashKeyFactory.instance(); + HV val2 = hashValueFactory.instance(); + hashOps.put(key, key1, val1); + hashOps.put(key, key2, val2); + + Iterator> it = hashOps.scan(key, ScanOptions.scanOptions().count(1).build()); + + long count = 0; + while (it.hasNext()) { + Map.Entry entry = it.next(); + assertThat(entry.getKey(), anyOf(equalTo(key1), equalTo(key2))); + assertThat(entry.getValue(), anyOf(equalTo(val1), equalTo(val2))); + count++; + } + + assertThat(count, is(hashOps.size(key))); + } + }