From f6a17fb2685d27a97ff813bddc91ccde4aae3573 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Tue, 3 Oct 2017 18:30:25 +0200 Subject: [PATCH] DATAREDIS-698 - Add support for HSTRLEN. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now support HSTRLEN command throug RedisHashCommands for Lettuce and Jedis in both an imperative and reactive manner. However, Jedis not natively supporting HSTRLEN via its API we’ve come up with some more reflective invocation allowing to execute commands currently not known by Jedis. We also added this behavior to the cluster implementation which as of now also supports RedisClusterConnection#execute. Original pull request: #283. --- .../DefaultStringRedisConnection.java | 23 ++- .../DefaultedRedisClusterConnection.java | 23 +++ .../connection/DefaultedRedisConnection.java | 7 + .../connection/ReactiveHashCommands.java | 79 +++++++++ .../connection/RedisClusterConnection.java | 22 +++ .../redis/connection/RedisHashCommands.java | 12 ++ .../connection/StringRedisConnection.java | 12 ++ .../connection/jedis/JedisClientUtils.java | 158 ++++++++++++++++++ .../jedis/JedisClusterConnection.java | 36 +++- .../jedis/JedisClusterHashCommands.java | 13 ++ .../connection/jedis/JedisConnection.java | 72 +------- .../connection/jedis/JedisHashCommands.java | 15 ++ .../lettuce/LettuceHashCommands.java | 26 +++ .../lettuce/LettuceReactiveHashCommands.java | 16 ++ .../data/redis/core/BoundHashOperations.java | 11 ++ .../core/DefaultBoundHashOperations.java | 11 ++ .../redis/core/DefaultHashOperations.java | 14 ++ .../data/redis/core/HashOperations.java | 12 ++ .../AbstractConnectionIntegrationTests.java | 30 ++++ .../connection/ClusterConnectionTests.java | 9 + .../jedis/JedisClusterConnectionTests.java | 48 +++++- .../LettuceClusterConnectionTests.java | 21 +++ .../LettuceReactiveHashCommandsTests.java | 26 +++ .../core/DefaultHashOperationsTests.java | 19 +++ 24 files changed, 641 insertions(+), 74 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java 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 aede7d0b9..f2724bc92 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -3201,6 +3201,16 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return this.delegate.hScan(key, options); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisSetCommands#hStrLen(byte[], byte[]) + */ + @Nullable + @Override + public Long hStrLen(byte[] key, byte[] field) { + return convertAndReturn(delegate.hStrLen(key, field), identityConverter); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisServerCommands#setClientName(java.lang.String) @@ -3263,6 +3273,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco }); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#hStrLen(java.lang.String, java.lang.String) + */ + @Override + public Long hStrLen(String key, String field) { + return hStrLen(serialize(key), serialize(field)); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.StringRedisConnection#sScan(java.lang.String, org.springframework.data.redis.core.ScanOptions) @@ -3472,8 +3491,8 @@ public class DefaultStringRedisConnection implements StringRedisConnection, Deco return null; } - - return value == null ? null : ObjectUtils.nullSafeEquals(converter, identityConverter) ? (T) value : (T) converter.convert(value); + return value == null ? null + : ObjectUtils.nullSafeEquals(converter, identityConverter) ? (T) value : (T) converter.convert(value); } private void addResultConverter(Converter converter) { diff --git a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java index 9fbefc428..fe210456e 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisClusterConnection.java @@ -15,10 +15,14 @@ */ package org.springframework.data.redis.connection; +import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Properties; import org.springframework.data.redis.core.types.RedisClientInfo; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; /** * @author Christoph Strobl @@ -131,4 +135,23 @@ public interface DefaultedRedisClusterConnection extends RedisClusterConnection, default List getClientList(RedisClusterNode node) { return serverCommands().getClientList(node); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#execute(String, byte[], Collection) + */ + @Nullable + @Override + default T execute(String command, byte[] key, Collection args) { + + Assert.notNull(command, "Command must not be null!"); + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(args, "Args must not be null!"); + + ArrayList allArgs = new ArrayList(); + allArgs.add(key); + allArgs.addAll(args); + + return (T) execute(command, allArgs.toArray(new byte[allArgs.size()][])); + } } 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 62591279b..5ee509042 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultedRedisConnection.java @@ -916,6 +916,13 @@ public interface DefaultedRedisConnection extends RedisConnection { return hashCommands().hScan(key, options); } + /** @deprecated in favor of {@link RedisConnection#hashCommands()}. */ + @Override + @Deprecated + default Long hStrLen(byte[] key, byte[] field) { + return hashCommands().hStrLen(key, field); + } + // GEO COMMANDS /** @deprecated in favor of {@link RedisConnection#geoCommands()}}. */ diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java index 03979020d..bf9a3ab61 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -578,4 +578,83 @@ public interface ReactiveHashCommands { * @see Redis Documentation: HGETALL */ Flux>>> hGetAll(Publisher commands); + + /** + * @author Christoph Strobl + * @see Redis Documentation: HSTRLEN + * @since 2.1 + */ + class HStrLenCommand extends KeyCommand { + + private ByteBuffer field; + + /** + * Creates a new {@link HStrLenCommand} given a {@code key}. + * + * @param key can be {@literal null}. + * @param field must not be {@literal null}. + */ + private HStrLenCommand(@Nullable ByteBuffer key, ByteBuffer field) { + + super(key); + this.field = field; + } + + /** + * Specify the {@code field} within the hash to get the length of the {@code value} of.ø + * + * @param field must not be {@literal null}. + * @return new instance of {@link HStrLenCommand}. + */ + public static HStrLenCommand lengthOf(ByteBuffer field) { + + Assert.notNull(field, "Field must not be null!"); + return new HStrLenCommand(null, field); + } + + /** + * Define the {@code key} the hash is stored at. + * + * @param key must not be {@literal null}. + * @return new instance of {@link HStrLenCommand}. + */ + public HStrLenCommand from(ByteBuffer key) { + return new HStrLenCommand(key, field); + } + + /** + * @return {@literal null} if not already set. + */ + @Nullable + public ByteBuffer getField() { + return field; + } + } + + /** + * Get the length of the value associated with {@code hashKey}. If either the {@code key} or the {@code hashKey} do + * not exist, {@code 0} is emitted. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return never {@literal null}. + * @since 2.1 + */ + default Mono hStrLen(ByteBuffer key, ByteBuffer field) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(field, "Field must not be null!"); + + return hStrLen(Mono.just(HStrLenCommand.lengthOf(field).from(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get the length of the value associated with {@code hashKey}. If either the {@code key} or the {@code hashKey} do + * not exist, {@code 0} is emitted. + * + * @param commands must not be {@literal null}. + * @return never {@literal null}. + * @since 2.1 + */ + Flux> hStrLen(Publisher commands); } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java index 7af3f274e..8bb79ef60 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConnection.java @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.connection; +import java.util.Collection; import java.util.Set; import org.springframework.lang.Nullable; @@ -56,6 +57,27 @@ public interface RedisClusterConnection extends RedisConnection, RedisClusterCom @Nullable byte[] randomKey(RedisClusterNode node); + /** + * Execute the given command for the {@code key} provided potentially appending args.
+ * This method, other than {@link #execute(String, byte[]...)}, dispatches the command to the {@code key} serving + * master node. + * + *
+	 * 
+	 * // SET foo bar EX 10 NX
+	 * execute("SET", "foo".getBytes(), asBinaryList("bar", "EX", 10, "NX")
+	 * 
+	 * 
+ * + * @param command must not be {@literal null}. + * @param key must not be {@literal null}. + * @param args must not be {@literal null}. + * @return command result as delivered by the underlying Redis driver. Can be {@literal null}. + * @since 2.1 + */ + @Nullable + T execute(String command, byte[] key, Collection args); + /** * Get {@link RedisClusterServerCommands}. * 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 d4bb421b6..7da765a3d 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisHashCommands.java @@ -183,4 +183,16 @@ public interface RedisHashCommands { * @see Redis Documentation: HSCAN */ Cursor> hScan(byte[] key, ScanOptions options); + + /** + * Returns the length of the value associated with {@code field} in the hash stored at {@code key}. If the key or the + * field do not exist, {@code 0} is returned. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + */ + @Nullable + Long hStrLen(byte[] key, byte[] field); } 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 86e24b8c1..1b8ad7eac 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -1509,6 +1509,18 @@ public interface StringRedisConnection extends RedisConnection { */ Cursor> hScan(String key, ScanOptions options); + /** + * Returns the length of the value associated with {@code field} in the hash stored at {@code key}. If the key or the + * field do not exist, {@code 0} is returned. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + */ + @Nullable + Long hStrLen(String key, String field); + // ------------------------------------------------------------------------- // Methods dealing with HyperLogLog // ------------------------------------------------------------------------- diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java new file mode 100644 index 000000000..eb79a2607 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java @@ -0,0 +1,158 @@ +/* + * Copyright 2017 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.connection.jedis; + +import redis.clients.jedis.BinaryJedis; +import redis.clients.jedis.Builder; +import redis.clients.jedis.Client; +import redis.clients.jedis.Connection; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.Protocol.Command; +import redis.clients.jedis.Queable; +import redis.clients.jedis.Response; +import redis.clients.util.RedisOutputStream; +import redis.clients.util.SafeEncoder; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.function.Supplier; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * @author Christoph Strobl + * @since 2.1 + */ +class JedisClientUtils { + + private static final Field CLIENT_FIELD; + private static final Method SEND_COMMAND; + private static final Method GET_RESPONSE; + private static final Method PROTOCOL_SEND_COMMAND; + + static { + + CLIENT_FIELD = ReflectionUtils.findField(BinaryJedis.class, "client", Client.class); + ReflectionUtils.makeAccessible(CLIENT_FIELD); + + PROTOCOL_SEND_COMMAND = ReflectionUtils.findMethod(Protocol.class, "sendCommand", RedisOutputStream.class, + byte[].class, byte[][].class); + ReflectionUtils.makeAccessible(PROTOCOL_SEND_COMMAND); + + try { + Class commandType = ClassUtils.isPresent("redis.clients.jedis.ProtocolCommand", null) + ? ClassUtils.forName("redis.clients.jedis.ProtocolCommand", null) + : ClassUtils.forName("redis.clients.jedis.Protocol$Command", null); + + SEND_COMMAND = ReflectionUtils.findMethod(Connection.class, "sendCommand", + new Class[] { commandType, byte[][].class }); + } catch (Exception e) { + throw new NoClassDefFoundError( + "Could not find required flavor of command required by 'redis.clients.jedis.Connection#sendCommand'."); + } + + ReflectionUtils.makeAccessible(SEND_COMMAND); + + GET_RESPONSE = ReflectionUtils.findMethod(Queable.class, "getResponse", Builder.class); + ReflectionUtils.makeAccessible(GET_RESPONSE); + } + + @Nullable + static T execute(String command, Collection keys, Collection args, Supplier jedis) { + + List mArgs = new ArrayList<>(keys); + mArgs.addAll(args); + + Client client = retrieveClient(jedis.get()); + sendCommand(client, command, mArgs.toArray(new byte[mArgs.size()][])); + + return (T) client.getOne(); + } + + static Client retrieveClient(Jedis jedis) { + return (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis); + } + + static Client sendCommand(Jedis jedis, String command, byte[][] args) { + + Client client = retrieveClient(jedis); + + if (isKnownCommand(command)) { + ReflectionUtils.invokeMethod(SEND_COMMAND, client, Command.valueOf(command.trim().toUpperCase()), args); + } else { + sendProtocolCommand(client, command, args); + } + + return client; + } + + static void sendCommand(Client client, String command, byte[][] args) { + + if (isKnownCommand(command)) { + ReflectionUtils.invokeMethod(SEND_COMMAND, client, Command.valueOf(command.trim().toUpperCase()), args); + } else { + sendProtocolCommand(client, command, args); + } + } + + static void sendProtocolCommand(Client client, String command, byte[][] args) { + + DirectFieldAccessor dfa = new DirectFieldAccessor(client); + + client.connect(); + + RedisOutputStream os = (RedisOutputStream) dfa.getPropertyValue("outputStream"); + ReflectionUtils.invokeMethod(PROTOCOL_SEND_COMMAND, null, os, SafeEncoder.encode(command), args); + + Integer pipelinedCommands = (Integer) dfa.getPropertyValue("pipelinedCommands"); + dfa.setPropertyValue("pipelinedCommands", pipelinedCommands.intValue() + 1); + } + + static boolean isKnownCommand(String command) { + + try { + Command.valueOf(command); + return true; + } catch (IllegalArgumentException e) { + return false; + } + } + + static boolean isInMulti(Jedis jedis) { + return retrieveClient(jedis).isInMulti(); + } + + static Response getGetResponse(Object target) { + + return (Response) ReflectionUtils.invokeMethod(GET_RESPONSE, target, new Builder() { + public Object build(Object data) { + return data; + } + + public String toString() { + return "Object"; + } + }); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java index 217596b49..3167a8ad9 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterConnection.java @@ -23,7 +23,10 @@ import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisClusterConnectionHandler; import redis.clients.jedis.JedisPool; +import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; @@ -130,8 +133,34 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { @Override public Object execute(String command, byte[]... args) { - // TODO: execute command on all nodes? or throw exception requiring to execute command on a specific node - throw new UnsupportedOperationException("Execute is currently not supported in cluster mode."); + Assert.notNull(command, "Command must not be null!"); + + return clusterCommandExecutor + .executeCommandOnArbitraryNode((JedisClusterCommandCallback) client -> JedisClientUtils.execute(command, + Collections.emptyList(), Arrays.asList(args), () -> client)) + .getValue(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#execute(String, byte[], java.util.Collection) + */ + @Nullable + @Override + public T execute(String command, byte[] key, Collection args) { + + Assert.notNull(command, "Command must not be null!"); + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(args, "Args must not be null!"); + + Collection commandArgs = new ArrayList<>(); + commandArgs.add(key); + commandArgs.addAll(args); + + RedisClusterNode keyMaster = topologyProvider.getTopology().getKeyServingMasterNode(key); + + return clusterCommandExecutor.executeCommandOnSingleNode((JedisClusterCommandCallback) client -> JedisClientUtils + .execute(command, Collections.emptyList(), commandArgs, () -> client), keyMaster).getValue(); } /* @@ -787,8 +816,7 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { PropertyAccessor accessor = new DirectFieldAccessFallbackBeanWrapper(cluster); this.connectionHandler = accessor.isReadableProperty("connectionHandler") - ? (JedisClusterConnectionHandler) accessor.getPropertyValue("connectionHandler") - : null; + ? (JedisClusterConnectionHandler) accessor.getPropertyValue("connectionHandler") : null; } else { this.connectionHandler = null; } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java index 7cad0b3cd..7ba089a47 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClusterHashCommands.java @@ -18,6 +18,7 @@ package org.springframework.data.redis.connection.jedis; import redis.clients.jedis.ScanParams; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Map.Entry; @@ -29,6 +30,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -286,7 +288,18 @@ class JedisClusterHashCommands implements RedisHashCommands { }.open(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hStrLen(byte[], byte[]) + */ + @Nullable + @Override + public Long hStrLen(byte[] key, byte[] field) { + return Long.class.cast(connection.execute("HSTRLEN", key, Collections.singleton(field))); + } + private DataAccessException convertJedisAccessException(Exception ex) { + return connection.convertJedisAccessException(ex); } } 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 1ee1e844a..834f231ae 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 @@ -15,22 +15,15 @@ */ package org.springframework.data.redis.connection.jedis; -import redis.clients.jedis.BinaryJedis; import redis.clients.jedis.BinaryJedisPubSub; -import redis.clients.jedis.Builder; import redis.clients.jedis.Client; -import redis.clients.jedis.Connection; import redis.clients.jedis.Jedis; import redis.clients.jedis.Pipeline; -import redis.clients.jedis.Protocol.Command; -import redis.clients.jedis.Queable; import redis.clients.jedis.Response; import redis.clients.jedis.Transaction; import redis.clients.jedis.exceptions.JedisDataException; import redis.clients.util.Pool; -import java.lang.reflect.Field; -import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -48,10 +41,8 @@ import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.convert.TransactionResultConverter; import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; /** @@ -70,38 +61,10 @@ import org.springframework.util.StringUtils; */ public class JedisConnection extends AbstractRedisConnection { - private static final Field CLIENT_FIELD; - private static final Method SEND_COMMAND; - private static final Method GET_RESPONSE; - private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy( JedisConverters.exceptionConverter()); - static { - - CLIENT_FIELD = ReflectionUtils.findField(BinaryJedis.class, "client", Client.class); - ReflectionUtils.makeAccessible(CLIENT_FIELD); - - try { - Class commandType = ClassUtils.isPresent("redis.clients.jedis.ProtocolCommand", null) - ? ClassUtils.forName("redis.clients.jedis.ProtocolCommand", null) - : ClassUtils.forName("redis.clients.jedis.Protocol$Command", null); - - SEND_COMMAND = ReflectionUtils.findMethod(Connection.class, "sendCommand", - new Class[] { commandType, byte[][].class }); - } catch (Exception e) { - throw new NoClassDefFoundError( - "Could not find required flavor of command required by 'redis.clients.jedis.Connection#sendCommand'."); - } - - ReflectionUtils.makeAccessible(SEND_COMMAND); - - GET_RESPONSE = ReflectionUtils.findMethod(Queable.class, "getResponse", Builder.class); - ReflectionUtils.makeAccessible(GET_RESPONSE); - } - private final Jedis jedis; - private final Client client; private @Nullable Transaction transaction; private final @Nullable Pool pool; /** @@ -117,6 +80,7 @@ public class JedisConnection extends AbstractRedisConnection { private Queue>> txResults = new LinkedList<>(); class JedisResult extends FutureResult> { + public JedisResult(Response resultHolder, Converter converter) { super(resultHolder, converter); } @@ -180,9 +144,6 @@ public class JedisConnection extends AbstractRedisConnection { */ protected JedisConnection(Jedis jedis, @Nullable Pool pool, int dbIndex, String clientName) { - // extract underlying connection for batch operations - client = (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis); - this.jedis = jedis; this.pool = pool; this.dbIndex = dbIndex; @@ -319,21 +280,13 @@ public class JedisConnection extends AbstractRedisConnection { Collections.addAll(mArgs, args); } - ReflectionUtils.invokeMethod(SEND_COMMAND, client, Command.valueOf(command.trim().toUpperCase()), - mArgs.toArray(new byte[mArgs.size()][])); + Client client = JedisClientUtils.sendCommand(this.jedis, command, mArgs.toArray(new byte[mArgs.size()][])); + if (isQueueing() || isPipelined()) { Object target = (isPipelined() ? pipeline : transaction); @SuppressWarnings("unchecked") - Response result = (Response) ReflectionUtils.invokeMethod(GET_RESPONSE, target, - new Builder() { - public Object build(Object data) { - return data; - } - public String toString() { - return "Object"; - } - }); + Response result = JedisClientUtils.getGetResponse(target); if (isPipelined()) { pipeline(new JedisResult(result)); } else { @@ -378,19 +331,6 @@ public class JedisConnection extends AbstractRedisConnection { } // else close the connection normally (doing the try/catch dance) Exception exc = null; - if (isQueueing()) { - try { - client.quit(); - } catch (Exception o_O) { - // ignore exception - } - try { - client.disconnect(); - } catch (Exception o_O) { - // ignore exception - } - return; - } try { jedis.quit(); } catch (Exception ex) { @@ -433,7 +373,7 @@ public class JedisConnection extends AbstractRedisConnection { */ @Override public boolean isQueueing() { - return client.isInMulti(); + return JedisClientUtils.isInMulti(jedis); } /* @@ -652,7 +592,7 @@ public class JedisConnection extends AbstractRedisConnection { return new JedisStatusResult(response); } - JedisStatusResult newStatusResult(Response response, Converter converter) { + JedisStatusResult newStatusResult(Response response, Converter converter) { return new JedisStatusResult(response, converter); } diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java index 2250b35b1..78ed24152 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisHashCommands.java @@ -31,6 +31,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -414,6 +415,20 @@ class JedisHashCommands implements RedisHashCommands { }.open(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hStrLen(byte[], byte[]) + */ + @Nullable + @Override + public Long hStrLen(byte[] key, byte[] field) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(field, "Field must not be null!"); + + return Long.class.cast(connection.execute("HSTRLEN", key, field)); + } + private boolean isPipelined() { return connection.isPipelined(); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java index 771d9563c..109af3970 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceHashCommands.java @@ -35,6 +35,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -418,6 +419,31 @@ class LettuceHashCommands implements RedisHashCommands { }.open(); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisHashCommands#hStrLen(byte[], byte[]) + */ + @Nullable + @Override + public Long hStrLen(byte[] key, byte[] field) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(field, "Field must not be null!"); + try { + if (isPipelined()) { + pipeline(connection.newLettuceResult(getAsyncConnection().hstrlen(key, field))); + return null; + } + if (isQueueing()) { + transaction(connection.newLettuceTxResult(getConnection().hstrlen(key, field))); + return null; + } + return getConnection().hstrlen(key, field); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + private boolean isPipelined() { return connection.isPipelined(); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java index 8a23b757f..99827eafb 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -214,4 +214,20 @@ class LettuceReactiveHashCommands implements ReactiveHashCommands { return Mono.just(new CommandResponse<>(command, result.flatMapMany(v -> Flux.fromStream(v.entrySet().stream())))); })); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hstrlen(org.reactivestreams.Publisher) + */ + @Override + public Flux> hStrLen(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Command.getKey() must not be null!"); + Assert.notNull(command.getField(), "Command.getField() must not be null!"); + + return cmd.hstrlen(command.getKey(), command.getField()).map(value -> new NumericResponse<>(command, value)); + })); + } } 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 b4a4a9015..e9b43d64c 100644 --- a/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/BoundHashOperations.java @@ -96,6 +96,17 @@ public interface BoundHashOperations extends BoundKeyOperations { @Nullable Set keys(); + /** + * Returns the length of the value associated with {@code hashKey}. If the {@code hashKey} do not exist, {@code 0} is + * returned. + * + * @param hashKey must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + */ + @Nullable + Long lengthOfValue(HK hashKey); + /** * Get size of hash at the bound key. * 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 76686f95d..7f0f8d76d 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultBoundHashOperations.java @@ -22,6 +22,7 @@ import java.util.Map.Entry; import java.util.Set; import org.springframework.data.redis.connection.DataType; +import org.springframework.lang.Nullable; /** * Default implementation for {@link HashOperations}. @@ -118,6 +119,16 @@ class DefaultBoundHashOperations extends DefaultBoundKeyOperations return ops.keys(getKey()); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.BoundHashOperations#lengthOfValue(java.lang.Object, java.lang.Object) + */ + @Nullable + @Override + public Long lengthOfValue(HK hashKey) { + return ops.lengthOfValue(getKey(), hashKey); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.BoundHashOperations#size() 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 3dd849fe6..8aa55055e 100644 --- a/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/DefaultHashOperations.java @@ -24,6 +24,7 @@ import java.util.Map.Entry; import java.util.Set; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; /** * Default implementation of {@link HashOperations}. @@ -114,6 +115,19 @@ class DefaultHashOperations extends AbstractOperations imp return execute(connection -> connection.hLen(rawKey), true); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.core.HashOperations#lengthOfValue(java.lang.Object, java.lang.Object) + */ + @Nullable + @Override + public Long lengthOfValue(K key, HK hashKey) { + + byte[] rawKey = rawKey(key); + byte[] rawHashKey = rawHashKey(hashKey); + return execute(connection -> connection.hStrLen(rawKey, rawHashKey), true); + } + /* * (non-Javadoc) * @see org.springframework.data.redis.core.HashOperations#putAll(java.lang.Object, java.util.Map) 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 e278e2d47..948db810b 100644 --- a/src/main/java/org/springframework/data/redis/core/HashOperations.java +++ b/src/main/java/org/springframework/data/redis/core/HashOperations.java @@ -96,6 +96,18 @@ public interface HashOperations { */ Set keys(H key); + /** + * Returns the length of the value associated with {@code hashKey}. If either the {@code key} or the {@code hashKey} + * do not exist, {@code 0} is returned. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @return {@literal null} when used in pipeline / transaction. + * @since 2.1 + */ + @Nullable + Long lengthOfValue(H key, HK hashKey); + /** * Get size of hash at {@code key}. * 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 fc1e269e0..316bf6199 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -2648,6 +2648,36 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(((GeoResults>) results.get(1)).getContent(), hasSize(2)); } + @Test // DATAREDIS-698 + public void hStrLenReturnsFieldLength() { + + actual.add(connection.hSet("hash-hstrlen", "key-1", "value-1")); + actual.add(connection.hSet("hash-hstrlen", "key-2", "value-2")); + actual.add(connection.hStrLen("hash-hstrlen", "key-2")); + + verifyResults( + Arrays.asList(new Object[] { Boolean.TRUE, Boolean.TRUE, Long.valueOf("value-2".length()) })); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenFieldDoesNotExist() { + + actual.add(connection.hSet("hash-hstrlen", "key-1", "value-1")); + actual.add(connection.hStrLen("hash-hstrlen", "key-2")); + + verifyResults( + Arrays.asList(new Object[] { Boolean.TRUE, 0L })); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenKeyDoesNotExist() { + + actual.add(connection.hStrLen("hash-no-exist", "key-2")); + + verifyResults( + Arrays.asList(new Object[] { 0L })); + } + protected void verifyResults(List expected) { assertEquals(expected, getResults()); } diff --git a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java index 1bf559015..6be7493b8 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -633,4 +633,13 @@ public interface ClusterConnectionTests { // DATAREDIS-438 void geoRemoveDeletesMembers(); + + // DATAREDIS-698 + void hStrLenReturnsFieldLength(); + + // DATAREDIS-698 + void hStrLenReturnsZeroWhenFieldDoesNotExist(); + + // DATAREDIS-698 + void hStrLenReturnsZeroWhenKeyDoesNotExist(); } 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 b5fc03e0d..973af65a0 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 @@ -22,7 +22,6 @@ import static org.springframework.data.redis.connection.RedisGeoCommands.Distanc import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; import static org.springframework.data.redis.core.ScanOptions.*; -import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import redis.clients.jedis.HostAndPort; import redis.clients.jedis.JedisCluster; import redis.clients.jedis.JedisPool; @@ -57,6 +56,7 @@ import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.DefaultSortParameters; import org.springframework.data.redis.connection.DefaultTuple; import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; @@ -1729,7 +1729,8 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { public void infoShouldCollectionInfoFromAllClusterNodes() { Properties singleNodeInfo = clusterConnection.serverCommands().info(new RedisClusterNode("127.0.0.1", 7380)); - assertThat(Double.valueOf(clusterConnection.serverCommands().info().size()), closeTo(singleNodeInfo.size() * 3, 12d)); + assertThat(Double.valueOf(clusterConnection.serverCommands().info().size()), + closeTo(singleNodeInfo.size() * 3, 12d)); } @Test // DATAREDIS-315 @@ -2135,4 +2136,47 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { assertThat(clusterConnection.geoRemove(KEY_1_BYTES, ARIGENTO.getName()), is(1L)); } + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-689 + public void executeWithNoKeyAndArgsThrowsException() { + clusterConnection.execute("KEYS", null, Collections.singletonList("*".getBytes())); + } + + @Test // DATAREDIS-689 + public void executeWithArgs() { + + assertThat(clusterConnection.execute("SET", KEY_1_BYTES, VALUE_1_BYTES), is("OK".getBytes())); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + @Test // DATAREDIS-689 + public void executeWithKeyAndArgs() { + + assertThat(clusterConnection.execute("SET", KEY_1_BYTES, Collections.singletonList(VALUE_1_BYTES)), + is("OK".getBytes())); + + assertThat(nativeConnection.get(KEY_1), is(VALUE_1)); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsFieldLength() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_2_BYTES), is(Long.valueOf(VALUE_3.length()))); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_3_BYTES), is(0L)); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenKeyDoesNotExist() { + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_1_BYTES), is(0L)); + } } 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 6b4f09bc6..589d70f62 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 @@ -2145,4 +2145,25 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { assertThat(clusterConnection.geoRemove(KEY_1_BYTES, ARIGENTO_BYTES.getName()), is(1L)); } + + @Test // DATAREDIS-698 + public void hStrLenReturnsFieldLength() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_2_BYTES), is(Long.valueOf(VALUE_3.length()))); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenFieldDoesNotExist() { + + nativeConnection.hset(KEY_1, KEY_2, VALUE_3); + + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_3_BYTES), is(0L)); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenKeyDoesNotExist() { + assertThat(clusterConnection.hashCommands().hStrLen(KEY_1_BYTES, KEY_1_BYTES), is(0L)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java index b7780c8cc..a6dd4524c 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java @@ -205,6 +205,32 @@ public class LettuceReactiveHashCommandsTests extends LettuceReactiveCommandsTes assertTrue(list.containsAll(expected.entrySet())); }) // .verifyComplete(); + } + @Test // DATAREDIS-698 + public void hStrLenReturnsFieldLength() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + + StepVerifier.create(connection.hashCommands().hStrLen(KEY_1_BBUFFER, FIELD_1_BBUFFER)) + .expectNext(Long.valueOf(VALUE_1.length())) // + .verifyComplete(); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenFieldDoesNotExist() { + + nativeCommands.hset(KEY_1, FIELD_2, VALUE_3); + + StepVerifier.create(connection.hashCommands().hStrLen(KEY_1_BBUFFER, FIELD_1_BBUFFER)).expectNext(0L) // + .verifyComplete(); + } + + @Test // DATAREDIS-698 + public void hStrLenReturnsZeroWhenKeyDoesNotExist() { + + StepVerifier.create(connection.hashCommands().hStrLen(KEY_1_BBUFFER, FIELD_1_BBUFFER)).expectNext(0L) // + .verifyComplete(); } } 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 465082ea0..fd60829cc 100644 --- a/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java +++ b/src/test/java/org/springframework/data/redis/core/DefaultHashOperationsTests.java @@ -17,6 +17,7 @@ package org.springframework.data.redis.core; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; import java.io.IOException; import java.util.Arrays; @@ -172,4 +173,22 @@ public class DefaultHashOperationsTests { assertThat(count, is(hashOps.size(key))); } + @Test // DATAREDIS-698 + @IfProfileValue(name = "redisVersion", value = "3.0.3+") + public void lengthOfValue() throws IOException { + + assumeThat(hashValueFactory instanceof StringObjectFactory, is(true)); + + 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); + + assertThat(hashOps.lengthOfValue(key, key1), is(Long.valueOf(val1.toString().length()))); + } + }