diff --git a/pom.xml b/pom.xml index 617c18c957..d15db14de6 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,7 @@ org.springframework.data spring-data-redis - 2.1.0.BUILD-SNAPSHOT + 2.1.0.DATAREDIS-698-SNAPSHOT Spring Data Redis 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 aede7d0b95..e87dbd62cc 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -15,17 +15,8 @@ */ package org.springframework.data.redis.connection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.Map.Entry; -import java.util.Properties; -import java.util.Queue; -import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; @@ -3047,7 +3038,7 @@ public void openPipeline() { */ @Override public Object execute(String command) { - return execute(command, (byte[][]) null); + return execute(command, EMPTY_2D_BYTE_ARRAY); } /* @@ -3201,6 +3192,16 @@ public Cursor> hScan(byte[] key, ScanOptions options) { 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 +3264,15 @@ public String setValue(String value) { }); } + /* + * (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 +3482,8 @@ private T convertAndReturn(@Nullable Object value, Converter converter) { 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 9fbefc4282..7b06aebd25 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,13 @@ */ package org.springframework.data.redis.connection; +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 +134,29 @@ default Long time(RedisClusterNode node) { default List getClientList(RedisClusterNode node) { return serverCommands().getClientList(node); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisClusterConnection#execute(String, byte[], Collection) + */ + @Nullable + @Override + @SuppressWarnings("unchecked") + 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!"); + + byte[][] commandArgs = new byte[args.size() + 1][]; + + commandArgs[0] = key; + int targetIndex = 1; + + for (byte[] binaryArgument : args) { + commandArgs[targetIndex++] = binaryArgument; + } + + return (T) execute(command, commandArgs); + } } 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 62591279bf..5ee509042c 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 @@ default Cursor> hScan(byte[] key, ScanOptions options) { 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 03979020d4..a737afd0b7 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,82 @@ default Flux> hGetAll(ByteBuffer key) { * @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 the field. + */ + public ByteBuffer getField() { + return field; + } + } + + /** + * Get the length of the value associated with {@code field}. If either the {@code key} or the {@code field} 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 field}. If either the {@code key} or the {@code field} 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 7af3f274e8..b690ac475a 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 d4bb421b6f..7da765a3d1 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 86e24b8c15..1b8ad7eacb 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 @@ interface StringTuple extends Tuple { */ 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 0000000000..3fa9537b93 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisClientUtils.java @@ -0,0 +1,200 @@ +/* + * 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.Arrays; +import java.util.Set; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.util.ClassUtils; +import org.springframework.util.ReflectionUtils; + +/** + * Utility class to dispatch arbitrary Redis commands using Jedis commands. + * + * @author Christoph Strobl + * @author Mark Paluch + * @since 2.1 + */ +@SuppressWarnings({ "unchecked", "ConstantConditions" }) +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; + private static final Set KNOWN_COMMANDS; + private static final Builder OBJECT_BUILDER; + + 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", 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); + + KNOWN_COMMANDS = Arrays.stream(Command.values()).map(Enum::name).collect(Collectors.toSet()); + + OBJECT_BUILDER = new Builder() { + public Object build(Object data) { + return data; + } + + public String toString() { + return "Object"; + } + }; + } + + /** + * Execute an arbitrary on the supplied {@link Jedis} instance. + * + * @param command the command. + * @param keys must not be {@literal null}, may be empty. + * @param args must not be {@literal null}, may be empty. + * @param jedis must not be {@literal null}. + * @return the response, can be be {@literal null}. + */ + static T execute(String command, byte[][] keys, byte[][] args, Supplier jedis) { + + byte[][] commandArgs = getCommandArguments(keys, args); + + Client client = sendCommand(command, commandArgs, jedis.get()); + + return (T) client.getOne(); + } + + /** + * Send a Redis command and retrieve the {@link Client} for response retrieval. + * + * @param command the command. + * @param args must not be {@literal null}, may be empty. + * @param jedis must not be {@literal null}. + * @return the {@link Client} instance used to send the command. + */ + static Client sendCommand(String command, byte[][] args, Jedis jedis) { + + Client client = retrieveClient(jedis); + + sendCommand(client, command, args); + + return client; + } + + private 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); + } + } + + private static void sendProtocolCommand(Client client, String command, byte[][] args) { + + // quite expensive to construct for each command invocation + 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 + 1); + } + + private static boolean isKnownCommand(String command) { + return KNOWN_COMMANDS.contains(command); + } + + private static byte[][] getCommandArguments(byte[][] keys, byte[][] args) { + + if (keys.length == 0) { + return args; + } + + if (args.length == 0) { + return keys; + } + + byte[][] commandArgs = new byte[keys.length + args.length][]; + + System.arraycopy(keys, 0, commandArgs, 0, keys.length); + System.arraycopy(args, 0, commandArgs, keys.length, args.length); + + return commandArgs; + } + + /** + * @param jedis the client instance. + * @return {@literal true} if the connection has entered {@literal MULTI} state. + */ + static boolean isInMulti(Jedis jedis) { + return retrieveClient(jedis).isInMulti(); + } + + /** + * Retrieve the {@link Response} object from a {@link redis.clients.jedis.Transaction} or a + * {@link redis.clients.jedis.Pipeline} for response synchronization. + * + * @param target a {@link redis.clients.jedis.Transaction} or {@link redis.clients.jedis.Pipeline}, must not be + * {@literal null}. + * @return the {@link Response} wrapper object. + */ + static Response getResponse(Object target) { + return (Response) ReflectionUtils.invokeMethod(GET_RESPONSE, target, OBJECT_BUILDER); + } + + private static Client retrieveClient(Jedis jedis) { + return (Client) ReflectionUtils.getField(CLIENT_FIELD, jedis); + } +} 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 217596b49f..40ac08398f 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 @@ -67,6 +67,8 @@ public class JedisClusterConnection implements DefaultedRedisClusterConnection { private static final ExceptionTranslationStrategy EXCEPTION_TRANSLATION = new FallbackExceptionTranslationStrategy( JedisConverters.exceptionConverter()); + private static final byte[][] EMPTY_2D_BYTE_ARRAY = new byte[0][]; + private final Log log = LogFactory.getLog(getClass()); private final JedisCluster cluster; @@ -127,11 +129,51 @@ public JedisClusterConnection(JedisCluster cluster, ClusterCommandExecutor execu * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisCommands#execute(java.lang.String, byte[][]) */ + @Nullable @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!"); + Assert.notNull(args, "Args must not be null!"); + + return clusterCommandExecutor + .executeCommandOnArbitraryNode((JedisClusterCommandCallback) client -> JedisClientUtils.execute(command, + EMPTY_2D_BYTE_ARRAY, 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!"); + + byte[][] commandArgs = getCommandArguments(key, args); + + RedisClusterNode keyMaster = topologyProvider.getTopology().getKeyServingMasterNode(key); + + return clusterCommandExecutor.executeCommandOnSingleNode((JedisClusterCommandCallback) client -> JedisClientUtils + .execute(command, EMPTY_2D_BYTE_ARRAY, commandArgs, () -> client), keyMaster).getValue(); + } + + private static byte[][] getCommandArguments(byte[] key, Collection args) { + + byte[][] commandArgs = new byte[args.size() + 1][]; + + commandArgs[0] = key; + int targetIndex = 1; + + for (byte[] binaryArgument : args) { + commandArgs[targetIndex++] = binaryArgument; + } + + return commandArgs; } /* 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 7cad0b3cd4..7ba089a47e 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 @@ 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.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 @@ protected ScanIteration> doScan(long cursorId, ScanOptions }.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 1ee1e844a9..475d89af3e 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,7 @@ 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 +60,10 @@ */ 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 +79,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 +143,6 @@ public JedisConnection(Jedis jedis, Pool pool, int dbIndex) { */ 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; @@ -312,28 +272,18 @@ public RedisHyperLogLogCommands hyperLogLogCommands() { */ @Override public Object execute(String command, byte[]... args) { - Assert.hasText(command, "a valid command needs to be specified"); + + Assert.hasText(command, "A valid command needs to be specified!"); + Assert.notNull(args, "Arguments must not be null!"); + try { - List mArgs = new ArrayList<>(); - if (!ObjectUtils.isEmpty(args)) { - Collections.addAll(mArgs, args); - } - ReflectionUtils.invokeMethod(SEND_COMMAND, client, Command.valueOf(command.trim().toUpperCase()), - mArgs.toArray(new byte[mArgs.size()][])); + Client client = JedisClientUtils.sendCommand(command, args, this.jedis); + 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 + .getResponse(isPipelined() ? getRequiredPipeline() : getRequiredTransaction()); if (isPipelined()) { pipeline(new JedisResult(result)); } else { @@ -378,19 +328,6 @@ public void close() throws DataAccessException { } // 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 +370,7 @@ public boolean isClosed() { */ @Override public boolean isQueueing() { - return client.isInMulti(); + return JedisClientUtils.isInMulti(jedis); } /* @@ -652,7 +589,7 @@ JedisStatusResult newStatusResult(Response response) { 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 2250b35b1f..78ed241524 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.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 @@ protected void doClose() { }.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 771d9563c1..109af3970f 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.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 @@ protected void doClose() { }.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 8a23b757f9..326b638a0c 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 @@ public Flux>> 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(), "Key must not be null!"); + Assert.notNull(command.getField(), "Field 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 b4a4a90154..e9b43d64c1 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 76686f95d6..7f0f8d76de 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.Set; import org.springframework.data.redis.connection.DataType; +import org.springframework.lang.Nullable; /** * Default implementation for {@link HashOperations}. @@ -118,6 +119,16 @@ public Set keys() { 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 3dd849fe64..8aa55055e0 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.Set; import org.springframework.core.convert.converter.Converter; +import org.springframework.lang.Nullable; /** * Default implementation of {@link HashOperations}. @@ -114,6 +115,19 @@ public Long size(K key) { 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 e278e2d47d..948db810bd 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 fc1e269e03..316bf61999 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 void geoRadiusByMemberShouldApplyLimit() { 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 1bf5590159..6be7493b85 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/DefaultStringRedisConnectionTests.java b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java index cd3a7c2772..8b86c0d98c 100644 --- a/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/DefaultStringRedisConnectionTests.java @@ -18,16 +18,7 @@ import static org.junit.Assert.*; import static org.mockito.Mockito.*; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Map; -import java.util.Properties; -import java.util.Set; +import java.util.*; import java.util.concurrent.TimeUnit; import org.junit.Before; @@ -1702,7 +1693,7 @@ public void testEvalSha() { @Test public void testExecute() { - doReturn("foo").when(nativeConnection).execute("something", (byte[][]) null); + doReturn("foo").when(nativeConnection).execute("something", new byte[0][]); actual.add(connection.execute("something")); verifyResults(Arrays.asList(new Object[] { "foo" })); } 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 b5fc03e0d8..973af65a01 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.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.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 void zRangeByLexShouldReturnResultCorrectly() { 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 void geoRemoveDeletesMembers() { 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 6b4f09bc68..589d70f62e 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 void geoRemoveDeletesMembers() { 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 b7780c8ccc..a6dd4524c9 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 void hGetAllShouldReturnEntriesCorrectly() { 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 465082ea0b..fd60829cc6 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 @@ 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 void testHScanReadsValuesFully() throws IOException { 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()))); + } + }