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 702948003..5cdf6ee9e 100644 --- a/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/DefaultStringRedisConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -36,6 +36,7 @@ import org.springframework.data.redis.connection.convert.SetConverter; import org.springframework.data.redis.core.ConvertingCursor; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; @@ -730,6 +731,15 @@ public class DefaultStringRedisConnection implements StringRedisConnection { delegate.set(key, value); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOptions) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + delegate.set(key, value, expiration, option); + } + public Boolean setBit(byte[] key, long offset, boolean value) { return delegate.setBit(key, offset, value); } @@ -1860,6 +1870,14 @@ public class DefaultStringRedisConnection implements StringRedisConnection { delegate.set(serialize(key), serialize(value)); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.StringRedisConnection#set(java.lang.String, java.lang.String, org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOptions) + */ + public void set(String key, String value, Expiration expiration, SetOption option) { + set(serialize(key), serialize(value), expiration, option); + } + public Boolean setBit(String key, long offset, boolean value) { return delegate.setBit(serialize(key), offset, value); } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java index 52afdc755..305629e9c 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisStringCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,8 @@ package org.springframework.data.redis.connection; import java.util.List; import java.util.Map; +import org.springframework.data.redis.core.types.Expiration; + /** * String/Value-specific commands supported by Redis. * @@ -71,6 +73,18 @@ public interface RedisStringCommands { */ void set(byte[] key, byte[] value); + /** + * Set {@code value} for {@code key} applying timeouts from {@code expiration} if set and inserting/updating values + * depending on {@code options}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration can be {@literal null}. Defaulted to {@link Expiration#persistent()}. + * @param option can be {@literal null}. Defaulted to {@link SetOption#UPSERT}. + * @since 1.7 + */ + void set(byte[] key, byte[] value, Expiration expiration, SetOption option); + /** * Set {@code value} for {@code key}, only if {@code key} does not exist. *

@@ -278,4 +292,61 @@ public interface RedisStringCommands { * @return */ Long strLen(byte[] key); + + /** + * {@code SET} command arguments for {@code NX}, {@code XX}. + * + * @author Christoph Strobl + * @since 1.7 + */ + public static enum SetOption { + + /** + * Do not set any additional command argument. + * + * @return + */ + UPSERT, + + /** + * {@code NX} + * + * @return + */ + SET_IF_ABSENT, + + /** + * {@code XX} + * + * @return + */ + SET_IF_PRESENT; + + /** + * Do not set any additional command argument. + * + * @return + */ + public static SetOption upsert() { + return UPSERT; + } + + /** + * {@code XX} + * + * @return + */ + public static SetOption ifPresent() { + return SET_IF_PRESENT; + } + + /** + * {@code NX} + * + * @return + */ + public static SetOption ifAbsent() { + return SET_IF_ABSENT; + } + } } 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 c3c5b6548..b776bceef 100644 --- a/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/StringRedisConnection.java @@ -24,6 +24,7 @@ import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.RedisCallback; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.RedisSerializer; @@ -95,6 +96,18 @@ public interface StringRedisConnection extends RedisConnection { void set(String key, String value); + /** + * Set {@code value} for {@code key} applying timeouts from {@code expiration} if set and inserting/updating values + * depending on {@code options}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration can be {@literal null}. Defaulted to {@link Expiration#persistent()}. + * @param option can be {@literal null}. Defaulted to {@link SetOption#UPSERT}. + * @since 1.7 + */ + void set(String key, String value, Expiration expiration, SetOption option); + Boolean setNX(String key, String value); void setEx(String key, long seconds, String value); 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 b53c32cf3..11aab7af5 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 @@ -28,6 +28,7 @@ import java.util.Map.Entry; import java.util.Properties; import java.util.Random; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.beans.DirectFieldAccessor; import org.springframework.dao.DataAccessException; @@ -61,10 +62,11 @@ 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.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; +import org.springframework.util.ObjectUtils; import redis.clients.jedis.BinaryJedisPubSub; import redis.clients.jedis.Jedis; @@ -249,7 +251,8 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public byte[] randomKey() { - List nodes = new ArrayList(topologyProvider.getTopology().getActiveMasterNodes()); + List nodes = new ArrayList(topologyProvider.getTopology() + .getActiveMasterNodes()); Set inspectedNodes = new HashSet(nodes.size()); do { @@ -594,6 +597,49 @@ public class JedisClusterConnection implements RedisClusterConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOptions) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + if (expiration == null || expiration.isPersitent()) { + + if (option == null || ObjectUtils.nullSafeEquals(SetOption.UPSERT, option)) { + set(key, value); + } else { + + // BinaryCluster does not support set with nxxx and binary key/value pairs. + if (ObjectUtils.nullSafeEquals(SetOption.SET_IF_PRESENT, option)) { + throw new UnsupportedOperationException("Jedis does not support SET XX without PX or EX on BinaryCluster."); + } + + setNX(key, value); + } + } else { + + if (option == null || ObjectUtils.nullSafeEquals(SetOption.UPSERT, option)) { + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + pSetEx(key, expiration.getExpirationTime(), value); + } else { + setEx(key, expiration.getExpirationTime(), value); + } + } else { + + byte[] nxxx = JedisConverters.toSetCommandNxXxArgument(option); + byte[] expx = JedisConverters.toSetCommandExPxArgument(expiration); + + try { + cluster.set(key, value, nxxx, expx, expiration.getExpirationTime()); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + } + } + /* * (non-Javadoc) * @see org.springframework.data.redis.connection.RedisStringCommands#setNX(byte[], byte[]) @@ -3432,7 +3478,8 @@ public class JedisClusterConnection implements RedisClusterConnection { @Override public void clusterForget(final RedisClusterNode node) { - Set nodes = new LinkedHashSet(topologyProvider.getTopology().getActiveMasterNodes()); + Set nodes = new LinkedHashSet(topologyProvider.getTopology() + .getActiveMasterNodes()); final RedisClusterNode nodeToRemove = topologyProvider.getTopology().lookup(node); nodes.remove(nodeToRemove); @@ -3536,8 +3583,7 @@ public class JedisClusterConnection implements RedisClusterConnection { Assert.notNull(master, "Master cannot be null!"); - final RedisClusterNode nodeToUse = topologyProvider.getTopology() - .lookup(master); + final RedisClusterNode nodeToUse = topologyProvider.getTopology().lookup(master); return JedisConverters.toSetOfRedisClusterNodes(clusterCommandExecutor.executeCommandOnSingleNode( new JedisClusterCommandCallback>() { 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 c199294ae..f9d8f83ea 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 @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,6 +54,7 @@ import org.springframework.data.redis.core.KeyBoundCursor; import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -1139,6 +1140,87 @@ public class JedisConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOption) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + if (expiration == null || expiration.isPersitent()) { + + if (option == null || ObjectUtils.nullSafeEquals(SetOption.UPSERT, option)) { + set(key, value); + } else { + + try { + + byte[] nxxx = JedisConverters.toSetCommandNxXxArgument(option); + + if (isPipelined()) { + + pipeline(new JedisStatusResult(pipeline.set(key, value, nxxx))); + return; + } + if (isQueueing()) { + + transaction(new JedisStatusResult(transaction.set(key, value, nxxx))); + return; + } + + jedis.set(key, value, nxxx); + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + + } else { + + if (option == null || ObjectUtils.nullSafeEquals(SetOption.UPSERT, option)) { + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, expiration.getTimeUnit())) { + pSetEx(key, expiration.getExpirationTime(), value); + } else { + setEx(key, expiration.getExpirationTime(), value); + } + } else { + + byte[] nxxx = JedisConverters.toSetCommandNxXxArgument(option); + byte[] expx = JedisConverters.toSetCommandExPxArgument(expiration); + + try { + if (isPipelined()) { + + if (expiration.getExpirationTime() > Integer.MAX_VALUE) { + + throw new IllegalArgumentException( + "Expiration.exprirationTime must be less than equals Integer.MAX_VALUE for pipeline in Jedis."); + } + + pipeline(new JedisStatusResult(pipeline.set(key, value, nxxx, expx, (int) expiration.getExpirationTime()))); + return; + } + if (isQueueing()) { + + if (expiration.getExpirationTime() > Integer.MAX_VALUE) { + throw new IllegalArgumentException( + "Expiration.exprirationTime must be less than equals Integer.MAX_VALUE for transactions in Jedis."); + } + + transaction(new JedisStatusResult(transaction.set(key, value, nxxx, expx, + (int) expiration.getExpirationTime()))); + return; + } + + jedis.set(key, value, nxxx, expx, expiration.getExpirationTime()); + + } catch (Exception ex) { + throw convertJedisAccessException(ex); + } + } + } + } + public byte[] getSet(byte[] key, byte[] value) { try { if (isPipelined()) { diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java index e4a6c4101..b83b5da2c 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConverters.java @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,11 +17,11 @@ package org.springframework.data.redis.connection.jedis; import java.nio.ByteBuffer; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; @@ -29,7 +29,9 @@ import org.springframework.data.redis.connection.DefaultTuple; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisServer; +import org.springframework.data.redis.connection.RedisStringCommands; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.SortParameters; @@ -40,9 +42,11 @@ import org.springframework.data.redis.connection.convert.ListConverter; import org.springframework.data.redis.connection.convert.MapConverter; import org.springframework.data.redis.connection.convert.SetConverter; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; import redis.clients.jedis.BinaryClient.LIST_POSITION; @@ -71,11 +75,17 @@ abstract public class JedisConverters extends Converters { private static final Converter TUPLE_CONVERTER; private static final ListConverter TUPLE_LIST_TO_TUPLE_LIST_CONVERTER; private static final Converter OBJECT_TO_CLUSTER_NODE_CONVERTER; + private static final Converter EXPIRATION_TO_COMMAND_OPTION_CONVERTER; + private static final Converter SET_OPTION_TO_COMMAND_OPTION_CONVERTER; public static final byte[] PLUS_BYTES; public static final byte[] MINUS_BYTES; public static final byte[] POSITIVE_INFINITY_BYTES; public static final byte[] NEGATIVE_INFINITY_BYTES; + private static final byte[] EX; + private static final byte[] PX; + private static final byte[] NX; + private static final byte[] XX; static { @@ -113,6 +123,46 @@ abstract public class JedisConverters extends Converters { ((Number) nodeInfo.get(1)).intValue(), range); } }; + + EX = toBytes("EX"); + PX = toBytes("PX"); + EXPIRATION_TO_COMMAND_OPTION_CONVERTER = new Converter() { + + @Override + public byte[] convert(Expiration source) { + + if (source == null || source.isPersitent()) { + return new byte[0]; + } + + if (ObjectUtils.nullSafeEquals(TimeUnit.MILLISECONDS, source.getTimeUnit())) { + return PX; + } + + return EX; + } + }; + + NX = toBytes("NX"); + XX = toBytes("XX"); + SET_OPTION_TO_COMMAND_OPTION_CONVERTER = new Converter() { + + @Override + public byte[] convert(SetOption source) { + + switch (source) { + case UPSERT: + return new byte[0]; + case SET_IF_ABSENT: + return NX; + case SET_IF_PRESENT: + return XX; + } + + throw new IllegalArgumentException(String.format("Invalid argument %s for SetOption.", source)); + } + + }; } public static Converter stringToBytes() { @@ -322,6 +372,42 @@ abstract public class JedisConverters extends Converters { return boundaryToBytes(boundary, toBytes("["), toBytes("(")); } + /** + * Converts a given {@link Expiration} to the according {@code SET} command argument.
+ *

+ *
{@link TimeUnit#SECONDS}
+ *
{@code EX}
+ *
{@link TimeUnit#MILLISECONDS}
+ *
{@code PX}
+ *
+ * + * @param expiration + * @return + * @since 1.7 + */ + public static byte[] toSetCommandExPxArgument(Expiration expiration) { + return EXPIRATION_TO_COMMAND_OPTION_CONVERTER.convert(expiration); + } + + /** + * Converts a given {@link SetOption} to the according {@code SET} command argument.
+ *
+ *
{@link SetOption#UPSERT}
+ *
{@code byte[0]}
+ *
{@link SetOption#SET_IF_ABSENT}
+ *
{@code NX}
+ *
{@link SetOption#SET_IF_PRESENT}
+ *
{@code XX}
+ *
+ * + * @param option + * @return + * @since 1.7 + */ + public static byte[] toSetCommandNxXxArgument(SetOption option) { + return SET_OPTION_TO_COMMAND_OPTION_CONVERTER.convert(option); + } + private static byte[] boundaryToBytes(Boundary boundary, byte[] inclPrefix, byte[] exclPrefix) { byte[] prefix = boundary.isIncluding() ? inclPrefix : exclPrefix; 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 39ade9b97..9f44e22fb 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 @@ -47,6 +47,7 @@ import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -470,6 +471,16 @@ public class JredisConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOption) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + throw new UnsupportedOperationException( + "SET with options is not supported for JRedis. Please use SETNX, SETEX, PSETEX."); + } + public byte[] getSet(byte[] key, byte[] value) { try { return jredis.getset(key, value); 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 d3547b04a..d7b312ca2 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 @@ -62,6 +62,7 @@ import org.springframework.data.redis.core.RedisCommand; import org.springframework.data.redis.core.ScanCursor; import org.springframework.data.redis.core.ScanIteration; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -1218,6 +1219,30 @@ public class LettuceConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOption) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + + try { + if (isPipelined()) { + pipeline(new LettuceStatusResult(getAsyncConnection().set(key, value, + LettuceConverters.toSetArgs(expiration, option)))); + return; + } + if (isQueueing()) { + transaction(new LettuceTxStatusResult(getConnection().set(key, value, + LettuceConverters.toSetArgs(expiration, option)))); + return; + } + getConnection().set(key, value, LettuceConverters.toSetArgs(expiration, option)); + } catch (Exception ex) { + throw convertLettuceAccessException(ex); + } + } + public byte[] getSet(byte[] key, byte[] value) { try { if (isPipelined()) { 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 06d641e17..cabc0ed4a 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 @@ -1,5 +1,5 @@ /* - * Copyright 2013-2015 the original author or authors. + * Copyright 2013-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.TimeUnit; import org.springframework.core.convert.converter.Converter; import org.springframework.dao.DataAccessException; @@ -39,6 +40,7 @@ import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisNode.NodeType; import org.springframework.data.redis.connection.RedisSentinelConfiguration; import org.springframework.data.redis.connection.RedisServer; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.ReturnType; @@ -47,6 +49,7 @@ import org.springframework.data.redis.connection.SortParameters.Order; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.convert.LongToBooleanConverter; import org.springframework.data.redis.connection.convert.StringToRedisClientInfoConverter; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -60,6 +63,7 @@ import com.lambdaworks.redis.SortArgs; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag; import com.lambdaworks.redis.protocol.LettuceCharsets; +import com.lambdaworks.redis.protocol.SetArgs; /** * Lettuce type converters @@ -614,4 +618,43 @@ abstract public class LettuceConverters extends Converters { return CLUSTER_NODE_TO_CLUSTER_NODE_CONVERTER.convert(source); } + /** + * Converts a given {@link Expiration} and {@link SetOption} to the according {@link SetArgs}.
+ * + * @param expiration can be {@literal null}. + * @param option can be {@literal null}. + * @since 1.7 + */ + public static SetArgs toSetArgs(Expiration expiration, SetOption option) { + + SetArgs args = new SetArgs(); + if (expiration != null && !expiration.isPersitent()) { + + switch (expiration.getTimeUnit()) { + case SECONDS: + args.ex(expiration.getExpirationTime()); + break; + default: + args.px(expiration.getConverted(TimeUnit.MILLISECONDS)); + break; + } + } + + if (option != null) { + + switch (option) { + case SET_IF_ABSENT: + args.nx(); + break; + case SET_IF_PRESENT: + args.xx(); + break; + default: + break; + } + } + + return args; + } + } 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 8c592bf8c..acbc966ec 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 @@ -46,6 +46,7 @@ import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.Subscription; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.Assert; @@ -875,6 +876,16 @@ public class SrpConnection extends AbstractRedisConnection { } } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisStringCommands#set(byte[], byte[], org.springframework.data.redis.core.types.Expiration, org.springframework.data.redis.connection.RedisStringCommands.SetOption) + */ + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption option) { + throw new UnsupportedOperationException( + "SET with options is not supported for Srp. Please use SETNX, SETEX, PSETEX."); + } + public byte[] getSet(byte[] key, byte[] value) { try { if (isPipelined()) { diff --git a/src/main/java/org/springframework/data/redis/core/types/Expiration.java b/src/main/java/org/springframework/data/redis/core/types/Expiration.java new file mode 100644 index 000000000..cad5606b2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/types/Expiration.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.core.types; + +import java.util.concurrent.TimeUnit; + +import org.springframework.util.Assert; + +/** + * Expiration holds a value with its associated {@link TimeUnit}. + * + * @author Christoph Strobl + * @since 1.7 + */ +public class Expiration { + + private long expirationTime; + private TimeUnit timeUnit; + + /** + * Creates new {@link Expiration}. + * + * @param expirationTime can be {@literal null}. Defaulted to {@link TimeUnit#SECONDS} + * @param timeUnit + */ + protected Expiration(long expirationTime, TimeUnit timeUnit) { + + this.expirationTime = expirationTime; + this.timeUnit = timeUnit != null ? timeUnit : TimeUnit.SECONDS; + } + + /** + * Get the expiration time converted into {@link TimeUnit#MILLISECONDS}. + * + * @return + */ + public long getExpirationTimeInMilliseconds() { + return getConverted(TimeUnit.MILLISECONDS); + } + + /** + * Get the expiration time converted into {@link TimeUnit#SECONDS}. + * + * @return + */ + public long getExpirationTimeInSeconds() { + return getConverted(TimeUnit.SECONDS); + } + + /** + * Get the expiration time. + * + * @return + */ + public long getExpirationTime() { + return expirationTime; + } + + /** + * Get the time unit for the expiration time. + * + * @return + */ + public TimeUnit getTimeUnit() { + return this.timeUnit; + } + + /** + * Get the expiration time converted into the desired {@code targetTimeUnit}. + * + * @param targetTimeUnit must not {@literal null}. + * @return + * @throws IllegalArgumentException + */ + public long getConverted(TimeUnit targetTimeUnit) { + + Assert.notNull(targetTimeUnit, "TargetTimeUnit must not be null!"); + return targetTimeUnit.convert(expirationTime, timeUnit); + } + + /** + * Creates new {@link Expiration} with {@link TimeUnit#SECONDS}. + * + * @param expirationTime + * @return + */ + public static Expiration seconds(long expirationTime) { + return new Expiration(expirationTime, TimeUnit.SECONDS); + } + + /** + * Creates new {@link Expiration} with {@link TimeUnit#MILLISECONDS}. + * + * @param expirationTime + * @return + */ + public static Expiration milliseconds(long expirationTime) { + return new Expiration(expirationTime, TimeUnit.MILLISECONDS); + } + + /** + * Creates new persistent {@link Expiration}. + * + * @return + */ + public static Expiration persistent() { + return new Expiration(-1, TimeUnit.SECONDS); + } + + /** + * @return {@literal true} if {@link Expiration} is set to persistent. + */ + public boolean isPersitent() { + return expirationTime == -1; + } +} 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 4bf7dca8d..2d72eebab 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractConnectionIntegrationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2015 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.IsCloseTo.*; import static org.junit.Assert.*; import static org.junit.Assume.*; import static org.springframework.data.redis.SpinBarrier.*; @@ -52,6 +53,7 @@ import org.springframework.data.redis.RedisVersionUtils; import org.springframework.data.redis.TestCondition; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; @@ -60,6 +62,7 @@ import org.springframework.data.redis.connection.StringRedisConnection.StringTup import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @@ -2220,6 +2223,282 @@ public abstract class AbstractConnectionIntegrationTests { assertThat(values, not(hasItems("a", "b", "c", "d"))); } + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndNullOpionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "foo", Expiration.milliseconds(500), null); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(500d, 499d))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndUpsertOpionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "foo", Expiration.milliseconds(500), SetOption.upsert()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(500d, 499d))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndUpsertOpionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.milliseconds(500), SetOption.upsert()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(500d, 499d))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndAbsentOptionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.milliseconds(500), SetOption.ifAbsent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + assertThat(((String) result.get(2)), is(equalTo("spring"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndAbsentOptionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "data", Expiration.milliseconds(500), SetOption.ifAbsent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(500d, 499d))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndPresentOptionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.milliseconds(500), SetOption.ifPresent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(500, 499))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithExpirationAndPresentOptionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "data", Expiration.milliseconds(500), SetOption.ifPresent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.FALSE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-2, 0))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithNullExpirationAndUpsertOpionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "foo", null, SetOption.upsert()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndUpsertOpionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "foo", Expiration.persistent(), SetOption.upsert()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndUpsertOpionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.persistent(), SetOption.upsert()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndAbsentOptionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.persistent(), SetOption.ifAbsent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + assertThat(((String) result.get(2)), is(equalTo("spring"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndAbsentOptionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "data", Expiration.persistent(), SetOption.ifAbsent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndPresentOptionShouldSetTtlWhenKeyDoesExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "spring"); + connection.set(key, "data", Expiration.persistent(), SetOption.ifPresent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.TRUE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-1, 0))); + assertThat(((String) result.get(2)), is(equalTo("data"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + @WithRedisDriver({ RedisDriver.JEDIS, RedisDriver.LETTUCE }) + public void setWithoutExpirationAndPresentOptionShouldSetTtlWhenKeyDoesNotExist() { + + String key = "exp-" + UUID.randomUUID(); + connection.set(key, "data", Expiration.persistent(), SetOption.ifPresent()); + + actual.add(connection.exists(key)); + actual.add(connection.pTtl(key)); + actual.add(connection.get(key)); + + List result = getResults(); + assertThat((Boolean) result.get(0), is(Boolean.FALSE)); + assertThat(((Long) result.get(1)).doubleValue(), is(closeTo(-2, 0))); + } + 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 fef7bfb58..87dafe868 100644 --- a/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java +++ b/src/test/java/org/springframework/data/redis/connection/ClusterConnectionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,6 +15,9 @@ */ package org.springframework.data.redis.connection; +/** + * @author Christoph Strobl + */ public interface ClusterConnectionTests { /** @@ -893,4 +896,44 @@ public interface ClusterConnectionTests { */ void clusterGetMasterSlaveMapShouldListMastersAndSlavesCorrectly(); + /** + * @see DATAREDIS-316 + */ + void setWithExpirationInSecondsShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithExpirationInMillisecondsShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithOptionIfPresentShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithOptionIfAbsentShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithExpirationAndIfAbsentShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithExpirationAndIfAbsentShouldNotBeAppliedWhenKeyExists(); + + /** + * @see DATAREDIS-316 + */ + void setWithExpirationAndIfPresentShouldWorkCorrectly(); + + /** + * @see DATAREDIS-316 + */ + void setWithExpirationAndIfPresentShouldNotBeAppliedWhenKeyDoesNotExists(); + } diff --git a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java index 7442f3628..cb2d660c6 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisConnectionUnitTests.java @@ -30,6 +30,7 @@ import org.springframework.dao.DataAccessException; import org.springframework.data.redis.connection.RedisNode.RedisNodeBuilder; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import org.springframework.util.ObjectUtils; @@ -891,5 +892,10 @@ public class RedisConnectionUnitTests { public void migrate(byte[] key, RedisNode target, int dbIndex, MigrateOption option, long timeout) { delegate.migrate(key, target, dbIndex, option, timeout); } + + @Override + public void set(byte[] key, byte[] value, Expiration expiration, SetOption options) { + delegate.set(key, value, expiration, options); + } } } 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 cef0fe947..13a667ceb 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection.jedis; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.IsCloseTo.*; import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; @@ -46,10 +47,12 @@ import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.test.util.RedisClusterRule; import redis.clients.jedis.HostAndPort; @@ -2308,4 +2311,103 @@ public class JedisClusterConnectionTests implements ClusterConnectionTests { assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)).isEmpty(), is(true)); assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_3_PORT)).isEmpty(), is(true)); } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationInSecondsShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.upsert()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationInMillisecondsShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.milliseconds(500), SetOption.upsert()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.pttl(KEY_1).doubleValue(), is(closeTo(500d, 499d))); + } + + /** + * @see DATAREDIS-316 + */ + @Test(expected = UnsupportedOperationException.class) + public void setWithOptionIfPresentShouldWorkCorrectly() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.persistent(), SetOption.ifPresent()); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithOptionIfAbsentShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.persistent(), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(-1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfAbsentShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfAbsentShouldNotBeAppliedWhenKeyExists() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.seconds(1), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(-1L)); + assertThat(nativeConnection.get(KEY_1_BYTES), is(equalTo(VALUE_1_BYTES))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfPresentShouldWorkCorrectly() { + + nativeConnection.set(KEY_1_BYTES, VALUE_1_BYTES); + + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.seconds(1), SetOption.ifPresent()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(true)); + assertThat(nativeConnection.ttl(KEY_1_BYTES), is(1L)); + assertThat(nativeConnection.get(KEY_1_BYTES), is(equalTo(VALUE_2_BYTES))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfPresentShouldNotBeAppliedWhenKeyDoesNotExists() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.ifPresent()); + + assertThat(nativeConnection.exists(KEY_1_BYTES), is(false)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java index 9680a302c..1f4a68f66 100644 --- a/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/jedis/JedisConvertersUnitTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2015 the original author or authors. + * Copyright 2014-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,9 @@ import java.util.Map; import org.junit.Test; import org.springframework.data.redis.connection.RedisServer; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; /** @@ -257,6 +259,56 @@ public class JedisConvertersUnitTests { JedisConverters.boundaryToBytesForZRange(Range.range().gt(new Date()).getMin(), null); } + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandExPxOptionShouldReturnEXforSeconds() { + assertThat(JedisConverters.toSetCommandExPxArgument(Expiration.seconds(100)), equalTo(JedisConverters.toBytes("EX"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandExPxOptionShouldReturnEXforMilliseconds() { + + assertThat(JedisConverters.toSetCommandExPxArgument(Expiration.milliseconds(100)), + equalTo(JedisConverters.toBytes("PX"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandExPxOptionShouldReturnEmptyArrayForNull() { + assertThat(JedisConverters.toSetCommandExPxArgument(null), equalTo(new byte[] {})); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandNxXxOptionShouldReturnNXforAbsent() { + assertThat(JedisConverters.toSetCommandNxXxArgument(SetOption.ifAbsent()), equalTo(JedisConverters.toBytes("NX"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandNxXxOptionShouldReturnXXforAbsent() { + assertThat(JedisConverters.toSetCommandNxXxArgument(SetOption.ifPresent()), equalTo(JedisConverters.toBytes("XX"))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetCommandNxXxOptionShouldReturnEmptyArrayforUpsert() { + assertThat(JedisConverters.toSetCommandNxXxArgument(SetOption.upsert()), equalTo(new byte[] {})); + } + private void verifyRedisServerInfo(RedisServer server, Map values) { for (Map.Entry entry : values.entrySet()) { 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 9e7071649..8bc60a2f3 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 @@ -1,5 +1,5 @@ /* - * Copyright 2015 the original author or authors. + * Copyright 2015-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection.lettuce; import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.number.IsCloseTo.*; import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; @@ -44,11 +45,13 @@ import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisListCommands.Position; import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; import org.springframework.data.redis.connection.RedisZSetCommands.Range; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; import org.springframework.data.redis.connection.jedis.JedisConverters; import org.springframework.data.redis.core.Cursor; import org.springframework.data.redis.core.ScanOptions; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.test.util.RedisClusterRule; import com.lambdaworks.redis.RedisURI.Builder; @@ -2298,4 +2301,105 @@ public class LettuceClusterConnectionTests implements ClusterConnectionTests { assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_2_PORT)).isEmpty(), is(true)); assertThat(masterSlaveMap.get(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_3_PORT)).isEmpty(), is(true)); } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationInSecondsShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.upsert()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.ttl(KEY_1), is(1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationInMillisecondsShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.milliseconds(500), SetOption.upsert()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.pttl(KEY_1).doubleValue(), is(closeTo(500d, 499d))); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithOptionIfPresentShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.persistent(), SetOption.ifPresent()); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithOptionIfAbsentShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.persistent(), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.ttl(KEY_1), is(-1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfAbsentShouldWorkCorrectly() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.ttl(KEY_1), is(1L)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfAbsentShouldNotBeAppliedWhenKeyExists() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.seconds(1), SetOption.ifAbsent()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.ttl(KEY_1), is(-1L)); + assertThat(nativeConnection.get(KEY_1), is(equalTo(VALUE_1))); + + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfPresentShouldWorkCorrectly() { + + nativeConnection.set(KEY_1, VALUE_1); + + clusterConnection.set(KEY_1_BYTES, VALUE_2_BYTES, Expiration.seconds(1), SetOption.ifPresent()); + + assertThat(nativeConnection.exists(KEY_1), is(true)); + assertThat(nativeConnection.ttl(KEY_1), is(1L)); + assertThat(nativeConnection.get(KEY_1), is(equalTo(VALUE_2))); + + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void setWithExpirationAndIfPresentShouldNotBeAppliedWhenKeyDoesNotExists() { + + clusterConnection.set(KEY_1_BYTES, VALUE_1_BYTES, Expiration.seconds(1), SetOption.ifPresent()); + + assertThat(nativeConnection.exists(KEY_1), is(false)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java index 134a5abd5..b4f1b3e2a 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConvertersUnitTests.java @@ -21,6 +21,7 @@ import static org.hamcrest.core.IsEqual.*; import static org.hamcrest.core.IsNull.*; import static org.junit.Assert.*; import static org.springframework.data.redis.connection.ClusterTestVariables.*; +import static org.springframework.test.util.ReflectionTestUtils.*; import java.util.Arrays; import java.util.Collections; @@ -31,11 +32,14 @@ import org.junit.Test; import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.data.redis.core.types.Expiration; import org.springframework.data.redis.core.types.RedisClientInfo; import com.lambdaworks.redis.RedisURI; import com.lambdaworks.redis.cluster.models.partitions.Partitions; import com.lambdaworks.redis.cluster.models.partitions.RedisClusterNode.NodeFlag; +import com.lambdaworks.redis.protocol.SetArgs; /** * @author Christoph Strobl @@ -111,4 +115,102 @@ public class LettuceConvertersUnitTests { assertThat(node.getLinkState(), is(LinkState.CONNECTED)); assertThat(node.getSlotRange().getSlots(), hasItems(1, 2, 3, 4, 5)); } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldReturnEmptyArgsForNullValues() { + + SetArgs args = LettuceConverters.toSetArgs(null, null); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldNotSetExOrPxForPersistent() { + + SetArgs args = LettuceConverters.toSetArgs(Expiration.persistent(), null); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldSetExForSeconds() { + + SetArgs args = LettuceConverters.toSetArgs(Expiration.seconds(10), null); + + assertThat((Long) getField(args, "ex"), is(10L)); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldSetPxForMilliseconds() { + + SetArgs args = LettuceConverters.toSetArgs(Expiration.milliseconds(100), null); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat((Long) getField(args, "px"), is(100L)); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldSetNxForAbsent() { + + SetArgs args = LettuceConverters.toSetArgs(null, SetOption.ifAbsent()); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.TRUE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldSetXxForPresent() { + + SetArgs args = LettuceConverters.toSetArgs(null, SetOption.ifPresent()); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.TRUE)); + } + + /** + * @see DATAREDIS-316 + */ + @Test + public void toSetArgsShouldNotSetNxOrXxForUpsert() { + + SetArgs args = LettuceConverters.toSetArgs(null, SetOption.upsert()); + + assertThat(getField(args, "ex"), is(nullValue())); + assertThat(getField(args, "px"), is(nullValue())); + assertThat((Boolean) getField(args, "nx"), is(Boolean.FALSE)); + assertThat((Boolean) getField(args, "xx"), is(Boolean.FALSE)); + } }