diff --git a/pom.xml b/pom.xml index 0e21a78f6..9c58e2267 100644 --- a/pom.xml +++ b/pom.xml @@ -1,5 +1,6 @@ - + 4.0.0 @@ -86,11 +87,11 @@ - io.projectreactor + io.projectreactor reactor-core ${reactor} true - + @@ -188,6 +189,13 @@ ${multithreadedtc} test + + + io.projectreactor.addons + reactor-test + ${reactor} + test + diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java index 0f4018932..0c325160d 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java @@ -417,7 +417,7 @@ public interface ReactiveGeoCommands { Assert.notNull(member, "Member must not be null!"); return geoHash(key, Collections.singletonList(member)) // - .map(vals -> vals.isEmpty() ? null : vals.iterator().next()); + .then(vals -> vals.isEmpty() ? Mono.empty() : Mono.justOrEmpty(vals.iterator().next())); } /** @@ -523,7 +523,8 @@ public interface ReactiveGeoCommands { Assert.notNull(member, "Member must not be null!"); - return geoPos(key, Collections.singletonList(member)).map(vals -> vals.isEmpty() ? null : vals.iterator().next()); + return geoPos(key, Collections.singletonList(member)) + .then(vals -> vals.isEmpty() ? Mono.empty() : Mono.justOrEmpty(vals.iterator().next())); } /** diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java index 47985840e..ab6bf52c2 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -19,6 +19,8 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; import java.util.List; import org.reactivestreams.Publisher; @@ -259,4 +261,353 @@ public interface ReactiveKeyCommands { * @see Redis Documentation: DEL */ Flux, Long>> mDel(Publisher> keys); + + /** + * {@code EXPIRE}/{@code PEXPIRE} command parameters. + * + * @author Mark Paluch + * @see Redis Documentation: EXPIRE + * @see Redis Documentation: PEXPIRE + */ + class ExpireCommand extends KeyCommand { + + private Duration timeout; + + private ExpireCommand(ByteBuffer key, Duration timeout) { + + super(key); + + this.timeout = timeout; + } + + /** + * Creates a new {@link ExpireCommand} given a {@link ByteBuffer key}. + * + * @param key must not be {@literal null}. + * @return a new {@link ExpireCommand} for {@link ByteBuffer key}. + */ + public static ExpireCommand key(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new ExpireCommand(key, null); + } + + /** + * Applies the {@literal timeout}. Constructs a new command instance with all previously configured properties. + * + * @param timeout must not be {@literal null}. + * @return a new {@link ExpireCommand} with {@literal timeout} applied. + */ + public ExpireCommand timeout(Duration timeout) { + + Assert.notNull(timeout, "Timeout must not be null!"); + + return new ExpireCommand(getKey(), timeout); + } + + /** + * @return + */ + public Duration getTimeout() { + return timeout; + } + } + + /** + * Set time to live for given {@code key} in seconds. + * + * @param key must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return + * @see Redis Documentation: EXPIRE + */ + default Mono expire(ByteBuffer key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + + return expire(Mono.just(new ExpireCommand(key, timeout))).next().map(BooleanResponse::getOutput); + } + + /** + * Expire {@literal keys} one by one. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} removed along with the expiration + * result. + * @see Redis Documentation: EXPIRE + */ + Flux> expire(Publisher commands); + + /** + * Set time to live for given {@code key} in milliseconds. + * + * @param key must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return + * @see Redis Documentation: PEXPIRE + */ + default Mono pExpire(ByteBuffer key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + + return expire(Mono.just(new ExpireCommand(key, timeout))).next().map(BooleanResponse::getOutput); + } + + /** + * Expire {@literal keys} one by one. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} removed along with the expiration + * result. + * @see Redis Documentation: PEXPIRE + */ + Flux> pExpire(Publisher commands); + + /** + * {@code EXPIREAT}/{@code PEXPIREAT} command parameters. + * + * @author Mark Paluch + * @see Redis Documentation: EXPIREAT + * @see Redis Documentation: PEXPIREAT + */ + class ExpireAtCommand extends KeyCommand { + + private Instant expireAt; + + private ExpireAtCommand(ByteBuffer key, Instant expireAt) { + + super(key); + + this.expireAt = expireAt; + } + + /** + * Creates a new {@link ExpireAtCommand} given a {@link ByteBuffer key}. + * + * @param key must not be {@literal null}. + * @return a new {@link ExpireCommand} for {@link ByteBuffer key}. + */ + public static ExpireAtCommand key(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new ExpireAtCommand(key, null); + } + + /** + * Applies the {@literal expireAt}. Constructs a new command instance with all previously configured properties. + * + * @param expireAt must not be {@literal null}. + * @return a new {@link ExpireAtCommand} with {@literal expireAt} applied. + */ + public ExpireAtCommand timeout(Instant expireAt) { + + Assert.notNull(expireAt, "Expire at must not be null!"); + + return new ExpireAtCommand(getKey(), expireAt); + } + + /** + * @return + */ + public Instant getExpireAt() { + return expireAt; + } + } + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return + * @see Redis Documentation: EXPIREAT + */ + default Mono expireAt(ByteBuffer key, Instant expireAt) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expireAt, "Expire at must not be null!"); + + return expireAt(Mono.just(new ExpireAtCommand(key, expireAt))).next().map(BooleanResponse::getOutput); + } + + /** + * Set one-by-one the expiration for given {@code key} as a {@literal UNIX} timestamp. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} removed along with the expiration + * result. + * @see Redis Documentation: EXPIREAT + */ + Flux> expireAt(Publisher commands); + + /** + * Set the expiration for given {@code key} as a {@literal UNIX} timestamp. + * + * @param key must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return + * @see Redis Documentation: PEXPIREAT + */ + default Mono pExpireAt(ByteBuffer key, Instant expireAt) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expireAt, "Expire at must not be null!"); + + return pExpireAt(Mono.just(new ExpireAtCommand(key, expireAt))).next().map(BooleanResponse::getOutput); + } + + /** + * Set one-by-one the expiration for given {@code key} as a {@literal UNIX} timestamp in milliseconds. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} removed along with the expiration + * result. + * @see Redis Documentation: PEXPIREAT + */ + Flux> pExpireAt(Publisher commands); + + /** + * Remove the expiration from given {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: PERSIST + */ + default Mono persist(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return persist(Mono.just(new KeyCommand(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Remove one-by-one the expiration from given {@code key}. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} persisted along with the persist result. + * @see Redis Documentation: PERSIST + */ + Flux> persist(Publisher commands); + + /** + * Get the time to live for {@code key} in seconds. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: TTL + */ + default Mono ttl(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return ttl(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get one-by-one the time to live for keys. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link NumericResponse} holding the {@literal key} along with the time to live result. + * @see Redis Documentation: TTL + */ + Flux> ttl(Publisher commands); + + /** + * Get the time to live for {@code key} in milliseconds. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: TTL + */ + default Mono pTtl(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return pTtl(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get one-by-one the time to live for keys. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link NumericResponse} holding the {@literal key} along with the time to live result. + * @see Redis Documentation: PTTL + */ + Flux> pTtl(Publisher commands); + + /** + * {@code MOVE} command parameters. + * + * @author Mark Paluch + * @see Redis Documentation: MOVE + */ + class MoveCommand extends KeyCommand { + + private Integer database; + + private MoveCommand(ByteBuffer key, Integer database) { + + super(key); + + this.database = database; + } + + /** + * Creates a new {@link MoveCommand} given a {@link ByteBuffer key}. + * + * @param key must not be {@literal null}. + * @return a new {@link ExpireCommand} for {@link ByteBuffer key}. + */ + public static MoveCommand key(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return new MoveCommand(key, null); + } + + /** + * Applies the {@literal database} index. Constructs a new command instance with all previously configured + * properties. + * + * @param database + * @return a new {@link MoveCommand} with {@literal database} applied. + */ + public MoveCommand timeout(int database) { + return new MoveCommand(getKey(), database); + } + + /** + * @return + */ + public Integer getDatabase() { + return database; + } + } + + /** + * Move given {@code key} to database with {@code index}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: MOVE + */ + default Mono move(ByteBuffer key, int database) { + + Assert.notNull(key, "Key must not be null!"); + + return move(Mono.just(new MoveCommand(key, database))).next().map(BooleanResponse::getOutput); + } + + /** + * Move keys one-by-one between databases. + * + * @param commands must not be {@literal null}. + * @return {@link Flux} of {@link BooleanResponse} holding the {@literal key} to move along with the move result. + * @see Redis Documentation: MOVE + */ + Flux> move(Publisher commands); } diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java index 620209353..ac596c7ee 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java @@ -16,6 +16,7 @@ package org.springframework.data.redis.connection; import java.io.Closeable; +import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; @@ -47,6 +48,9 @@ import lombok.Data; */ public interface ReactiveRedisConnection extends Closeable { + @Override + void close(); + /** * Get {@link ReactiveKeyCommands}. * diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java new file mode 100644 index 000000000..a4463d58e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java @@ -0,0 +1,44 @@ +/* + * 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; + +import org.springframework.dao.support.PersistenceExceptionTranslator; + +/** + * Thread-safe factory of reactive Redis connections. + * + * @author Mark Paluch + * @since 2.0 + * @see reactor.core.publisher.Mono + * @see reactor.core.publisher.Flux + * @see ReactiveRedisConnection + * @see ReactiveRedisClusterConnection + */ +public interface ReactiveRedisConnectionFactory extends PersistenceExceptionTranslator { + + /** + * @return a reactive Redis connection. + * @since 2.0. + */ + ReactiveRedisConnection getReactiveConnection(); + + /** + * @return a reactive Redis Cluster connection. + * @since 2.0 + */ + ReactiveRedisClusterConnection getReactiveClusterConnection(); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java index 11c0adbd1..8b350f95a 100644 --- a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java @@ -89,7 +89,7 @@ public interface ReactiveZSetCommands { * @param tuples must not be {@literal null}. * @return a new {@link ZAddCommand} for {@link Tuple}. */ - public static ZAddCommand tuples(Collection tuples) { + public static ZAddCommand tuples(Collection tuples) { Assert.notNull(tuples, "Tuples must not be null!"); @@ -197,6 +197,22 @@ public interface ReactiveZSetCommands { .map(resp -> resp.getOutput().longValue()); } + /** + * Add a {@literal tuples} to a sorted set at {@literal key}, or update their score if it already exists. + * + * @param key must not be {@literal null}. + * @param tuples must not be {@literal null}. + * @return + * @see Redis Documentation: ZADD + */ + default Mono zAdd(ByteBuffer key, Collection tuples) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(tuples, "Tuples must not be null!"); + + return zAdd(Mono.just(ZAddCommand.tuples(tuples).to(key))).next().map(resp -> resp.getOutput().longValue()); + } + /** * Add {@link ZAddCommand#getTuples()} to a sorted set at {@link ZAddCommand#getKey()}, or update its {@literal score} * if it already exists. diff --git a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java index b51acf276..75c961346 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -20,7 +20,7 @@ import org.springframework.dao.support.PersistenceExceptionTranslator; /** * Thread-safe factory of Redis connections. - * + * * @author Costin Leau * @author Christoph Strobl */ @@ -28,45 +28,32 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { /** * Provides a suitable connection for interacting with Redis. - * + * * @return connection for interacting with Redis. */ RedisConnection getConnection(); /** * Provides a suitable connection for interacting with Redis Cluster. - * + * * @return * @since 1.7 */ RedisClusterConnection getClusterConnection(); - /** - * @return - * @since 2.0. - */ - ReactiveRedisConnection getReactiveConnection(); - - /** - * - * @return - * @since 2.0 - */ - ReactiveRedisClusterConnection getReactiveClusterConnection(); - /** * Specifies if pipelined results should be converted to the expected data type. If false, results of * {@link RedisConnection#closePipeline()} and {RedisConnection#exec()} will be of the type returned by the underlying * driver This method is mostly for backwards compatibility with 1.0. It is generally always a good idea to allow * results to be converted and deserialized. In fact, this is now the default behavior. - * + * * @return Whether or not to convert pipeline and tx results */ boolean getConvertPipelineAndTxResults(); /** * Provides a suitable connection for interacting with Redis Sentinel. - * + * * @return connection for interacting with Redis Sentinel. * @since 1.4 */ diff --git a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java index 0799ab8f5..1a0a1d74b 100644 --- a/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/jedis/JedisConnectionFactory.java @@ -15,6 +15,16 @@ */ package org.springframework.data.redis.connection.jedis; +import redis.clients.jedis.HostAndPort; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.JedisCluster; +import redis.clients.jedis.JedisPool; +import redis.clients.jedis.JedisPoolConfig; +import redis.clients.jedis.JedisSentinelPool; +import redis.clients.jedis.JedisShardInfo; +import redis.clients.jedis.Protocol; +import redis.clients.util.Pool; + import java.lang.reflect.Method; import java.util.Collection; import java.util.Collections; @@ -33,22 +43,19 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.data.redis.connection.*; +import org.springframework.data.redis.connection.ClusterCommandExecutor; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisClusterConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSentinelConnection; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import redis.clients.jedis.HostAndPort; -import redis.clients.jedis.Jedis; -import redis.clients.jedis.JedisCluster; -import redis.clients.jedis.JedisPool; -import redis.clients.jedis.JedisPoolConfig; -import redis.clients.jedis.JedisSentinelPool; -import redis.clients.jedis.JedisShardInfo; -import redis.clients.jedis.Protocol; -import redis.clients.util.Pool; - /** * Connection factory creating Jedis based connections. * @@ -358,24 +365,11 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, return new JedisClusterConnection(cluster, clusterCommandExecutor); } + /* * (non-Javadoc) - * @see org.springframework.data.redis.connection.RedisConnectionFactory#getReactiveConnection() + * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) */ - @Override - public ReactiveRedisConnection getReactiveConnection() { - throw new UnsupportedOperationException("Jedis does not support racative connections"); - } - - @Override - public ReactiveRedisClusterConnection getReactiveClusterConnection() { - throw new UnsupportedOperationException("Jedis does not support racative connections"); - } - - /* - * (non-Javadoc) - * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) - */ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EXCEPTION_TRANSLATION.translate(ex); } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java index ee6ce206d..31d9112cf 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2016 the original author or authors. + * Copyright 2011-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. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - package org.springframework.data.redis.connection.lettuce; import java.util.ArrayList; @@ -30,15 +29,7 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.data.redis.ExceptionTranslationStrategy; import org.springframework.data.redis.PassThroughExceptionTranslationStrategy; import org.springframework.data.redis.RedisConnectionFailureException; -import org.springframework.data.redis.connection.ClusterCommandExecutor; -import org.springframework.data.redis.connection.Pool; -import org.springframework.data.redis.connection.RedisClusterConfiguration; -import org.springframework.data.redis.connection.RedisClusterConnection; -import org.springframework.data.redis.connection.RedisConnection; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.connection.RedisNode; -import org.springframework.data.redis.connection.RedisSentinelConfiguration; -import org.springframework.data.redis.connection.RedisSentinelConnection; +import org.springframework.data.redis.connection.*; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -70,7 +61,8 @@ import com.lambdaworks.redis.resource.ClientResources; * @author Mark Paluch * @author Balázs Németh */ -public class LettuceConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory { +public class LettuceConnectionFactory + implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory { public static final String PING_REPLY = "PONG"; @@ -206,13 +198,17 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea /* * (non-Javadoc) - * @see org.springframework.data.redis.connection.RedisConnectionFactory#getReactiveConnection() + * @see org.springframework.data.redis.connection.ReactiveRedisConnectionFactory#getReactiveConnection() */ @Override public LettuceReactiveRedisConnection getReactiveConnection() { return new LettuceReactiveRedisConnection(client); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnectionFactory#getReactiveClusterConnection() + */ @Override public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() { if(!isClusterAware()) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java index 5250f7225..e37b550e8 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java @@ -16,6 +16,9 @@ package org.springframework.data.redis.connection.lettuce; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import java.nio.ByteBuffer; import java.util.List; @@ -30,9 +33,6 @@ import org.springframework.util.Assert; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.api.reactive.RedisKeyReactiveCommands; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - /** * @author Christoph Strobl * @author Mark Paluch @@ -130,4 +130,12 @@ public class LettuceReactiveClusterKeyCommands extends LettuceReactiveKeyCommand return result.map(val -> new BooleanResponse<>(command, val)); })); } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.lettuce.LettuceReactiveKeyCommands#move(org.reactivestreams.Publisher) + */ + @Override + public Flux> move(Publisher commands) { + throw new UnsupportedOperationException("MOVE not supported in CLUSTER mode!"); + } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java index 1dcf53d1a..f31cdfd28 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -15,6 +15,9 @@ */ package org.springframework.data.redis.connection.lettuce; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import java.nio.ByteBuffer; import java.util.List; import java.util.stream.Collectors; @@ -29,13 +32,11 @@ import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiVa import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; import org.springframework.util.Assert; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - import com.lambdaworks.redis.api.reactive.RedisKeyReactiveCommands; /** * @author Christoph Strobl + * @author Mark Paluch * @since 2.0 */ public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { @@ -44,7 +45,7 @@ public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { /** * Create new {@link LettuceReactiveKeyCommands}. - * + * * @param connection must not be {@literal null}. */ public LettuceReactiveKeyCommands(LettuceReactiveRedisConnection connection) { @@ -86,37 +87,6 @@ public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { })); } - /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#del(org.reactivestreams.Publisher) - */ - @Override - public Flux> del(Publisher commands) { - - return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { - - Assert.notNull(command.getKey(), "Key must not be null!"); - - return cmd.del(command.getKey()).map((value) -> new NumericResponse<>(command, value)); - })); - } - - /* - * (non-Javadoc) - * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#mDel(org.reactivestreams.Publisher) - */ - @Override - public Flux, Long>> mDel(Publisher> keysCollection) { - - return connection.execute(cmd -> Flux.from(keysCollection).flatMap((keys) -> { - - Assert.notEmpty(keys, "Keys must not be null!"); - - return cmd.del(keys.stream().collect(Collectors.toList()).toArray(new ByteBuffer[keys.size()])) - .map((value) -> new NumericResponse<>(keys, value)); - })); - } - /* * (non-Javadoc) * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#keys(org.reactivestreams.Publisher) @@ -126,7 +96,7 @@ public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { return connection.execute(cmd -> Flux.from(patterns).flatMap(pattern -> { Assert.notNull(pattern, "Pattern must not be null!"); - + // TODO: stream elements instead of collection return cmd.keys(pattern).collectList().map(value -> new MultiValueResponse<>(pattern, value)); })); } @@ -172,4 +142,156 @@ public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { return cmd.renamenx(command.getKey(), command.getNewName()).map(value -> new BooleanResponse<>(command, value)); })); } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#del(org.reactivestreams.Publisher) + */ + @Override + public Flux> del(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return cmd.del(command.getKey()).map((value) -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#mDel(org.reactivestreams.Publisher) + */ + @Override + public Flux, Long>> mDel(Publisher> keysCollection) { + + return connection.execute(cmd -> Flux.from(keysCollection).flatMap((keys) -> { + + Assert.notEmpty(keys, "Keys must not be null!"); + + return cmd.del(keys.stream().collect(Collectors.toList()).toArray(new ByteBuffer[keys.size()])) + .map((value) -> new NumericResponse<>(keys, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#expire(org.reactivestreams.Publisher) + */ + @Override + public Flux> expire(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + return cmd.expire(command.getKey(), command.getTimeout().getSeconds()) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#pExpire(org.reactivestreams.Publisher) + */ + @Override + public Flux> pExpire(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + return cmd.pexpire(command.getKey(), command.getTimeout().getSeconds()) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#expireAt(org.reactivestreams.Publisher) + */ + @Override + public Flux> expireAt(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getExpireAt(), "Expire at must not be null!"); + + return cmd.expireat(command.getKey(), command.getExpireAt().getEpochSecond()) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#pExpireAt(org.reactivestreams.Publisher) + */ + @Override + public Flux> pExpireAt(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getExpireAt(), "Expire at must not be null!"); + + return cmd.expireat(command.getKey(), command.getExpireAt().toEpochMilli()) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#persist(org.reactivestreams.Publisher) + */ + @Override + public Flux> persist(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return cmd.persist(command.getKey()).map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#ttl(org.reactivestreams.Publisher) + */ + @Override + public Flux> ttl(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return cmd.ttl(command.getKey()).map(value -> new NumericResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#pTtl(org.reactivestreams.Publisher) + */ + @Override + public Flux> pTtl(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return cmd.pttl(command.getKey()).map(value -> new NumericResponse<>(command, value)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveKeyCommands#move(org.reactivestreams.Publisher) + */ + @Override + public Flux> move(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDatabase(), "Database must not be null!"); + + return cmd.move(command.getKey(), command.getDatabase()).map(value -> new BooleanResponse<>(command, value)); + })); + } } diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java index d880637d7..c1fa299e3 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -15,6 +15,9 @@ */ package org.springframework.data.redis.connection.lettuce; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + import java.nio.ByteBuffer; import java.util.List; @@ -42,9 +45,6 @@ import com.lambdaworks.redis.ZStoreArgs; import com.lambdaworks.redis.codec.StringCodec; import com.lambdaworks.redis.protocol.LettuceCharsets; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; - /** * @author Christoph Strobl * @author Mark Paluch @@ -258,7 +258,7 @@ public class LettuceReactiveZSetCommands implements ReactiveZSetCommands { } } else { - Range range = ArgumentConverters.toRevRange(command.getRange()); + Range range = ArgumentConverters.toRange(command.getRange()); if (command.isWithScores()) { @@ -447,14 +447,14 @@ public class LettuceReactiveZSetCommands implements ReactiveZSetCommands { result = cmd.zrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()), LettuceConverters.toLimit(command.getLimit())); } else { - result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRevRange(command.getRange()), + result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange()), LettuceConverters.toLimit(command.getLimit())); } } else { if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) { result = cmd.zrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange())); } else { - result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRevRange(command.getRange())); + result = cmd.zrevrangebylex(command.getKey(), ArgumentConverters.toRange(command.getRange())); } } @@ -507,10 +507,6 @@ public class LettuceReactiveZSetCommands implements ReactiveZSetCommands { return Range.from(lowerBoundArgOf(range), upperBoundArgOf(range)); } - static Range toRevRange(org.springframework.data.domain.Range range) { - return Range.from(upperBoundArgOf(range), lowerBoundArgOf(range)); - } - @SuppressWarnings("unchecked") static Boundary lowerBoundArgOf(org.springframework.data.domain.Range range) { return (Boundary) rangeToBoundArgumentConverter(false).convert(range); diff --git a/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java b/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java index bd23949bd..149659364 100644 --- a/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java +++ b/src/main/java/org/springframework/data/redis/core/CloseSuppressingInvocationHandler.java @@ -33,9 +33,9 @@ class CloseSuppressingInvocationHandler implements InvocationHandler { private static final String HASH_CODE = "hashCode"; private static final String EQUALS = "equals"; - private final RedisConnection target; + private final Object target; - public CloseSuppressingInvocationHandler(RedisConnection target) { + public CloseSuppressingInvocationHandler(Object target) { this.target = target; } diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java new file mode 100644 index 000000000..cfb1ef52f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java @@ -0,0 +1,395 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResult; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.ReactiveGeoCommands; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveGeoOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveGeoOperations implements ReactiveGeoOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveGeoOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoAdd(java.lang.Object, org.springframework.data.geo.Point, java.lang.Object) + */ + @Override + public Mono geoAdd(K key, Point point, V member) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(point, "Point must not be null!"); + Assert.notNull(member, "Member must not be null!"); + + return createMono(connection -> connection.geoAdd(rawKey(key), point, rawValue(member))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoAdd(java.lang.Object, org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation) + */ + @Override + public Mono geoAdd(K key, GeoLocation location) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(location, "GeoLocation must not be null!"); + + return createMono(connection -> connection.geoAdd(rawKey(key), + new GeoLocation<>(rawValue(location.getName()), location.getPoint()))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoAdd(java.lang.Object, java.util.Map) + */ + @Override + public Mono geoAdd(K key, Map memberCoordinateMap) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(memberCoordinateMap, "Map must not be null!"); + + return createMono(connection -> { + + Mono>> serializedList = Flux + .fromIterable(() -> memberCoordinateMap.entrySet().iterator()) + .map(entry -> new GeoLocation<>(rawValue(entry.getKey()), entry.getValue())).collectList(); + + return serializedList.flatMap(list -> connection.geoAdd(rawKey(key), list)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoAdd(java.lang.Object, java.lang.Iterable) + */ + @Override + public Mono geoAdd(K key, Iterable> geoLocations) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(geoLocations, "GeoLocations must not be null!"); + + return createMono(connection -> { + + Mono>> serializedList = Flux.fromIterable(geoLocations) + .map(location -> new GeoLocation<>(rawValue(location.getName()), location.getPoint())).collectList(); + + return serializedList.flatMap(list -> connection.geoAdd(rawKey(key), list)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoAdd(java.lang.Object, org.reactivestreams.Publisher) + */ + @Override + public Flux geoAdd(K key, Publisher>> locations) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(locations, "GeoLocations must not be null!"); + + return createFlux(connection -> { + + return Flux.from(locations) + .map(locationList -> locationList.stream() + .map(location -> new GeoLocation<>(rawValue(location.getName()), location.getPoint())) + .collect(Collectors.toList())) + .flatMap(list -> connection.geoAdd(rawKey(key), list)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoDist(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono geoDist(K key, V member1, V member2) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member1, "Member 1 must not be null!"); + Assert.notNull(member2, "Member 2 must not be null!"); + + return createMono(connection -> connection.geoDist(rawKey(key), rawValue(member1), rawValue(member2))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoDist(java.lang.Object, java.lang.Object, java.lang.Object, org.springframework.data.geo.Metric) + */ + @Override + public Mono geoDist(K key, V member1, V member2, Metric metric) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member1, "Member 1 must not be null!"); + Assert.notNull(member2, "Member 2 must not be null!"); + Assert.notNull(metric, "Metric must not be null!"); + + return createMono(connection -> connection.geoDist(rawKey(key), rawValue(member1), rawValue(member2), metric)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoHash(java.lang.Object, java.lang.Object) + */ + @Override + public Mono geoHash(K key, V member) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member, "Member must not be null!"); + + return createMono(connection -> connection.geoHash(rawKey(key), rawValue(member))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoHash(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono> geoHash(K key, V... members) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(members, "Member must not be null!"); + Assert.notEmpty(members, "Members must not be empty!"); + Assert.noNullElements(members, "Members must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(members) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.geoHash(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoPos(java.lang.Object, java.lang.Object) + */ + @Override + public Mono geoPos(K key, V member) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member, "Member must not be null!"); + + return createMono(connection -> connection.geoPos(rawKey(key), rawValue(member))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoPos(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono> geoPos(K key, V... members) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(members, "Member must not be null!"); + Assert.notEmpty(members, "Members must not be empty!"); + Assert.noNullElements(members, "Members must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(members) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.geoPos(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRadius(java.lang.Object, org.springframework.data.geo.Circle) + */ + @Override + public Mono>> geoRadius(K key, Circle within) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(within, "Circle must not be null!"); + + return createMono(connection -> { + + return connection.geoRadius(rawKey(key), within) // + .flatMap(Flux::fromIterable) // + .map(location -> new GeoLocation<>(readValue(location.getName()), location.getPoint())) // + .collectList(); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRadius(java.lang.Object, org.springframework.data.geo.Circle, org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs) + */ + @Override + public Mono>> geoRadius(K key, Circle within, GeoRadiusCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(within, "Circle must not be null!"); + Assert.notNull(args, "GeoRadiusCommandArgs must not be null!"); + + return createMono(connection -> { + + return connection.geoRadius(rawKey(key), within, args) // + .flatMap(Flux::fromIterable) // + .map(geoResult -> new GeoResult<>( + new GeoLocation<>(readValue(geoResult.getContent().getName()), geoResult.getContent().getPoint()), + geoResult.getDistance())) // + .collectList() // + .map(GeoResults::new); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRadiusByMember(java.lang.Object, java.lang.Object, double) + */ + @Override + public Mono>> geoRadiusByMember(K key, V member, double radius) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member, "Member must not be null!"); + + return createMono(connection -> { + + return connection.geoRadiusByMember(rawKey(key), rawValue(member), new Distance(radius)) // + .flatMap(Flux::fromIterable) // + .map(geoLocation -> new GeoLocation<>(readValue(geoLocation.getName()), geoLocation.getPoint())) // + .collectList(); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRadiusByMember(java.lang.Object, java.lang.Object, org.springframework.data.geo.Distance) + */ + @Override + public Mono>> geoRadiusByMember(K key, V member, Distance distance) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member, "Member must not be null!"); + Assert.notNull(distance, "Distance must not be null!"); + + return createMono(connection -> { + + return connection.geoRadiusByMember(rawKey(key), rawValue(member), distance) // + .flatMap(Flux::fromIterable) // + .map(geoLocation -> new GeoLocation<>(readValue(geoLocation.getName()), geoLocation.getPoint())) // + .collectList(); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRadiusByMember(java.lang.Object, java.lang.Object, org.springframework.data.geo.Distance, org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs) + */ + @Override + public Mono>> geoRadiusByMember(K key, V member, Distance distance, + GeoRadiusCommandArgs args) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(member, "Member must not be null!"); + Assert.notNull(distance, "Distance must not be null!"); + Assert.notNull(args, "GeoRadiusCommandArgs must not be null!"); + + return createMono(connection -> { + + return connection.geoRadiusByMember(rawKey(key), rawValue(member), distance, args) // + .flatMap(Flux::fromIterable) // + .map(geoResult -> new GeoResult<>( + new GeoLocation<>(readValue(geoResult.getContent().getName()), geoResult.getContent().getPoint()), + geoResult.getDistance())) // + .collectList() // + .map(GeoResults::new); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#geoRemove(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono geoRemove(K key, V... members) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(members, "Member must not be null!"); + Assert.notEmpty(members, "Members must not be empty!"); + Assert.noNullElements(members, "Members must not contain null elements!"); + + return template.createMono(connection -> { + + return Flux.fromArray(members) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.zSetCommands().zRem(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveGeoOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.geoCommands())); + } + + private Flux createFlux(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createFlux(connection -> function.apply(connection.geoCommands())); + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } + + private V readValue(ByteBuffer buffer) { + return serializationContext.value().read(buffer); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java new file mode 100644 index 000000000..ba9918884 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java @@ -0,0 +1,299 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveHashOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveHashOperations implements ReactiveHashOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveHashOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#delete(java.lang.Object, java.lang.Object[]) + */ + @Override + @SuppressWarnings("unchecked") + public Mono remove(H key, Object... hashKeys) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKeys, "Hash keys must not be null!"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty!"); + Assert.noNullElements(hashKeys, "Hash keys must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(hashKeys) // + .map(o -> (HK) o).map(this::rawHashKey) // + .collectList() // + .then(hks -> connection.hDel(rawKey(key), hks)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#hasKey(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono hasKey(H key, Object hashKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + + return createMono(connection -> connection.hExists(rawKey(key), rawHashKey((HK) hashKey))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#get(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono get(H key, Object hashKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + + return createMono(connection -> connection.hGet(rawKey(key), rawHashKey((HK) hashKey)).map(this::readHashValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#multiGet(java.lang.Object, java.util.Collection) + */ + @Override + public Mono> multiGet(H key, Collection hashKeys) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKeys, "Hash keys must not be null!"); + Assert.notEmpty(hashKeys, "Hash keys must not be empty!"); + + return createMono(connection -> { + + return Flux.fromIterable(hashKeys) // + .map(this::rawHashKey) // + .collectList() // + .then(hks -> connection.hMGet(rawKey(key), hks)).map(byteBuffers -> { + + List values = new ArrayList(byteBuffers.size()); + for (ByteBuffer byteBuffer : byteBuffers) { + values.add(readHashValue(byteBuffer)); + } + return values; + }); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#increment(java.lang.Object, java.lang.Object, long) + */ + @Override + public Mono increment(H key, HK hashKey, long delta) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + + return template.createMono(connection -> connection // + .numberCommands() // + .hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#increment(java.lang.Object, java.lang.Object, double) + */ + @Override + public Mono increment(H key, HK hashKey, double delta) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + + return template.createMono(connection -> connection // + .numberCommands() // + .hIncrBy(rawKey(key), rawHashKey(hashKey), delta)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#keys(java.lang.Object) + */ + @Override + public Mono> keys(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.hKeys(rawKey(key)) // + .flatMap(Flux::fromIterable) // + .map(this::readHashKey) // + .collectList()); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#size(java.lang.Object) + */ + @Override + public Mono size(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.hLen(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#putAll(java.lang.Object, java.util.Map) + */ + @Override + public Mono putAll(H key, Map map) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(map, "Map must not be null!"); + + return createMono(connection -> { + + return Flux.fromIterable(() -> map.entrySet().iterator()) // + .collectMap(entry -> rawHashKey(entry.getKey()), entry -> rawHashValue(entry.getValue())) // + .flatMap(serialized -> connection.hMSet(rawKey(key), serialized)); + + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#put(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono put(H key, HK hashKey, HV value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + Assert.notNull(value, "Hash value must not be null!"); + + return createMono(connection -> connection.hSet(rawKey(key), rawHashKey(hashKey), rawHashValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#putIfAbsent(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono putIfAbsent(H key, HK hashKey, HV value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(hashKey, "Hash key must not be null!"); + Assert.notNull(value, "Hash value must not be null!"); + + return createMono(connection -> connection.hSetNX(rawKey(key), rawHashKey(hashKey), rawHashValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#values(java.lang.Object) + */ + @Override + public Mono> values(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.hVals(rawKey(key)) // + .flatMap(Flux::fromIterable) // + .map(this::readHashValue) // + .collectList()); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#entries(java.lang.Object) + */ + @Override + public Mono> entries(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.hGetAll(rawKey(key)) // + .map(map -> { + + Map deserialized = new LinkedHashMap<>(map.size()); + + map.forEach((k, v) -> { + deserialized.put(readHashKey(k), readHashValue(v)); + }); + + return deserialized; + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHashOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(H key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.hashCommands())); + } + + private ByteBuffer rawKey(H key) { + return serializationContext.key().write(key); + } + + private ByteBuffer rawHashKey(HK key) { + return serializationContext.hashKey().write(key); + } + + private ByteBuffer rawHashValue(HV key) { + return serializationContext.hashValue().write(key); + } + + @SuppressWarnings("unchecked") + private HK readHashKey(ByteBuffer value) { + return (HK) serializationContext.hashKey().read(value); + } + + @SuppressWarnings("unchecked") + private HV readHashValue(ByteBuffer value) { + return (HV) serializationContext.hashValue().read(value); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java new file mode 100644 index 000000000..bec84265b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java @@ -0,0 +1,137 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveHyperLogLogCommands; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveHyperLogLogOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveHyperLogLogOperations implements ReactiveHyperLogLogOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveHyperLogLogOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHyperLogLogOperations#add(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono add(K key, V... values) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(values, "Values must not be null!"); + Assert.notEmpty(values, "Values must not be empty!"); + Assert.noNullElements(values, "Values must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serializedValues -> connection.pfAdd(rawKey(key), serializedValues)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHyperLogLogOperations#size(java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono size(K... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + Assert.notEmpty(keys, "Keys must not be empty!"); + Assert.noNullElements(keys, "Keys must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(connection::pfCount); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHyperLogLogOperations#union(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono union(K destination, K... sourceKeys) { + + Assert.notNull(destination, "Destination key must not be null!"); + Assert.notNull(sourceKeys, "Source keys must not be null!"); + Assert.notEmpty(sourceKeys, "Source keys must not be empty!"); + Assert.noNullElements(sourceKeys, "Source keys must not contain null elements!"); + + return createMono(connection -> { + + return Flux.fromArray(sourceKeys) // + .map(this::rawKey) // + .collectList() // + .flatMap(serialized -> connection.pfMerge(rawKey(destination), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveHyperLogLogOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.hyperLogLogCommands())); + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java new file mode 100644 index 000000000..58121bee3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java @@ -0,0 +1,368 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveListOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveListOperations implements ReactiveListOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveListOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#range(java.lang.Object, long, long) + */ + @Override + public Mono> range(K key, long start, long end) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lRange(rawKey(key), start, end).map(raw -> { + + List result = new ArrayList(raw.size()); + + for (ByteBuffer buffer : raw) { + result.add(readValue(buffer)); + } + + return result; + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#trim(java.lang.Object, long, long) + */ + @Override + public Mono trim(K key, long start, long end) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lTrim(rawKey(key), start, end)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#size(java.lang.Object) + */ + @Override + public Mono size(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lLen(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPush(java.lang.Object, java.lang.Object) + */ + @Override + public Mono leftPush(K key, V value) { + return leftPushAll(key, value); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPushAll(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono leftPushAll(K key, V... values) { + + Assert.notNull(values, "Values must not be null!"); + + return leftPushAll(key, Arrays.asList(values)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPushAll(java.lang.Object, java.util.Collection) + */ + @Override + public Mono leftPushAll(K key, Collection values) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(values, "Values must not be null!"); + Assert.notEmpty(values, "Values must not be empty!"); + + return createMono(connection -> { + + return Flux.fromIterable(values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.lPush(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPushIfPresent(java.lang.Object, java.lang.Object) + */ + @Override + public Mono leftPushIfPresent(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lPushX(rawKey(key), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPush(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono leftPush(K key, V pivot, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lInsert(rawKey(key), Position.BEFORE, rawValue(pivot), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPush(java.lang.Object, java.lang.Object) + */ + @Override + public Mono rightPush(K key, V value) { + return rightPushAll(key, value); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPushAll(java.lang.Object, java.lang.Object[]) + */ + @Override + @SafeVarargs + public final Mono rightPushAll(K key, V... values) { + + Assert.notNull(values, "Values must not be null!"); + + return rightPushAll(key, Arrays.asList(values)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPushAll(java.lang.Object, java.util.Collection) + */ + @Override + public Mono rightPushAll(K key, Collection values) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(values, "Values must not be null!"); + Assert.notEmpty(values, "Values must not be empty!"); + + return createMono(connection -> { + + return Flux.fromIterable(values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.rPush(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPushIfPresent(java.lang.Object, java.lang.Object) + */ + @Override + public Mono rightPushIfPresent(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.rPushX(rawKey(key), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPush(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono rightPush(K key, V pivot, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lInsert(rawKey(key), Position.AFTER, rawValue(pivot), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#set(java.lang.Object, long, java.lang.Object) + */ + @Override + public Mono set(K key, long index, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lSet(rawKey(key), index, rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#remove(java.lang.Object, long, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono remove(K key, long count, Object value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lRem(rawKey(key), count, rawValue((V) value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#index(java.lang.Object, long) + */ + @Override + public Mono index(K key, long index) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lIndex(rawKey(key), index).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPop(java.lang.Object) + */ + @Override + public Mono leftPop(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.lPop(rawKey(key)).map(this::readValue)); + + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#leftPop(java.lang.Object, java.time.Duration) + */ + @Override + public Mono leftPop(K key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Duration must not be null!"); + Assert.isTrue(isZeroOrGreater1Second(timeout), "Duration must be either zero or greater or equal to 1 second"); + + return createMono(connection -> connection.blPop(Collections.singletonList(rawKey(key)), timeout) + .map(popResult -> readValue(popResult.getValue()))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPop(java.lang.Object) + */ + @Override + public Mono rightPop(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.rPop(rawKey(key)).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPop(java.lang.Object, java.time.Duration) + */ + @Override + public Mono rightPop(K key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Duration must not be null!"); + Assert.isTrue(isZeroOrGreater1Second(timeout), "Duration must be either zero or greater or equal to 1 second"); + + return createMono(connection -> connection.brPop(Collections.singletonList(rawKey(key)), timeout) + .map(popResult -> readValue(popResult.getValue()))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPopAndLeftPush(java.lang.Object, java.lang.Object) + */ + @Override + public Mono rightPopAndLeftPush(K sourceKey, K destinationKey) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + + return createMono( + connection -> connection.rPopLPush(rawKey(sourceKey), rawKey(destinationKey)).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#rightPopAndLeftPush(java.lang.Object, java.lang.Object, java.time.Duration) + */ + @Override + public Mono rightPopAndLeftPush(K sourceKey, K destinationKey, Duration timeout) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destinationKey, "Destination key must not be null!"); + Assert.notNull(timeout, "Duration must not be null!"); + Assert.isTrue(isZeroOrGreater1Second(timeout), "Duration must be either zero or greater or equal to 1 second"); + + return createMono( + connection -> connection.bRPopLPush(rawKey(sourceKey), rawKey(destinationKey), timeout).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveListOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.listCommands())); + } + + private boolean isZeroOrGreater1Second(Duration timeout) { + return timeout.isZero() || timeout.getNano() % TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS) == 0; + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } + + private V readValue(ByteBuffer buffer) { + return serializationContext.value().read(buffer); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java new file mode 100644 index 000000000..7dc4cb667 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java @@ -0,0 +1,451 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveSetCommands; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveSetOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveSetOperations implements ReactiveSetOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveSetOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#add(java.lang.Object, java.lang.Object[]) + */ + @Override + public Mono add(K key, V... values) { + + Assert.notNull(key, "Key must not be null!"); + + if (values.length == 1) { + return createMono(connection -> connection.sAdd(rawKey(key), rawValue(values[0]))); + } + + return createMono(connection -> { + + return Flux.fromArray(values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.sAdd(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#remove(java.lang.Object, java.lang.Object[]) + */ + @Override + @SuppressWarnings("unchecked") + public Mono remove(K key, Object... values) { + + Assert.notNull(key, "Key must not be null!"); + + if (values.length == 1) { + return createMono(connection -> connection.sRem(rawKey(key), rawValue((V) values[0]))); + } + + return createMono(connection -> { + + return Flux.fromArray((V[]) values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.sRem(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#pop(java.lang.Object) + */ + @Override + public Mono pop(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.sPop(rawKey(key)).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#move(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono move(K sourceKey, V value, K destKey) { + + Assert.notNull(sourceKey, "Source key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return createMono(connection -> connection.sMove(rawKey(sourceKey), rawKey(destKey), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#size(java.lang.Object) + */ + @Override + public Mono size(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.sCard(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#isMember(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono isMember(K key, Object o) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.sIsMember(rawKey(key), rawValue((V) o))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#intersect(java.lang.Object, java.lang.Object) + */ + @Override + public Mono> intersect(K key, K otherKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + + return intersect(key, Collections.singleton(otherKey)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#intersect(java.lang.Object, java.util.Collection) + */ + @Override + public Mono> intersect(K key, Collection otherKeys) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(connection::sInter) // + .map(this::readValueSet); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#intersectAndStore(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono intersectAndStore(K key, K otherKey, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return intersectAndStore(key, Collections.singleton(otherKey), destKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#intersectAndStore(java.lang.Object, java.util.Collection, java.lang.Object) + */ + @Override + public Mono intersectAndStore(K key, Collection otherKeys, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(rawKeys -> connection.sInterStore(rawKey(destKey), rawKeys)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#union(java.lang.Object, java.lang.Object) + */ + @Override + public Mono> union(K key, K otherKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + + return union(key, Collections.singleton(otherKey)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#union(java.lang.Object, java.util.Collection) + */ + @Override + public Mono> union(K key, Collection otherKeys) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(connection::sUnion) // + .map(this::readValueSet); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#unionAndStore(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono unionAndStore(K key, K otherKey, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return unionAndStore(key, Collections.singleton(otherKey), destKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#unionAndStore(java.lang.Object, java.util.Collection, java.lang.Object) + */ + @Override + public Mono unionAndStore(K key, Collection otherKeys, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(rawKeys -> connection.sUnionStore(rawKey(destKey), rawKeys)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#difference(java.lang.Object, java.lang.Object) + */ + @Override + public Mono> difference(K key, K otherKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + + return difference(key, Collections.singleton(otherKey)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#difference(java.lang.Object, java.util.Collection) + */ + @Override + public Mono> difference(K key, Collection otherKeys) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(connection::sDiff) // + .map(this::readValueSet); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#differenceAndStore(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono differenceAndStore(K key, K otherKey, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return differenceAndStore(key, Collections.singleton(otherKey), destKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#differenceAndStore(java.lang.Object, java.util.Collection, java.lang.Object) + */ + @Override + public Mono differenceAndStore(K key, Collection otherKeys, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(rawKeys -> connection.sDiffStore(rawKey(destKey), rawKeys)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#members(java.lang.Object) + */ + @Override + public Mono> members(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.sMembers(rawKey(key)).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#randomMember(java.lang.Object) + */ + @Override + public Mono randomMember(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.sRandMember(rawKey(key)).map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#distinctRandomMembers(java.lang.Object, long) + */ + @Override + public Mono> distinctRandomMembers(K key, long count) { + + Assert.isTrue(count > 0, "Negative count not supported. Use randomMembers to allow duplicate elements."); + + return createMono(connection -> connection.sRandMember(rawKey(key), count).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#randomMembers(java.lang.Object, long) + */ + @Override + public Mono> randomMembers(K key, long count) { + + Assert.isTrue(count > 0, "Use a positive number for count. This method is already allowing duplicate elements."); + + return createMono(connection -> connection.sRandMember(rawKey(key), -count).map(this::readValueList)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveSetOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.setCommands())); + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private List getKeys(K key, Collection otherKeys) { + + List keys = new ArrayList<>(1 + otherKeys.size()); + + keys.add(key); + keys.addAll(otherKeys); + + return keys; + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } + + private V readValue(ByteBuffer buffer) { + return serializationContext.value().read(buffer); + } + + private Set readValueSet(Collection raw) { + + Set result = new LinkedHashSet<>(raw.size()); + + for (ByteBuffer buffer : raw) { + result.add(readValue(buffer)); + } + + return result; + } + + private List readValueList(Collection raw) { + + List result = new ArrayList<>(raw.size()); + + for (ByteBuffer buffer : raw) { + result.add(readValue(buffer)); + } + + return result; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java new file mode 100644 index 000000000..25050e20c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java @@ -0,0 +1,298 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveStringCommands; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveValueOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveValueOperations implements ReactiveValueOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveValueOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#set(java.lang.Object, java.lang.Object) + */ + @Override + public Mono set(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.set(rawKey(key), rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#set(java.lang.Object, java.lang.Object, long, java.util.concurrent.TimeUnit) + */ + @Override + public Mono set(K key, V value, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Duration must not be null!"); + + return createMono( + connection -> connection.set(rawKey(key), rawValue(value), Expiration.from(timeout), SetOption.UPSERT)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#setIfAbsent(java.lang.Object, java.lang.Object) + */ + @Override + public Mono setIfAbsent(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono( + connection -> connection.set(rawKey(key), rawValue(value), Expiration.persistent(), SetOption.SET_IF_ABSENT)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#setIfPresent(java.lang.Object, java.lang.Object) + */ + @Override + public Mono setIfPresent(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono( + connection -> connection.set(rawKey(key), rawValue(value), Expiration.persistent(), SetOption.SET_IF_PRESENT)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#multiSet(java.util.Map) + */ + @Override + public Mono multiSet(Map map) { + + Assert.notNull(map, "Map must not be null!"); + + return createMono(connection -> { + + Mono> serializedMap = Flux.fromIterable(() -> map.entrySet().iterator()) + .collectMap(entry -> rawKey(entry.getKey()), entry -> rawValue(entry.getValue())); + + return serializedMap.flatMap(connection::mSet); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#multiSetIfAbsent(java.util.Map) + */ + @Override + public Mono multiSetIfAbsent(Map map) { + + Assert.notNull(map, "Map must not be null!"); + + return createMono(connection -> { + + Mono> serializedMap = Flux.fromIterable(() -> map.entrySet().iterator()) + .collectMap(entry -> rawKey(entry.getKey()), entry -> rawValue(entry.getValue())); + + return serializedMap.flatMap(connection::mSetNX); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#get(java.lang.Object) + */ + @SuppressWarnings("unchecked") + @Override + public Mono get(Object key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.get(rawKey((K) key)) // + .map(this::readValue)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#getAndSet(java.lang.Object, java.lang.Object) + */ + @Override + public Mono getAndSet(K key, V value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.getSet(rawKey(key), rawValue(value)).map(value()::read)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#multiGet(java.util.Collection) + */ + @Override + public Mono> multiGet(Collection keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + return createMono(connection -> Flux.fromIterable(keys).map(key()::write).collectList().flatMap(connection::mGet) + .map(byteBuffers -> { + List result = new ArrayList<>(byteBuffers.size()); + + for (ByteBuffer buffer : byteBuffers) { + + if (buffer == null) { + result.add(null); + } else { + result.add(readValue(buffer)); + } + } + + return result; + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#append(java.lang.Object, java.lang.String) + */ + @Override + public Mono append(K key, String value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return createMono(connection -> connection.append(rawKey(key), serializationContext.string().write(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#get(java.lang.Object, long, long) + */ + @Override + public Mono get(K key, long start, long end) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.getRange(rawKey(key), start, end) // + .map(string()::read)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#set(java.lang.Object, java.lang.Object, long) + */ + @Override + public Mono set(K key, V value, long offset) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.setRange(rawKey(key), rawValue(value), offset)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#size(java.lang.Object) + */ + @Override + public Mono size(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.strLen(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#setBit(java.lang.Object, long, boolean) + */ + @Override + public Mono setBit(K key, long offset, boolean value) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.setBit(rawKey(key), offset, value)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#getBit(java.lang.Object, long) + */ + @Override + public Mono getBit(K key, long offset) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.getBit(rawKey(key), offset)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveValueOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.stringCommands())); + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } + + private V readValue(ByteBuffer buffer) { + return serializationContext.value().read(buffer); + } + + private SerializationTuple string() { + return serializationContext.string(); + } + + private SerializationTuple key() { + return serializationContext.key(); + } + + private SerializationTuple value() { + return serializationContext.value(); + } +} diff --git a/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java new file mode 100644 index 000000000..f9e1d1a26 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java @@ -0,0 +1,537 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.ReactiveZSetCommands; +import org.springframework.data.redis.connection.RedisZSetCommands.Limit; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.data.redis.util.ByteUtils; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveZSetOperations}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveZSetOperations implements ReactiveZSetOperations { + + private final ReactiveRedisTemplate template; + private final ReactiveSerializationContext serializationContext; + + public DefaultReactiveZSetOperations(ReactiveRedisTemplate template, + ReactiveSerializationContext serializationContext) { + + Assert.notNull(template, "ReactiveRedisTemplate must not be null!"); + Assert.notNull(serializationContext, "ReactiveSerializationContext must not be null!"); + + this.template = template; + this.serializationContext = serializationContext; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#add(java.lang.Object, java.lang.Object, double) + */ + @Override + public Mono add(K key, V value, double score) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zAdd(rawKey(key), score, rawValue(value)).map(l -> l != 0)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#add(java.lang.Object, java.util.Collection) + */ + @Override + public Mono addAll(K key, Collection> tuples) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(tuples, "Key must not be null!"); + + return createMono(connection -> { + + return Flux.fromIterable(tuples) // + .map(t -> new DefaultTuple(ByteUtils.getBytes(rawValue(t.getValue())), t.getScore())) // + .collectList() // + .flatMap(serialized -> connection.zAdd(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#remove(java.lang.Object, java.lang.Object[]) + */ + @Override + @SuppressWarnings("unchecked") + public Mono remove(K key, Object... values) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(values, "Values must not be null!"); + + if (values.length == 1) { + return createMono(connection -> connection.zRem(rawKey(key), rawValue((V) values[0]))); + } + + return createMono(connection -> { + + return Flux.fromArray((V[]) values) // + .map(this::rawValue) // + .collectList() // + .flatMap(serialized -> connection.zRem(rawKey(key), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#incrementScore(java.lang.Object, java.lang.Object, double) + */ + @Override + public Mono incrementScore(K key, V value, double delta) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zIncrBy(rawKey(key), delta, rawValue(value))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rank(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono rank(K key, Object o) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zRank(rawKey(key), rawValue((V) o))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRank(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono reverseRank(K key, Object o) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zRevRank(rawKey(key), rawValue((V) o))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#range(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> range(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRange(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeWithScores(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono>> rangeWithScores(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRangeWithScores(rawKey(key), range).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByScore(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> rangeByScore(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRangeByScore(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByScoreWithScores(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono>> rangeByScoreWithScores(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono( + connection -> connection.zRangeByScoreWithScores(rawKey(key), range).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByScore(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono> rangeByScore(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRangeByScore(rawKey(key), range, limit).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByScoreWithScores(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono>> rangeByScoreWithScores(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + Assert.notNull(limit, "Limit must not be null!"); + + return createMono( + connection -> connection.zRangeByScoreWithScores(rawKey(key), range, limit).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRange(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> reverseRange(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRevRange(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeWithScores(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono>> reverseRangeWithScores(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRevRangeWithScores(rawKey(key), range).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByScore(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> reverseRangeByScore(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRevRangeByScore(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByScoreWithScores(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono>> reverseRangeByScoreWithScores(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono( + connection -> connection.zRevRangeByScoreWithScores(rawKey(key), range).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByScore(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono> reverseRangeByScore(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRevRangeByScore(rawKey(key), range, limit).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByScoreWithScores(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono>> reverseRangeByScoreWithScores(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + Assert.notNull(limit, "Limit must not be null!"); + + return createMono( + connection -> connection.zRevRangeByScoreWithScores(rawKey(key), range, limit).map(this::readTypedTupleSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#count(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono count(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zCount(rawKey(key), range)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#size(java.lang.Object) + */ + @Override + public Mono size(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zCard(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#score(java.lang.Object, java.lang.Object) + */ + @Override + @SuppressWarnings("unchecked") + public Mono score(K key, Object o) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.zScore(rawKey(key), rawValue((V) o))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#removeRange(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono removeRange(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRemRangeByRank(rawKey(key), range)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#removeRangeByScore(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono removeRangeByScore(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRemRangeByScore(rawKey(key), range)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#unionAndStore(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono unionAndStore(K key, K otherKey, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return unionAndStore(key, Collections.singleton(otherKey), destKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#unionAndStore(java.lang.Object, java.util.Collection, java.lang.Object) + */ + @Override + public Mono unionAndStore(K key, Collection otherKeys, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(serialized -> connection.zUnionStore(rawKey(destKey), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#intersectAndStore(java.lang.Object, java.lang.Object, java.lang.Object) + */ + @Override + public Mono intersectAndStore(K key, K otherKey, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKey, "Other key must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + return intersectAndStore(key, Collections.singleton(otherKey), destKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#intersectAndStore(java.lang.Object, java.util.Collection, java.lang.Object) + */ + @Override + public Mono intersectAndStore(K key, Collection otherKeys, K destKey) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(otherKeys, "Other keys must not be null!"); + Assert.notNull(destKey, "Destination key must not be null!"); + + List keys = getKeys(key, otherKeys); + + return createMono(connection -> { + + return Flux.fromIterable(keys) // + .map(this::rawKey) // + .collectList() // + .flatMap(serialized -> connection.zInterStore(rawKey(destKey), serialized)); + }); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByLex(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> rangeByLex(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRangeByLex(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#rangeByLex(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono> rangeByLex(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + Assert.notNull(limit, "Limit must not be null!"); + + return createMono(connection -> connection.zRangeByLex(rawKey(key), range, limit).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByLex(java.lang.Object, org.springframework.data.domain.Range) + */ + @Override + public Mono> reverseRangeByLex(K key, Range range) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + + return createMono(connection -> connection.zRevRangeByLex(rawKey(key), range).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#reverseRangeByLex(java.lang.Object, org.springframework.data.domain.Range, org.springframework.data.redis.connection.RedisZSetCommands.Limit) + */ + @Override + public Mono> reverseRangeByLex(K key, Range range, Limit limit) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(range, "Range must not be null!"); + Assert.notNull(limit, "Limit must not be null!"); + + return createMono(connection -> connection.zRevRangeByLex(rawKey(key), range, limit).map(this::readValueSet)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveZSetOperations#delete(java.lang.Object) + */ + @Override + public Mono delete(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return template.createMono(connection -> connection.keyCommands().del(rawKey(key))).map(l -> l != 0); + } + + private Mono createMono(Function> function) { + + Assert.notNull(function, "Function must not be null!"); + + return template.createMono(connection -> function.apply(connection.zSetCommands())); + } + + private ByteBuffer rawKey(K key) { + return serializationContext.key().write(key); + } + + private List getKeys(K key, Collection otherKeys) { + + List keys = new ArrayList<>(1 + otherKeys.size()); + + keys.add(key); + keys.addAll(otherKeys); + + return keys; + } + + private ByteBuffer rawValue(V value) { + return serializationContext.value().write(value); + } + + private V readValue(ByteBuffer buffer) { + return serializationContext.value().read(buffer); + } + + private Set readValueSet(Collection raw) { + + Set result = new LinkedHashSet<>(raw.size()); + + for (ByteBuffer buffer : raw) { + result.add(readValue(buffer)); + } + + return result; + } + + private Set> readTypedTupleSet(Collection raw) { + + Set> result = new LinkedHashSet<>(raw.size()); + + for (Tuple tuple : raw) { + result.add(new DefaultTypedTuple<>(readValue(ByteBuffer.wrap(tuple.getValue())), tuple.getScore())); + } + + return result; + } +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java new file mode 100644 index 000000000..c6eb50922 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java @@ -0,0 +1,230 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.reactivestreams.Publisher; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Metric; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; + +/** + * Reactive Redis operations for geo commands. + * + * @author Mark Paluch + * @see Redis Documentation: Geo Commands + * @since 2.0 + */ +public interface ReactiveGeoOperations { + + /** + * Add {@link Point} with given member {@literal name} to {@literal key}. + * + * @param key must not be {@literal null}. + * @param point must not be {@literal null}. + * @param member must not be {@literal null}. + * @return Number of elements added. + * @see Redis Documentation: GEOADD + */ + Mono geoAdd(K key, Point point, M member); + + /** + * Add {@link GeoLocation} to {@literal key}. + * + * @param key must not be {@literal null}. + * @param location must not be {@literal null}. + * @return Number of elements added. + * @see Redis Documentation: GEOADD + */ + Mono geoAdd(K key, GeoLocation location); + + /** + * Add {@link Map} of member / {@link Point} pairs to {@literal key}. + * + * @param key must not be {@literal null}. + * @param memberCoordinateMap must not be {@literal null}. + * @return Number of elements added. + * @see Redis Documentation: GEOADD + */ + Mono geoAdd(K key, Map memberCoordinateMap); + + /** + * Add {@link GeoLocation}s to {@literal key} + * + * @param key must not be {@literal null}. + * @param locations must not be {@literal null}. + * @return Number of elements added. + * @see Redis Documentation: GEOADD + */ + Mono geoAdd(K key, Iterable> locations); + + /** + * Add {@link GeoLocation}s to {@literal key} + * + * @param key must not be {@literal null}. + * @param locations must not be {@literal null}. + * @return Number of elements added. + * @see Redis Documentation: GEOADD + */ + Flux geoAdd(K key, Publisher>> locations); + + /** + * Get the {@link Distance} between {@literal member1} and {@literal member2}. + * + * @param key must not be {@literal null}. + * @param member1 must not be {@literal null}. + * @param member2 must not be {@literal null}. + * @return can be {@literal null}. + * @see Redis Documentation: GEODIST + */ + Mono geoDist(K key, M member1, M member2); + + /** + * Get the {@link Distance} between {@literal member1} and {@literal member2} in the given {@link Metric}. + * + * @param key must not be {@literal null}. + * @param member1 must not be {@literal null}. + * @param member2 must not be {@literal null}. + * @param metric must not be {@literal null}. + * @return can be {@literal null}. + * @see Redis Documentation: GEODIST + */ + Mono geoDist(K key, M member1, M member2, Metric metric); + + /** + * Get Geohash representation of the position for one or more {@literal member}s. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEOHASH + */ + Mono geoHash(K key, M member); + + /** + * Get Geohash representation of the position for one or more {@literal member}s. + * + * @param key must not be {@literal null}. + * @param members must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEOHASH + */ + Mono> geoHash(K key, M... members); + + /** + * Get the {@link Point} representation of positions for one or more {@literal member}s. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEOPOS + */ + Mono geoPos(K key, M member); + + /** + * Get the {@link Point} representation of positions for one or more {@literal member}s. + * + * @param key must not be {@literal null}. + * @param members must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEOPOS + */ + Mono> geoPos(K key, M... members); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEORADIUS + */ + Mono>> geoRadius(K key, Circle within); + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} applying {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param within must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEORADIUS + */ + Mono>> geoRadius(K key, Circle within, GeoRadiusCommandArgs args); + + /** + * Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given + * {@literal radius}. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @param radius + * @return never {@literal null}. + * @see Redis Documentation: GEORADIUSBYMEMBER + */ + Mono>> geoRadiusByMember(K key, M member, double radius); + + /** + * Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given + * {@literal radius} applying {@link Metric}. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @param distance must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEORADIUSBYMEMBER + */ + Mono>> geoRadiusByMember(K key, M member, Distance distance); + + /** + * Get the {@literal member}s within the circle defined by the {@literal members} coordinates and given + * {@literal radius} applying {@link Metric} and {@link GeoRadiusCommandArgs}. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @param distance must not be {@literal null}. + * @param args must not be {@literal null}. + * @return never {@literal null}. + * @see Redis Documentation: GEORADIUSBYMEMBER + */ + Mono>> geoRadiusByMember(K key, M member, Distance distance, GeoRadiusCommandArgs args); + + /** + * Remove the {@literal member}s. + * + * @param key must not be {@literal null}. + * @param members must not be {@literal null}. + * @return Number of elements removed. + */ + Mono geoRemove(K key, M... members); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java new file mode 100644 index 000000000..1196fb351 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java @@ -0,0 +1,153 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Redis map specific operations working on a hash. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveHashOperations { + + /** + * Delete given hash {@code hashKeys} from the hash at {@literal key}. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return + */ + Mono remove(H key, Object... hashKeys); + + /** + * Determine if given hash {@code hashKey} exists. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @return + */ + Mono hasKey(H key, Object hashKey); + + /** + * Get value for given {@code hashKey} from hash at {@code key}. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @return + */ + Mono get(H key, Object hashKey); + + /** + * Get values for given {@code hashKeys} from hash at {@code key}. + * + * @param key must not be {@literal null}. + * @param hashKeys must not be {@literal null}. + * @return + */ + Mono> multiGet(H key, Collection hashKeys); + + /** + * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @param delta + * @return + */ + Mono increment(H key, HK hashKey, long delta); + + /** + * Increment {@code value} of a hash {@code hashKey} by the given {@code delta}. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @param delta + * @return + */ + Mono increment(H key, HK hashKey, double delta); + + /** + * Get key set (fields) of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + Mono> keys(H key); + + /** + * Get size of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + Mono size(H key); + + /** + * Set multiple hash fields to multiple values using data provided in {@code m}. + * + * @param key must not be {@literal null}. + * @param map must not be {@literal null}. + */ + Mono putAll(H key, Map map); + + /** + * Set the {@code value} of a hash {@code hashKey}. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @param value + */ + Mono put(H key, HK hashKey, HV value); + + /** + * Set the {@code value} of a hash {@code hashKey} only if {@code hashKey} does not exist. + * + * @param key must not be {@literal null}. + * @param hashKey must not be {@literal null}. + * @param value + * @return + */ + Mono putIfAbsent(H key, HK hashKey, HV value); + + /** + * Get entry set (values) of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + Mono> values(H key); + + /** + * Get entire hash stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + Mono> entries(H key); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(H key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java new file mode 100644 index 000000000..ba34b9cb6 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java @@ -0,0 +1,59 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +/** + * Redis cardinality specific operations working on a HyperLogLog multiset. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveHyperLogLogOperations { + + /** + * Adds the given {@literal values} to the {@literal key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return 1 of at least one of the values was added to the key; 0 otherwise. + */ + Mono add(K key, V... values); + + /** + * Gets the current number of elements within the {@literal key}. + * + * @param keys must not be {@literal null} or {@literal empty}. + * @return + */ + Mono size(K... keys); + + /** + * Merges all values of given {@literal sourceKeys} into {@literal destination} key. + * + * @param destination key of HyperLogLog to move source keys into. + * @param sourceKeys must not be {@literal null} or {@literal empty}. + */ + Mono union(K destination, K... sourceKeys); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java new file mode 100644 index 000000000..fc4670325 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java @@ -0,0 +1,269 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; + +/** + * Redis list specific operations. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveListOperations { + + /** + * Get elements between {@code begin} and {@code end} from list at {@code key}. + * + * @param key must not be {@literal null}. + * @param start + * @param end + * @return + * @see Redis Documentation: LRANGE + */ + Mono> range(K key, long start, long end); + + /** + * Trim list at {@code key} to elements between {@code start} and {@code end}. + * + * @param key must not be {@literal null}. + * @param start + * @param end + * @see Redis Documentation: LTRIM + */ + Mono trim(K key, long start, long end); + + /** + * Get the size of list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: LLEN + */ + Mono size(K key); + + /** + * Prepend {@code value} to {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: LPUSH + */ + Mono leftPush(K key, V value); + + /** + * Prepend {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values + * @return + * @see Redis Documentation: LPUSH + */ + Mono leftPushAll(K key, V... values); + + /** + * Prepend {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + * @since 1.5 + * @see Redis Documentation: LPUSH + */ + Mono leftPushAll(K key, Collection values); + + /** + * Prepend {@code values} to {@code key} only if the list exists. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: LPUSHX + */ + Mono leftPushIfPresent(K key, V value); + + /** + * Prepend {@code values} to {@code key} before {@code value}. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: LPUSH + */ + Mono leftPush(K key, V pivot, V value); + + /** + * Append {@code value} to {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: RPUSH + */ + Mono rightPush(K key, V value); + + /** + * Append {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values + * @return + * @see Redis Documentation: RPUSH + */ + Mono rightPushAll(K key, V... values); + + /** + * Append {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values + * @return + * @since 1.5 + * @see Redis Documentation: RPUSH + */ + Mono rightPushAll(K key, Collection values); + + /** + * Append {@code values} to {@code key} only if the list exists. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: RPUSHX + */ + Mono rightPushIfPresent(K key, V value); + + /** + * Append {@code values} to {@code key} before {@code value}. + * + * @param key must not be {@literal null}. + * @param value + * @return + * @see Redis Documentation: RPUSH + */ + Mono rightPush(K key, V pivot, V value); + + /** + * Set the {@code value} list element at {@code index}. + * + * @param key must not be {@literal null}. + * @param index + * @param value + * @see Redis Documentation: LSET + */ + Mono set(K key, long index, V value); + + /** + * Removes the first {@code count} occurrences of {@code value} from the list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param count + * @param value + * @return + * @see Redis Documentation: LREM + */ + Mono remove(K key, long count, Object value); + + /** + * Get element at {@code index} form list at {@code key}. + * + * @param key must not be {@literal null}. + * @param index + * @return + * @see Redis Documentation: LINDEX + */ + Mono index(K key, long index); + + /** + * Removes and returns first element in list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: LPOP + */ + Mono leftPop(K key); + + /** + * Removes and returns first element from lists stored at {@code key}.
+ * Results return once an element available or {@code timeout} reached. + * + * @param key must not be {@literal null}. + * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either + * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be + * used to wait indefinitely. Durations between zero and one second are not supported. + * @return + * @see Redis Documentation: BLPOP + */ + Mono leftPop(K key, Duration timeout); + + /** + * Removes and returns last element in list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: RPOP + */ + Mono rightPop(K key); + + /** + * Removes and returns last element from lists stored at {@code key}.
+ * Results return once an element available or {@code timeout} reached. + * + * @param key must not be {@literal null}. + * @param timeout maximal duration to wait until an entry in the list at {@code key} is available. Must be either + * {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero can be + * used to wait indefinitely. Durations between zero and one second are not supported. + * @return + * @see Redis Documentation: BRPOP + */ + Mono rightPop(K key, Duration timeout); + + /** + * Remove the last element from list at {@code sourceKey}, append it to {@code destinationKey} and return its value. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @return + * @see Redis Documentation: RPOPLPUSH + */ + Mono rightPopAndLeftPush(K sourceKey, K destinationKey); + + /** + * Remove the last element from list at {@code srcKey}, append it to {@code dstKey} and return its value.
+ * Results return once an element available or {@code timeout} reached. + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param timeout maximal duration to wait until an entry in the list at {@code sourceKey} is available. Must be + * either {@link Duration#ZERO} or greater {@link 1 second}, must not be {@literal null}. A timeout of zero + * can be used to wait indefinitely. Durations between zero and one second are not supported. + * @return + * @see Redis Documentation: BRPOPLPUSH + */ + Mono rightPopAndLeftPush(K sourceKey, K destinationKey, Duration timeout); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java new file mode 100644 index 000000000..99ff1007f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java @@ -0,0 +1,49 @@ +/* + * 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.core; + +import org.reactivestreams.Publisher; +import org.springframework.dao.DataAccessException; +import org.springframework.data.redis.connection.ReactiveRedisConnection; + +/** + * Generic callback interface for code that operates on a low-level {@link ReactiveRedisConnection}. Allows to execute + * any number of operations on a single {@link ReactiveRedisConnection}, using any type and number of commands. + *

+ * This is particularly useful for delegating to existing data access code that expects a + * {@link ReactiveRedisConnection} to work on. For newly written code, it is strongly recommended to use + * {@link ReactiveRedisOperations}'s more specific operations. + * + * @param + * @author Mark Paluch + * @since 2.0 + * @see ReactiveRedisOperations#execute(ReactiveRedisCallback) + */ +public interface ReactiveRedisCallback { + + /** + * Gets called by {@link ReactiveRedisTemplate#execute(ReactiveRedisCallback)} with an active Redis connection. Does + * not need to care about activating or closing the {@link ReactiveRedisConnection}. + *

+ * Allows for returning a result object created within the callback, i.e. a domain object or a collection of domain + * objects. + * + * @param connection active Redis connection. + * @return a result object publisher + * @throws DataAccessException in case of custom exceptions + */ + Publisher doInRedis(ReactiveRedisConnection connection) throws DataAccessException; +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java new file mode 100644 index 000000000..002630d2d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java @@ -0,0 +1,296 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.time.Instant; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; + +/** + * Interface that specified a basic set of Redis operations, implemented by {@link ReactiveRedisTemplate}. Not often + * used but a useful option for extensibility and testability (as it can be easily mocked or stubbed). + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveRedisOperations { + + /** + * Executes the given action within a Redis connection. Application exceptions thrown by the action object get + * propagated to the caller (can only be unchecked) whenever possible. Redis exceptions are transformed into + * appropriate DAO ones. Allows for returning a result object, that is a domain object or a collection of domain + * objects. Performs automatic serialization/deserialization for the given objects to and from binary data suitable + * for the Redis storage. Note: Callback code is not supposed to handle transactions itself! Use an appropriate + * transaction manager. Generally, callback code must not touch any Connection lifecycle methods, like close, to let + * the template do its work. + * + * @param return type + * @param action callback object that specifies the Redis action + * @return a result object returned by the action or null + */ + Flux execute(ReactiveRedisCallback action); + + // ------------------------------------------------------------------------- + // Methods dealing with Redis Keys + // ------------------------------------------------------------------------- + + /** + * Determine if given {@code key} exists. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: EXISTS + */ + Mono hasKey(K key); + + /** + * Determine the type stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: TYPE + */ + Mono type(K key); + + /** + * Find all keys matching the given {@code pattern}. + * + * @param pattern must not be {@literal null}. + * @return + * @see Redis Documentation: KEYS + */ + Flux keys(K pattern); + + /** + * Return a random key from the keyspace. + * + * @return + * @see Redis Documentation: RANDOMKEY + */ + Mono randomKey(); + + /** + * Rename key {@code oldKey} to {@code newKey}. + * + * @param oldKey must not be {@literal null}. + * @param newKey must not be {@literal null}. + * @see Redis Documentation: RENAME + */ + Mono rename(K oldKey, K newKey); + + /** + * Rename key {@code oleName} to {@code newKey} only if {@code newKey} does not exist. + * + * @param oldKey must not be {@literal null}. + * @param newKey must not be {@literal null}. + * @return + * @see Redis Documentation: RENAMENX + */ + Mono renameIfAbsent(K oldKey, K newKey); + + /** + * Delete given {@code key}. + * + * @param key must not be {@literal null}. + * @return The number of keys that were removed. + * @see Redis Documentation: DEL + */ + Mono delete(K... key); + + /** + * Delete given {@code keys}. + * + * @param keys must not be {@literal null}. + * @return The number of keys that were removed. + * @see Redis Documentation: DEL + */ + Mono delete(Publisher keys); + + /** + * Set time to live for given {@code key}. + * + * @param key must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return + */ + Mono expire(K key, Duration timeout); + + /** + * Set the expiration for given {@code key} as a {@literal expireAt} timestamp. + * + * @param key must not be {@literal null}. + * @param expireAt must not be {@literal null}. + * @return + */ + Mono expireAt(K key, Instant expireAt); + + /** + * Remove the expiration from given {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: PERSIST + */ + Mono persist(K key); + + /** + * Move given {@code key} to database with {@code index}. + * + * @param key must not be {@literal null}. + * @param dbIndex + * @return + * @see Redis Documentation: MOVE + */ + Mono move(K key, int dbIndex); + + /** + * Get the time to live for {@code key}. + * + * @param key must not be {@literal null}. + * @return the {@link Duration} of the associated key. {@link Duration#ZERO} if no timeout associated or empty + * {@link Mono} if the key does not exist. + * @see Redis Documentation: PTTL + */ + Mono getExpire(K key); + + // ------------------------------------------------------------------------- + // Methods to obtain specific operations interface objects. + // ------------------------------------------------------------------------- + + // operation types + + /** + * Returns the operations performed on simple values (or Strings in Redis terminology). + * + * @return value operations + */ + ReactiveValueOperations opsForValue(); + + /** + * Returns the operations performed on simple values (or Strings in Redis terminology) given a + * {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return value operations. + */ + ReactiveValueOperations opsForValue(ReactiveSerializationContext serializationContext); + + /** + * Returns the operations performed on list values. + * + * @return list operations. + */ + ReactiveListOperations opsForList(); + + /** + * Returns the operations performed on list values given a {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return list operations. + */ + ReactiveListOperations opsForList(ReactiveSerializationContext serializationContext); + + /** + * Returns the operations performed on set values. + * + * @return set operations. + */ + ReactiveSetOperations opsForSet(); + + /** + * Returns the operations performed on set values given a {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return set operations. + */ + ReactiveSetOperations opsForSet(ReactiveSerializationContext serializationContext); + + /** + * Returns the operations performed on zset values (also known as sorted sets). + * + * @return zset operations. + */ + ReactiveZSetOperations opsForZSet(); + + /** + * Returns the operations performed on zset values (also known as sorted sets) given a + * {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return zset operations. + */ + ReactiveZSetOperations opsForZSet(ReactiveSerializationContext serializationContext); + + /** + * Returns the operations performed on multisets using HyperLogLog. + * + * @return never {@literal null}. + */ + ReactiveHyperLogLogOperations opsForHyperLogLog(); + + /** + * Returns the operations performed on multisets using HyperLogLog given a {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return never {@literal null}. + */ + ReactiveHyperLogLogOperations opsForHyperLogLog(ReactiveSerializationContext serializationContext); + + /** + * Returns the operations performed on hash values. + * + * @param hash key (or field) type. + * @param hash value type. + * @return hash operations. + */ + ReactiveHashOperations opsForHash(); + + /** + * Returns the operations performed on hash values given a {@link ReactiveSerializationContext}. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @param hash key (or field) type. + * @param hash value type. + * @return hash operations. + */ + ReactiveHashOperations opsForHash(ReactiveSerializationContext serializationContext); + + /** + * Returns geospatial specific operations interface. + * + * @return geospatial specific operations. + */ + ReactiveGeoOperations opsForGeo(); + + /** + * Returns geospatial specific operations interface. + * + * @param serializationContext serializers to be used with the returned operations, must not be {@literal null}. + * @return geospatial specific operations. + */ + ReactiveGeoOperations opsForGeo(ReactiveSerializationContext serializationContext); + + /** + * @return the {@link ReactiveSerializationContext}. + */ + ReactiveSerializationContext getSerializationContext(); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java new file mode 100644 index 000000000..924614ae2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java @@ -0,0 +1,957 @@ +/* + * 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.core; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.lang.reflect.Proxy; +import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.beans.factory.BeanClassLoaderAware; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * Central abstraction for reactive Redis data access. + *

+ * Performs automatic serialization/deserialization between the given objects and the underlying binary data in the + * Redis store. By default, it uses Java serialization for its objects (through {@link JdkSerializationRedisSerializer} + * ). + *

+ * Once configured, this class is thread-safe. + *

+ * Note that while the template is generified, it is up to the serializers/deserializers to properly convert the given + * Objects to and from binary data. + * + * @author Mark Paluch + * @since 2.0 + * @param the Redis key type against which the template works (usually a String) + * @param the Redis value type against which the template works + */ +public class ReactiveRedisTemplate + implements BeanClassLoaderAware, InitializingBean, ReactiveRedisOperations { + + private ReactiveRedisConnectionFactory connectionFactory; + private boolean exposeConnection = true; + private boolean initialized = false; + private boolean enableDefaultSerializer = true; + private RedisSerializer defaultSerializer; + private ClassLoader classLoader; + private ReactiveSerializationContextSupport serializationContext = new MutableReactiveSerializationContext<>(); + + // cache singleton objects (where possible) + private ReactiveValueOperations valueOps; + private ReactiveListOperations listOps; + private ReactiveSetOperations setOps; + private ReactiveZSetOperations zSetOps; + private ReactiveHyperLogLogOperations hyperLogLogOps; + private ReactiveGeoOperations geoOps; + + /** + * Construct a new {@link ReactiveRedisTemplate} instance. + */ + public ReactiveRedisTemplate() {} + + /** + * Returns the connectionFactory. + * + * @return Returns the connectionFactory + */ + public ReactiveRedisConnectionFactory getConnectionFactory() { + return connectionFactory; + } + + /** + * Sets the connection factory. + * + * @param connectionFactory The connectionFactory to set. + */ + public void setConnectionFactory(ReactiveRedisConnectionFactory connectionFactory) { + this.connectionFactory = connectionFactory; + } + + /** + * @param enableDefaultSerializer Whether or not the default serializer should be used. If not, any serializers not + * explicilty set will remain null and values will not be serialized or deserialized. + */ + public void setEnableDefaultSerializer(boolean enableDefaultSerializer) { + this.enableDefaultSerializer = enableDefaultSerializer; + } + + /** + * Returns the default serializer used by this template. + * + * @return template default serializer + */ + public RedisSerializer getDefaultSerializer() { + return defaultSerializer; + } + + /** + * Sets the default serializer to use for this template. All serializers (expect the + * {@link #setStringSerializer(RedisSerializer)}) are initialized to this value unless explicitly set. Defaults to + * {@link JdkSerializationRedisSerializer}. + * + * @param serializer default serializer to use + */ + public void setDefaultSerializer(RedisSerializer serializer) { + this.defaultSerializer = serializer; + } + + /** + * Returns the key serializer used by this template. + * + * @return the key serializer used by this template. + */ + public RedisSerializer getKeySerializer() { + return serializationContext.getKeySerializer(); + } + + /** + * Sets the key serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param serializer the key serializer to be used by this template. + */ + @SuppressWarnings("unchecked") + public void setKeySerializer(RedisSerializer serializer) { + getMutableSerializationContext().setKeySerializer((RedisSerializer) serializer); + } + + /** + * Returns the value serializer used by this template. + * + * @return the value serializer used by this template. + */ + public RedisSerializer getValueSerializer() { + return serializationContext.getValueSerializer(); + } + + /** + * Sets the value serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param serializer the value serializer to be used by this template. + */ + @SuppressWarnings("unchecked") + public void setValueSerializer(RedisSerializer serializer) { + getMutableSerializationContext().setValueSerializer((RedisSerializer) serializer); + } + + /** + * Returns the hashKeySerializer. + * + * @return Returns the hashKeySerializer + */ + public RedisSerializer getHashKeySerializer() { + return serializationContext.getHashKeySerializer(); + } + + /** + * Sets the hash key (or field) serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param hashKeySerializer The hashKeySerializer to set. + */ + public void setHashKeySerializer(RedisSerializer hashKeySerializer) { + getMutableSerializationContext().setHashKeySerializer(hashKeySerializer); + } + + /** + * Returns the hashValueSerializer. + * + * @return Returns the hashValueSerializer + */ + public RedisSerializer getHashValueSerializer() { + return serializationContext.getHashValueSerializer(); + } + + /** + * Sets the hash value serializer to be used by this template. Defaults to {@link #getDefaultSerializer()}. + * + * @param hashValueSerializer The hashValueSerializer to set. + */ + public void setHashValueSerializer(RedisSerializer hashValueSerializer) { + getMutableSerializationContext().setHashValueSerializer(hashValueSerializer); + } + + /** + * Returns the stringSerializer. + * + * @return Returns the stringSerializer + */ + public RedisSerializer getStringSerializer() { + return serializationContext.getStringSerializer(); + } + + /** + * Sets the string value serializer to be used by this template (when the arguments or return types are always + * strings). Defaults to {@link StringRedisSerializer}. + * + * @param stringSerializer The stringValueSerializer to set. + * @see ValueOperations#get(Object, long, long) + */ + public void setStringSerializer(RedisSerializer stringSerializer) { + getMutableSerializationContext().setStringSerializer(stringSerializer); + } + + /** + * Set the {@link ClassLoader} to be used for the default {@link JdkSerializationRedisSerializer} in case no other + * {@link RedisSerializer} is explicitly set as the default one. + * + * @param classLoader can be {@literal null}. + * @see org.springframework.beans.factory.BeanClassLoaderAware#setBeanClassLoader + */ + @Override + public void setBeanClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + + /** + * Initializes the properties and creates and sets an immutable {@link ReactiveSerializationContext}. Serializers + * cannot be changed after properties are initialized. + */ + @Override + public void afterPropertiesSet() { + + boolean defaultUsed = false; + + if (this.defaultSerializer == null) { + + this.defaultSerializer = new JdkSerializationRedisSerializer( + this.classLoader != null ? this.classLoader : this.getClass().getClassLoader()); + } + + if (this.enableDefaultSerializer) { + + if (this.serializationContext.getKeySerializer() == null) { + setKeySerializer(this.defaultSerializer); + defaultUsed = true; + } + + if (this.serializationContext.getValueSerializer() == null) { + setValueSerializer(this.defaultSerializer); + defaultUsed = true; + } + + if (this.serializationContext.getHashKeySerializer() == null) { + setHashKeySerializer(this.defaultSerializer); + defaultUsed = true; + } + + if (this.serializationContext.getHashValueSerializer() == null) { + setHashValueSerializer(this.defaultSerializer); + defaultUsed = true; + } + } + + if (this.enableDefaultSerializer && defaultUsed) { + Assert.notNull(this.defaultSerializer, "Default serializer is null and not all serializers initialized"); + } + + this.serializationContext = new ImmutableReactiveSerializationContext(serializationContext); + this.initialized = true; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForValue() + */ + @Override + public ReactiveValueOperations opsForValue() { + + if (valueOps == null) { + valueOps = opsForValue(serializationContext); + } + + return valueOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForValue(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveValueOperations opsForValue( + ReactiveSerializationContext serializationContext) { + return new DefaultReactiveValueOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForList() + */ + @Override + public ReactiveListOperations opsForList() { + + if (listOps == null) { + listOps = opsForList(serializationContext); + } + + return listOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForList(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveListOperations opsForList(ReactiveSerializationContext serializationContext) { + return new DefaultReactiveListOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForSet() + */ + public ReactiveSetOperations opsForSet() { + + if (setOps == null) { + setOps = opsForSet(serializationContext); + } + + return setOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForSet(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveSetOperations opsForSet(ReactiveSerializationContext serializationContext) { + return new DefaultReactiveSetOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForZSet() + */ + public ReactiveZSetOperations opsForZSet() { + + if (zSetOps == null) { + zSetOps = opsForZSet(serializationContext); + } + + return zSetOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForZSet(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveZSetOperations opsForZSet(ReactiveSerializationContext serializationContext) { + return new DefaultReactiveZSetOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHyperLogLog() + */ + @Override + public ReactiveHyperLogLogOperations opsForHyperLogLog() { + + if (hyperLogLogOps == null) { + hyperLogLogOps = opsForHyperLogLog(serializationContext); + } + + return hyperLogLogOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHyperLogLog(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveHyperLogLogOperations opsForHyperLogLog( + ReactiveSerializationContext serializationContext) { + return new DefaultReactiveHyperLogLogOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHash() + */ + @Override + public ReactiveHashOperations opsForHash() { + return opsForHash(serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForHash(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveHashOperations opsForHash( + ReactiveSerializationContext serializationContext) { + return new DefaultReactiveHashOperations<>(this, serializationContext); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForGeo() + */ + @Override + public ReactiveGeoOperations opsForGeo() { + + if (geoOps == null) { + geoOps = opsForGeo(serializationContext); + } + + return geoOps; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#opsForGeo(org.springframework.data.redis.serializer.ReactiveSerializationContext) + */ + @Override + public ReactiveGeoOperations opsForGeo(ReactiveSerializationContext serializationContext) { + return new DefaultReactiveGeoOperations<>(this, serializationContext); + } + + // ------------------------------------------------------------------------- + // Execution methods + // ------------------------------------------------------------------------- + + public Flux execute(ReactiveRedisCallback action) { + return execute(action, exposeConnection); + } + + /** + * Executes the given action object within a connection that can be exposed or not. Additionally, the connection can + * be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios). + * + * @param return type + * @param action callback object to execute + * @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code + * @return object returned by the action + */ + public Flux execute(ReactiveRedisCallback action, boolean exposeConnection) { + + Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it"); + Assert.notNull(action, "Callback object must not be null"); + + ReactiveRedisConnectionFactory factory = getConnectionFactory(); + ReactiveRedisConnection conn = factory.getReactiveConnection(); + + try { + + ReactiveRedisConnection connToUse = preProcessConnection(conn, false); + + ReactiveRedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse)); + Publisher result = action.doInRedis(connToExpose); + + return Flux.from(postProcessResult(result, connToUse, false)); + } finally { + conn.close(); + } + } + + /** + * Create a reusable Flux for a {@link ReactiveRedisCallback}. Callback is executed within a connection context. The + * connection is released outside the callback. + * + * @param callback must not be {@literal null} + * @return a {@link Flux} wrapping the {@link ReactiveRedisCallback}. + */ + public Flux createFlux(ReactiveRedisCallback callback) { + + Assert.notNull(callback, "ReactiveRedisCallback must not be null!"); + + return Flux.defer(() -> doInConnection(callback, exposeConnection)); + } + + /** + * Create a reusable Mono for a {@link ReactiveRedisCallback}. Callback is executed within a connection context. The + * connection is released outside the callback. + * + * @param callback must not be {@literal null} + * @return a {@link Mono} wrapping the {@link ReactiveRedisCallback}. + */ + public Mono createMono(final ReactiveRedisCallback callback) { + + Assert.notNull(callback, "ReactiveRedisCallback must not be null!"); + + return Mono.defer(() -> Mono.from(doInConnection(callback, exposeConnection))); + } + + /** + * Executes the given action object within a connection that can be exposed or not. Additionally, the connection can + * be pipelined. Note the results of the pipeline are discarded (making it suitable for write-only scenarios). + * + * @param return type + * @param action callback object to execute + * @param exposeConnection whether to enforce exposure of the native Redis Connection to callback code + * @return object returned by the action + */ + private Publisher doInConnection(ReactiveRedisCallback action, boolean exposeConnection) { + + Assert.isTrue(initialized, "template not initialized; call afterPropertiesSet() before using it"); + Assert.notNull(action, "Callback object must not be null"); + + ReactiveRedisConnectionFactory factory = getConnectionFactory(); + ReactiveRedisConnection conn = factory.getReactiveConnection(); + + ReactiveRedisConnection connToUse = preProcessConnection(conn, false); + + ReactiveRedisConnection connToExpose = (exposeConnection ? connToUse : createRedisConnectionProxy(connToUse)); + Publisher result = action.doInRedis(connToExpose); + + return Flux.from(postProcessResult(result, connToUse, false)).doAfterTerminate(conn::close); + } + + // ------------------------------------------------------------------------- + // Methods dealing with Redis keys + // ------------------------------------------------------------------------- + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#hasKey(java.lang.Object) + */ + public Mono hasKey(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.keyCommands().exists(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#type(java.lang.Object) + */ + public Mono type(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.keyCommands().type(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#keys(java.lang.Object) + */ + public Flux keys(K pattern) { + + Assert.notNull(pattern, "Pattern must not be null!"); + + return createFlux(connection -> connection.keyCommands().keys(rawKey(pattern))) // + .flatMap(Flux::fromIterable) // + .map(this::readKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#randomKey() + */ + public Mono randomKey() { + return createMono(connection -> connection.keyCommands().randomKey()).map(this::readKey); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#rename(java.lang.Object, java.lang.Object) + */ + public Mono rename(K oldKey, K newKey) { + + Assert.notNull(oldKey, "Old key must not be null!"); + Assert.notNull(newKey, "New Key must not be null!"); + + return createMono(connection -> connection.keyCommands().rename(rawKey(oldKey), rawKey(newKey))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#renameIfAbsent(java.lang.Object, java.lang.Object) + */ + public Mono renameIfAbsent(K oldKey, K newKey) { + + Assert.notNull(oldKey, "Old key must not be null!"); + Assert.notNull(newKey, "New Key must not be null!"); + + return createMono(connection -> connection.keyCommands().renameNX(rawKey(oldKey), rawKey(newKey))); + + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#delete(java.lang.Object[]) + */ + @SafeVarargs + public final Mono delete(K... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + Assert.notEmpty(keys, "Keys must not be empty!"); + Assert.noNullElements(keys, "Keys must not contain null elements!"); + + if (keys.length == 1) { + return createMono(connection -> connection.keyCommands().del(rawKey(keys[0]))); + } + + Mono> listOfKeys = Flux.fromArray(keys).map(this::rawKey).collectList(); + return createMono(connection -> listOfKeys.flatMap(rawKeys -> connection.keyCommands().mDel(rawKeys))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#delete(org.reactivestreams.Publisher) + */ + public Mono delete(Publisher keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + return createMono(connection -> connection.keyCommands() // + .del(Flux.from(keys).map(this::rawKey).map(KeyCommand::new)) // + .map(CommandResponse::getOutput)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#expire(java.lang.Object, java.time.Duration) + */ + @Override + public Mono expire(K key, Duration timeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(timeout, "Timeout must not be null!"); + + if (timeout.getNano() == 0) { + return createMono(connection -> connection.keyCommands() // + .expire(rawKey(key), timeout)); + } + + return createMono(connection -> connection.keyCommands().pExpire(rawKey(key), timeout)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#expireAt(java.lang.Object, java.time.Instant) + */ + @Override + public Mono expireAt(K key, Instant expireAt) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(expireAt, "Expire at must not be null!"); + + if (expireAt.getNano() == 0) { + return createMono(connection -> connection.keyCommands() // + .expireAt(rawKey(key), expireAt)); + } + + return createMono(connection -> connection.keyCommands().pExpireAt(rawKey(key), expireAt)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#persist(java.lang.Object) + */ + @Override + public Mono persist(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.keyCommands().persist(rawKey(key))); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#getExpire(java.lang.Object) + */ + @Override + public Mono getExpire(K key) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.keyCommands().pTtl(rawKey(key)).flatMap(expiry -> { + + if (expiry == -1) { + return Mono.just(Duration.ZERO); + } + + if (expiry == -2) { + return Mono.empty(); + } + + return Mono.just(Duration.ofMillis(expiry)); + })); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#move(java.lang.Object, int) + */ + @Override + public Mono move(K key, int dbIndex) { + + Assert.notNull(key, "Key must not be null!"); + + return createMono(connection -> connection.keyCommands().move(rawKey(key), dbIndex)); + } + + // ------------------------------------------------------------------------- + // Implementation hooks and helper methods + // ------------------------------------------------------------------------- + + /** + * Processes the connection (before any settings are executed on it). Default implementation returns the connection as + * is. + * + * @param connection must not be {@literal null}. + * @param existingConnection + */ + protected ReactiveRedisConnection preProcessConnection(ReactiveRedisConnection connection, + boolean existingConnection) { + return connection; + } + + /** + * Processes the result before returning the {@link Publisher}. Default implementation returns the result as is. + * + * @param result must not be {@literal null}. + * @param connection must not be {@literal null}. + * @param existingConnection + * @return + */ + protected Publisher postProcessResult(Publisher result, ReactiveRedisConnection connection, + boolean existingConnection) { + return result; + } + + protected ReactiveRedisConnection createRedisConnectionProxy(ReactiveRedisConnection reactiveRedisConnection) { + + Class[] ifcs = ClassUtils.getAllInterfacesForClass(reactiveRedisConnection.getClass(), + getClass().getClassLoader()); + return (ReactiveRedisConnection) Proxy.newProxyInstance(reactiveRedisConnection.getClass().getClassLoader(), ifcs, + new CloseSuppressingInvocationHandler(reactiveRedisConnection)); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.core.ReactiveRedisOperations#serialization() + */ + @Override + public ReactiveSerializationContext getSerializationContext() { + return serializationContext; + } + + @SuppressWarnings("unchecked") + private MutableReactiveSerializationContext getMutableSerializationContext() { + + Assert.state(serializationContext instanceof MutableReactiveSerializationContext, + () -> String.format("Client configuration must be instance of MutableReactiveSerializationContext but is %s", + ClassUtils.getShortName(serializationContext.getClass()))); + + return (MutableReactiveSerializationContext) serializationContext; + } + + private ByteBuffer rawKey(K key) { + return getSerializationContext().key().getWriter().write(key); + } + + private K readKey(ByteBuffer buffer) { + return getSerializationContext().key().getReader().read(buffer); + } + + /** + * @author Mark Paluch + */ + static abstract class ReactiveSerializationContextSupport implements ReactiveSerializationContext { + + @Override + public abstract SerializationTuple key(); + + @Override + public abstract SerializationTuple value(); + + @Override + public abstract SerializationTuple string(); + + @Override + public abstract SerializationTuple hashKey(); + + @Override + public abstract SerializationTuple hashValue(); + + public abstract RedisSerializer getKeySerializer(); + + public abstract RedisSerializer getValueSerializer(); + + public abstract RedisSerializer getHashKeySerializer(); + + public abstract RedisSerializer getHashValueSerializer(); + + public abstract RedisSerializer getStringSerializer(); + } + + /** + * @author Mark Paluch + */ + static class ImmutableReactiveSerializationContext extends ReactiveSerializationContextSupport { + + private RedisSerializer keySerializer; + private final SerializationTuple keyTuple; + + private RedisSerializer valueSerializer; + private final SerializationTuple valueTuple; + + private RedisSerializer hashKeySerializer; + private final SerializationTuple hashKeyTuple; + + private RedisSerializer hashValueSerializer; + private final SerializationTuple hashValueTuple; + + private RedisSerializer stringSerializer; + private final SerializationTuple stringTuple; + + public ImmutableReactiveSerializationContext(ReactiveSerializationContextSupport context) { + + keySerializer = context.getKeySerializer(); + keyTuple = context.key(); + valueSerializer = context.getValueSerializer(); + valueTuple = context.value(); + hashKeySerializer = context.getHashKeySerializer(); + hashKeyTuple = context.hashKey(); + hashValueSerializer = context.getHashValueSerializer(); + hashValueTuple = context.hashValue(); + stringSerializer = context.getStringSerializer(); + stringTuple = context.string(); + } + + @Override + public SerializationTuple key() { + return keyTuple; + } + + @Override + public SerializationTuple value() { + return valueTuple; + } + + @Override + public SerializationTuple string() { + return stringTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashKey() { + return (SerializationTuple) hashKeyTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashValue() { + return (SerializationTuple) hashValueTuple; + } + + public RedisSerializer getKeySerializer() { + return keySerializer; + } + + public RedisSerializer getValueSerializer() { + return valueSerializer; + } + + public RedisSerializer getHashKeySerializer() { + return hashKeySerializer; + } + + public RedisSerializer getHashValueSerializer() { + return hashValueSerializer; + } + + public RedisSerializer getStringSerializer() { + return stringSerializer; + } + } + + /** + * @author Mark Paluch + */ + static class MutableReactiveSerializationContext extends ReactiveSerializationContextSupport { + + private RedisSerializer keySerializer; + private SerializationTuple keyTuple = SerializationTuple.raw(); + + private RedisSerializer valueSerializer; + private SerializationTuple valueTuple = SerializationTuple.raw(); + + private RedisSerializer hashKeySerializer; + private SerializationTuple hashKeyTuple = SerializationTuple.raw(); + + private RedisSerializer hashValueSerializer; + private SerializationTuple hashValueTuple = SerializationTuple.raw(); + + private RedisSerializer stringSerializer = new StringRedisSerializer(); + private SerializationTuple stringTuple = SerializationTuple.fromSerializer(stringSerializer); + + @Override + public SerializationTuple key() { + return keyTuple; + } + + @Override + public SerializationTuple value() { + return valueTuple; + } + + @Override + public SerializationTuple string() { + return stringTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashKey() { + return (SerializationTuple) hashKeyTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashValue() { + return (SerializationTuple) hashValueTuple; + } + + public RedisSerializer getKeySerializer() { + return keySerializer; + } + + public void setKeySerializer(RedisSerializer keySerializer) { + this.keySerializer = keySerializer; + this.keyTuple = SerializationTuple.fromSerializer(keySerializer); + } + + public RedisSerializer getValueSerializer() { + return valueSerializer; + } + + public void setValueSerializer(RedisSerializer valueSerializer) { + this.valueSerializer = valueSerializer; + this.valueTuple = SerializationTuple.fromSerializer(valueSerializer); + } + + public RedisSerializer getHashKeySerializer() { + return hashKeySerializer; + } + + public void setHashKeySerializer(RedisSerializer hashKeySerializer) { + this.hashKeySerializer = hashKeySerializer; + this.hashKeyTuple = SerializationTuple.fromSerializer(hashKeySerializer); + } + + public RedisSerializer getHashValueSerializer() { + return hashValueSerializer; + } + + public void setHashValueSerializer(RedisSerializer hashValueSerializer) { + this.hashValueSerializer = hashValueSerializer; + this.hashValueTuple = SerializationTuple.fromSerializer(hashValueSerializer); + } + + public RedisSerializer getStringSerializer() { + return stringSerializer; + } + + public void setStringSerializer(RedisSerializer stringSerializer) { + this.stringSerializer = stringSerializer; + this.stringTuple = SerializationTuple.fromSerializer(stringSerializer); + } + } +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java new file mode 100644 index 000000000..d378eae07 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java @@ -0,0 +1,261 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.List; +import java.util.Set; + +/** + * Redis set specific operations. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveSetOperations { + + /** + * Add given {@code values} to set at {@code key}. + * + * @param key must not be {@literal null}. + * @param values + * @return + * @see Redis Documentation: SADD + */ + Mono add(K key, V... values); + + /** + * Remove given {@code values} from set at {@code key} and return the number of removed elements. + * + * @param key must not be {@literal null}. + * @param values + * @return + * @see Redis Documentation: SREM + */ + Mono remove(K key, Object... values); + + /** + * Remove and return a random member from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: SPOP + */ + Mono pop(K key); + + /** + * Move {@code value} from {@code key} to {@code destKey} + * + * @param sourceKey must not be {@literal null}. + * @param value + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SMOVE + */ + Mono move(K sourceKey, V value, K destKey); + + /** + * Get size of set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: SCARD + */ + Mono size(K key); + + /** + * Check if set at {@code key} contains {@code value}. + * + * @param key must not be {@literal null}. + * @param o + * @return + * @see Redis Documentation: SISMEMBER + */ + Mono isMember(K key, Object o); + + /** + * Returns the members intersecting all given sets at {@code key} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @return + * @see Redis Documentation: SINTER + */ + Mono> intersect(K key, K otherKey); + + /** + * Returns the members intersecting all given sets at {@code key} and {@code otherKeys}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @return + * @see Redis Documentation: SINTER + */ + Mono> intersect(K key, Collection otherKeys); + + /** + * Intersect all given sets at {@code key} and {@code otherKey} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SINTERSTORE + */ + Mono intersectAndStore(K key, K otherKey, K destKey); + + /** + * Intersect all given sets at {@code key} and {@code otherKeys} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SINTERSTORE + */ + Mono intersectAndStore(K key, Collection otherKeys, K destKey); + + /** + * Union all sets at given {@code keys} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @return + * @see Redis Documentation: SUNION + */ + Mono> union(K key, K otherKey); + + /** + * Union all sets at given {@code keys} and {@code otherKeys}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @return + * @see Redis Documentation: SUNION + */ + Mono> union(K key, Collection otherKeys); + + /** + * Union all sets at given {@code key} and {@code otherKey} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SUNIONSTORE + */ + Mono unionAndStore(K key, K otherKey, K destKey); + + /** + * Union all sets at given {@code key} and {@code otherKeys} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SUNIONSTORE + */ + Mono unionAndStore(K key, Collection otherKeys, K destKey); + + /** + * Diff all sets for given {@code key} and {@code otherKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @return + * @see Redis Documentation: SDIFF + */ + Mono> difference(K key, K otherKey); + + /** + * Diff all sets for given {@code key} and {@code otherKeys}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @return + * @see Redis Documentation: SDIFF + */ + Mono> difference(K key, Collection otherKeys); + + /** + * Diff all sets for given {@code key} and {@code otherKey} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SDIFFSTORE + */ + Mono differenceAndStore(K key, K otherKey, K destKey); + + /** + * Diff all sets for given {@code key} and {@code otherKeys} and store result in {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: SDIFFSTORE + */ + Mono differenceAndStore(K key, Collection otherKeys, K destKey); + + /** + * Get all elements of set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: SMEMBERS + */ + Mono> members(K key); + + /** + * Get random element from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + * @see Redis Documentation: SRANDMEMBER + */ + Mono randomMember(K key); + + /** + * Get {@code count} distinct random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count + * @return + * @see Redis Documentation: SRANDMEMBER + */ + Mono> distinctRandomMembers(K key, long count); + + /** + * Get {@code count} random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count + * @return + * @see Redis Documentation: SRANDMEMBER + */ + Mono> randomMembers(K key, long count); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java new file mode 100644 index 000000000..788587fb0 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java @@ -0,0 +1,173 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +import java.time.Duration; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Reactive Redis operations for simple (or in Redis terminology 'string') values. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveValueOperations { + + /** + * Set {@code value} for {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @see Redis Documentation: SET + */ + Mono set(K key, V value); + + /** + * Set the {@code value} and expiration {@code timeout} for {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @param timeout must not be {@literal null}. + * @see Redis Documentation: SETEX + */ + Mono set(K key, V value, Duration timeout); + + /** + * Set {@code key} to hold the string {@code value} if {@code key} is absent. + * + * @param key must not be {@literal null}. + * @param value + * @see Redis Documentation: SETNX + */ + Mono setIfAbsent(K key, V value); + + /** + * Set {@code key} to hold the string {@code value} if {@code key} is present. + * + * @param key must not be {@literal null}. + * @param value + * @see Redis Documentation: SET + */ + Mono setIfPresent(K key, V value); + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code tuple}. + * + * @param map must not be {@literal null}. + * @see Redis Documentation: MSET + */ + Mono multiSet(Map map); + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code tuple} only if the provided key does + * not exist. + * + * @param map must not be {@literal null}. + * @see Redis Documentation: MSET + */ + Mono multiSetIfAbsent(Map map); + + /** + * Get the value of {@code key}. + * + * @param key must not be {@literal null}. + * @see Redis Documentation: GET + */ + Mono get(Object key); + + /** + * Set {@code value} of {@code key} and return its old value. + * + * @param key must not be {@literal null}. + * @see Redis Documentation: GETSET + */ + Mono getAndSet(K key, V value); + + /** + * Get multiple {@code keys}. Values are returned in the order of the requested keys. + * + * @param keys must not be {@literal null}. + * @see Redis Documentation: MGET + */ + Mono> multiGet(Collection keys); + + /** + * Append a {@code value} to {@code key}. + * + * @param key must not be {@literal null}. + * @param value + * @see Redis Documentation: APPEND + */ + Mono append(K key, String value); + + /** + * Get a substring of value of {@code key} between {@code begin} and {@code end}. + * + * @param key must not be {@literal null}. + * @param start + * @param end + * @see Redis Documentation: GETRANGE + */ + Mono get(K key, long start, long end); + + /** + * Overwrite parts of {@code key} starting at the specified {@code offset} with given {@code value}. + * + * @param key must not be {@literal null}. + * @param value + * @param offset + * @see Redis Documentation: SETRANGE + */ + Mono set(K key, V value, long offset); + + /** + * Get the length of the value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @see Redis Documentation: STRLEN + */ + Mono size(K key); + + /** + * Sets the bit at {@code offset} in value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param offset + * @param value + * @see Redis Documentation: SETBIT + */ + Mono setBit(K key, long offset, boolean value); + + /** + * « Get the bit value at {@code offset} of value at {@code key}. + * + * @param key must not be {@literal null}. + * @param offset + * @see Redis Documentation: GETBIT + */ + Mono getBit(K key, long offset); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java new file mode 100644 index 000000000..148c13ee1 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java @@ -0,0 +1,383 @@ +/* + * 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.core; + +import reactor.core.publisher.Mono; + +import java.util.Collection; +import java.util.Set; + +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.RedisZSetCommands.Limit; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.data.redis.core.ZSetOperations.TypedTuple; + +/** + * Redis ZSet/sorted set specific operations. + * + * @author Mark Paluch + * @since 2.0 + */ +public interface ReactiveZSetOperations { + + /** + * Add {@code value} to a sorted set at {@code key}, or update its {@code score} if it already exists. + * + * @param key must not be {@literal null}. + * @param score the score. + * @param value the value. + * @return + * @see Redis Documentation: ZADD + */ + Mono add(K key, V value, double score); + + /** + * Add {@code tuples} to a sorted set at {@code key}, or update their score if it already exists. + * + * @param key must not be {@literal null}. + * @param tuples the score. + * @return + * @see Redis Documentation: ZADD + */ + Mono addAll(K key, Collection> tuples); + + /** + * Add {@code tuples} to a sorted set at {@code key}, or update its {@code score} if it already exists. + * + * @param key must not be {@literal null}. + * @param tuples must not be {@literal null}. + * @return + * @see Redis Documentation: ZADD + */ + // TODO + // Mono add(K key, Set> tuples); + + /** + * Remove {@code values} from sorted set. Return number of removed elements. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + * @see Redis Documentation: ZREM + */ + Mono remove(K key, Object... values); + + /** + * Increment the score of element with {@code value} in sorted set by {@code increment}. + * + * @param key must not be {@literal null}. + * @param delta + * @param value the value. + * @return + * @see Redis Documentation: ZINCRBY + */ + Mono incrementScore(K key, V value, double delta); + + /** + * Determine the index of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return + * @see Redis Documentation: ZRANK + */ + Mono rank(K key, Object o); + + /** + * Determine the index of element with {@code value} in a sorted set when scored high to low. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return + * @see Redis Documentation: ZREVRANK + */ + Mono reverseRank(K key, Object o); + + /** + * Get elements between {@code start} and {@code end} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANGE + */ + Mono> range(K key, Range range); + + /** + * Get set of {@link Tuple}s between {@code start} and {@code end} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANGE + */ + Mono>> rangeWithScores(K key, Range range); + + /** + * Get elements where score is between {@code min} and {@code max} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANGEBYSCORE + */ + Mono> rangeByScore(K key, Range range); + + /** + * Get set of {@link Tuple}s where score is between {@code min} and {@code max} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + * @see Redis Documentation: ZRANGEBYSCORE + */ + Mono>> rangeByScoreWithScores(K key, Range range); + + /** + * Get elements in range from {@code start} to {@code end} where score is between {@code min} and {@code max} from + * sorted set. + * + * @param key must not be {@literal null}. + * @param range + * @param limit + * @return + * @see Redis Documentation: ZRANGEBYSCORE + */ + Mono> rangeByScore(K key, Range range, Limit limit); + + /** + * Get set of {@link Tuple}s in range from {@code start} to {@code end} where score is between {@code min} and + * {@code max} from sorted set. + * + * @param key + * @param range + * @param limit + * @return + * @see Redis Documentation: ZRANGEBYSCORE + */ + Mono>> rangeByScoreWithScores(K key, Range range, Limit limit); + + /** + * Get elements in range from {@code start} to {@code end} from sorted set ordered from high to low. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREVRANGE + */ + Mono> reverseRange(K key, Range range); + + /** + * Get set of {@link Tuple}s in range from {@code start} to {@code end} from sorted set ordered from high to low. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREVRANGE + */ + Mono>> reverseRangeWithScores(K key, Range range); + + /** + * Get elements where score is between {@code min} and {@code max} from sorted set ordered from high to low. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREVRANGE + */ + Mono> reverseRangeByScore(K key, Range range); + + /** + * Get set of {@link Tuple} where score is between {@code min} and {@code max} from sorted set ordered from high to + * low. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREVRANGEBYSCORE + */ + Mono>> reverseRangeByScoreWithScores(K key, Range range); + + /** + * Get elements in range from {@code start} to {@code end} where score is between {@code min} and {@code max} from + * sorted set ordered high -> low. + * + * @param key must not be {@literal null}. + * @param range + * @param limit + * @return + * @see Redis Documentation: ZREVRANGEBYSCORE + */ + Mono> reverseRangeByScore(K key, Range range, Limit limit); + + /** + * Get set of {@link Tuple} in range from {@code start} to {@code end} where score is between {@code min} and + * {@code max} from sorted set ordered high -> low. + * + * @param key must not be {@literal null}. + * @param range + * @param limit + * @return + * @see Redis Documentation: ZREVRANGEBYSCORE + */ + Mono>> reverseRangeByScoreWithScores(K key, Range range, Limit limit); + + /** + * Count number of elements within sorted set with scores between {@code min} and {@code max}. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZCOUNT + */ + Mono count(K key, Range range); + + /** + * Returns the number of elements of the sorted set stored with given {@code key}. + * + * @param key + * @return + * @see Redis Documentation: ZCARD + */ + Mono size(K key); + + /** + * Get the score of element with {@code value} from sorted set with key {@code key}. + * + * @param key must not be {@literal null}. + * @param o the value. + * @return + * @see Redis Documentation: ZREM + */ + Mono score(K key, Object o); + + /** + * Remove elements in range between {@code start} and {@code end} from sorted set with {@code key}. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREMRANGEBYRANK + */ + Mono removeRange(K key, Range range); + + /** + * Remove elements with scores between {@code min} and {@code max} from sorted set with {@code key}. + * + * @param key must not be {@literal null}. + * @param range + * @return + * @see Redis Documentation: ZREMRANGEBYSCORE + */ + Mono removeRangeByScore(K key, Range range); + + /** + * Union sorted sets at {@code key} and {@code otherKeys} and store result in destination {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: ZUNIONSTORE + */ + Mono unionAndStore(K key, K otherKey, K destKey); + + /** + * Union sorted sets at {@code key} and {@code otherKeys} and store result in destination {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: ZUNIONSTORE + */ + Mono unionAndStore(K key, Collection otherKeys, K destKey); + + /** + * Intersect sorted sets at {@code key} and {@code otherKey} and store result in destination {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKey must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: ZINTERSTORE + */ + Mono intersectAndStore(K key, K otherKey, K destKey); + + /** + * Intersect sorted sets at {@code key} and {@code otherKeys} and store result in destination {@code destKey}. + * + * @param key must not be {@literal null}. + * @param otherKeys must not be {@literal null}. + * @param destKey must not be {@literal null}. + * @return + * @see Redis Documentation: ZINTERSTORE + */ + Mono intersectAndStore(K key, Collection otherKeys, K destKey); + + /** + * Get all elements with lexicographical ordering from {@literal ZSET} at {@code key} with a value between + * {@link Range#getLowerBound()} and {@link Range#getUpperBound()}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @see Redis Documentation: ZRANGEBYLEX + */ + Mono> rangeByLex(K key, Range range); + + /** + * Get all elements {@literal n} elements, where {@literal n = } {@link Limit#getCount()}, starting at + * {@link Limit#getOffset()} with lexicographical ordering from {@literal ZSET} at {@code key} with a value between + * {@link Range#getLowerBound()} and {@link Range#getUpperBound()}. + * + * @param key must not be {@literal null} + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + * @see Redis Documentation: ZRANGEBYLEX + */ + Mono> rangeByLex(K key, Range range, Limit limit); + + /** + * Get all elements with reverse lexicographical ordering from {@literal ZSET} at {@code key} with a value between + * {@link Range#getLowerBound()} and {@link Range#getUpperBound()}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @see Redis Documentation: ZREVRANGEBYLEX + */ + Mono> reverseRangeByLex(K key, Range range); + + /** + * Get all elements {@literal n} elements, where {@literal n = } {@link Limit#getCount()}, starting at + * {@link Limit#getOffset()} with reverse lexicographical ordering from {@literal ZSET} at {@code key} with a value + * between {@link Range#getLowerBound()} and {@link Range#getUpperBound()}. + * + * @param key must not be {@literal null} + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + * @see Redis Documentation: ZREVRANGEBYLEX + */ + Mono> reverseRangeByLex(K key, Range range, Limit limit); + + /** + * Removes the given {@literal key}. + * + * @param key must not be {@literal null}. + */ + Mono delete(K key); +} diff --git a/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java index 02d6cf06b..dd026fb18 100644 --- a/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java +++ b/src/main/java/org/springframework/data/redis/core/convert/KeyspaceConfiguration.java @@ -21,6 +21,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.TimeToLive; +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -28,7 +29,7 @@ import org.springframework.util.ClassUtils; * {@link KeyspaceConfiguration} allows programmatic setup of keyspaces and time to live options for certain types. This * is suitable for cases where there is no option to use the equivalent {@link RedisHash} or {@link TimeToLive} * annotations. - * + * * @author Christoph Strobl * @since 1.7 */ @@ -44,9 +45,36 @@ public class KeyspaceConfiguration { } } + @EnableRedisRepositories(keyspaceConfiguration = MyKeyspaceConfiguration.class) + static class MyConfig { + + } + + static class MyKeyspaceConfiguration extends KeyspaceConfiguration { + + @Override + protected Iterable initialConfiguration() { + return super.initialConfiguration(); + } + + @Override + public boolean hasSettingsFor(Class type) { + return true; + } + + @Override + public KeyspaceSettings getKeyspaceSettings(Class type) { + + KeyspaceSettings keyspaceSettings = new KeyspaceSettings(type, "my-keyspace"); + keyspaceSettings.setTimeToLive(3600L); + + return keyspaceSettings; + } + } + /** * Check if specific {@link KeyspaceSettings} are available for given type. - * + * * @param type must not be {@literal null}. * @return true if settings exist. */ @@ -78,7 +106,7 @@ public class KeyspaceConfiguration { /** * Get the {@link KeyspaceSettings} for given type. - * + * * @param type must not be {@literal null} * @return {@literal null} if no settings configured. */ @@ -98,7 +126,7 @@ public class KeyspaceConfiguration { /** * Customization hook. - * + * * @return must not return {@literal null}. */ protected Iterable initialConfiguration() { @@ -107,7 +135,7 @@ public class KeyspaceConfiguration { /** * Add {@link KeyspaceSettings} for type. - * + * * @param keyspaceSettings must not be {@literal null}. */ public void addKeyspaceSettings(KeyspaceSettings keyspaceSettings) { @@ -171,7 +199,7 @@ public class KeyspaceConfiguration { /** * Marker class indicating no settings defined. - * + * * @author Christoph Strobl * @since 1.7 */ 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 index d6471b0d9..69e748b85 100644 --- a/src/main/java/org/springframework/data/redis/core/types/Expiration.java +++ b/src/main/java/org/springframework/data/redis/core/types/Expiration.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 the original author or authors. + * Copyright 2016-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. @@ -15,6 +15,7 @@ */ package org.springframework.data.redis.core.types; +import java.time.Duration; import java.util.concurrent.TimeUnit; import org.springframework.util.Assert; @@ -22,7 +23,7 @@ import org.springframework.util.ObjectUtils; /** * Expiration holds a value with its associated {@link TimeUnit}. - * + * * @author Christoph Strobl * @author Mark Paluch * @since 1.7 @@ -34,7 +35,7 @@ public class Expiration { /** * Creates new {@link Expiration}. - * + * * @param expirationTime can be {@literal null}. Defaulted to {@link TimeUnit#SECONDS} * @param timeUnit */ @@ -46,7 +47,7 @@ public class Expiration { /** * Get the expiration time converted into {@link TimeUnit#MILLISECONDS}. - * + * * @return */ public long getExpirationTimeInMilliseconds() { @@ -55,7 +56,7 @@ public class Expiration { /** * Get the expiration time converted into {@link TimeUnit#SECONDS}. - * + * * @return */ public long getExpirationTimeInSeconds() { @@ -64,7 +65,7 @@ public class Expiration { /** * Get the expiration time. - * + * * @return */ public long getExpirationTime() { @@ -73,7 +74,7 @@ public class Expiration { /** * Get the time unit for the expiration time. - * + * * @return */ public TimeUnit getTimeUnit() { @@ -82,7 +83,7 @@ public class Expiration { /** * Get the expiration time converted into the desired {@code targetTimeUnit}. - * + * * @param targetTimeUnit must not {@literal null}. * @return * @throws IllegalArgumentException @@ -95,7 +96,7 @@ public class Expiration { /** * Creates new {@link Expiration} with {@link TimeUnit#SECONDS}. - * + * * @param expirationTime * @return */ @@ -105,7 +106,7 @@ public class Expiration { /** * Creates new {@link Expiration} with {@link TimeUnit#MILLISECONDS}. - * + * * @param expirationTime * @return */ @@ -116,8 +117,8 @@ public class Expiration { /** * Creates new {@link Expiration} with the provided {@link TimeUnit}. Greater units than {@link TimeUnit#SECONDS} are * converted to {@link TimeUnit#SECONDS}. Units smaller than {@link TimeUnit#MILLISECONDS} are converted to - * {@link TimeUnit#MILLISECONDS} and can lose precision since {@link TimeUnit#MILLISECONDS} is the smallest granularity - * supported by Redis. + * {@link TimeUnit#MILLISECONDS} and can lose precision since {@link TimeUnit#MILLISECONDS} is the smallest + * granularity supported by Redis. * * @param expirationTime * @param timeUnit can be {@literal null}. Defaulted to {@link TimeUnit#SECONDS} @@ -138,9 +139,29 @@ public class Expiration { return new Expiration(expirationTime, TimeUnit.SECONDS); } + /** + * Creates new {@link Expiration} with the provided {@link java.time.Duration}. Durations with at least + * {@link TimeUnit#SECONDS} resolution use seconds, durations using milliseconds use {@link TimeUnit#MILLISECONDS} + * resolution. + * + * @param duration must not be {@literal null}. + * @return + * @since 2.0 + */ + public static Expiration from(Duration duration) { + + Assert.notNull(duration, "Duration must not be null!"); + + if (duration.toMillis() % 1000 == 0) { + return new Expiration(duration.getSeconds(), TimeUnit.SECONDS); + } + + return new Expiration(duration.toMillis(), TimeUnit.MILLISECONDS); + } + /** * Creates new persistent {@link Expiration}. - * + * * @return */ public static Expiration persistent() { diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultReactiveSerializationContextBuilder.java b/src/main/java/org/springframework/data/redis/serializer/DefaultReactiveSerializationContextBuilder.java new file mode 100644 index 000000000..81c8932e6 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultReactiveSerializationContextBuilder.java @@ -0,0 +1,204 @@ +/* + * 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.serializer; + +import org.springframework.data.redis.serializer.ReactiveSerializationContext.ReactiveSerializationContextBuilder; +import org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link ReactiveSerializationContextBuilder}. + * + * @author Mark Paluch + * @since 2.0 + */ +public class DefaultReactiveSerializationContextBuilder implements ReactiveSerializationContextBuilder { + + private SerializationTuple keyTuple; + + private SerializationTuple valueTuple; + + private SerializationTuple hashKeyTuple; + + private SerializationTuple hashValueTuple; + + private SerializationTuple stringTuple = SerializationTuple.fromSerializer(new StringRedisSerializer()); + + @Override + public ReactiveSerializationContextBuilder key(SerializationTuple tuple) { + + Assert.notNull(tuple, "SerializationTuple must not be null!"); + + this.keyTuple = tuple; + + return this; + } + + @Override + public ReactiveSerializationContextBuilder key(RedisElementReader reader, RedisElementWriter writer) { + return key(SerializationTuple.just(reader, writer)); + } + + @Override + public ReactiveSerializationContextBuilder key(RedisSerializer serializer) { + return key(SerializationTuple.fromSerializer(serializer)); + } + + @Override + public ReactiveSerializationContextBuilder value(SerializationTuple tuple) { + + Assert.notNull(tuple, "SerializationTuple must not be null!"); + + this.valueTuple = tuple; + + return this; + } + + @Override + public ReactiveSerializationContextBuilder value(RedisElementReader reader, RedisElementWriter writer) { + return value(SerializationTuple.just(reader, writer)); + } + + @Override + public ReactiveSerializationContextBuilder value(RedisSerializer serializer) { + return value(SerializationTuple.fromSerializer(serializer)); + } + + @Override + public ReactiveSerializationContextBuilder hashKey(SerializationTuple tuple) { + + Assert.notNull(tuple, "SerializationTuple must not be null!"); + + this.hashKeyTuple = tuple; + + return this; + } + + @Override + public ReactiveSerializationContextBuilder hashKey(RedisElementReader reader, RedisElementWriter writer) { + return hashKey(SerializationTuple.just(reader, writer)); + } + + @Override + public ReactiveSerializationContextBuilder hashKey(RedisSerializer serializer) { + return hashKey(SerializationTuple.fromSerializer(serializer)); + } + + @Override + public ReactiveSerializationContextBuilder hashValue(SerializationTuple tuple) { + + Assert.notNull(tuple, "SerializationTuple must not be null!"); + + this.hashValueTuple = tuple; + + return this; + } + + @Override + public ReactiveSerializationContextBuilder hashValue(RedisElementReader reader, + RedisElementWriter writer) { + return hashValue(SerializationTuple.just(reader, writer)); + } + + @Override + public ReactiveSerializationContextBuilder hashValue(RedisSerializer serializer) { + return hashValue(SerializationTuple.fromSerializer(serializer)); + } + + @Override + public ReactiveSerializationContextBuilder string(SerializationTuple tuple) { + + Assert.notNull(tuple, "SerializationTuple must not be null!"); + + this.hashValueTuple = tuple; + + return this; + } + + @Override + public ReactiveSerializationContextBuilder string(RedisElementReader reader, + RedisElementWriter writer) { + return string(SerializationTuple.just(reader, writer)); + } + + @Override + public ReactiveSerializationContextBuilder string(RedisSerializer serializer) { + return string(SerializationTuple.fromSerializer(serializer)); + } + + @Override + public ReactiveSerializationContext build() { + + Assert.notNull(keyTuple, "Key SerializationTuple must not be null!"); + Assert.notNull(valueTuple, "Value SerializationTuple must not be null!"); + Assert.notNull(hashKeyTuple, "HashKey SerializationTuple must not be null!"); + Assert.notNull(hashValueTuple, "ValueKey SerializationTuple must not be null!"); + + return new DefaultReactiveSerializationContext(keyTuple, valueTuple, hashKeyTuple, hashValueTuple, + stringTuple); + } + + static class DefaultReactiveSerializationContext implements ReactiveSerializationContext { + + private final SerializationTuple keyTuple; + + private final SerializationTuple valueTuple; + + private final SerializationTuple hashKeyTuple; + + private final SerializationTuple hashValueTuple; + + private final SerializationTuple stringTuple; + + public DefaultReactiveSerializationContext(SerializationTuple keyTuple, SerializationTuple valueTuple, + SerializationTuple hashKeyTuple, SerializationTuple hashValueTuple, + SerializationTuple stringTuple) { + + this.keyTuple = keyTuple; + this.valueTuple = valueTuple; + this.hashKeyTuple = hashKeyTuple; + this.hashValueTuple = hashValueTuple; + this.stringTuple = stringTuple; + } + + @Override + public SerializationTuple key() { + return keyTuple; + } + + @Override + public SerializationTuple value() { + return valueTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashKey() { + return (SerializationTuple) hashKeyTuple; + } + + @Override + @SuppressWarnings("unchecked") + public SerializationTuple hashValue() { + return (SerializationTuple) hashValueTuple; + } + + @Override + public SerializationTuple string() { + return stringTuple; + } + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java new file mode 100644 index 000000000..c75879a88 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java @@ -0,0 +1,49 @@ +/* + * 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.serializer; + +import lombok.RequiredArgsConstructor; + +import java.nio.ByteBuffer; + +/** + * Default implementation of {@link RedisElementReader}. + * + * @author Mark Paluch + * @since 2.0 + */ +@RequiredArgsConstructor +class DefaultRedisElementReader implements RedisElementReader { + + private final RedisSerializer serializer; + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisElementReader#read(java.nio.ByteBuffer) + */ + @Override + @SuppressWarnings("unchecked") + public T read(ByteBuffer buffer) { + + if (serializer == null) { + return (T) buffer; + } + + byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + + return serializer.deserialize(bytes); + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java new file mode 100644 index 000000000..49fa19e0f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java @@ -0,0 +1,54 @@ +/* + * 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.serializer; + +import lombok.RequiredArgsConstructor; + +import java.nio.ByteBuffer; + +/** + * Default implementation of {@link RedisElementWriter}. + * + * @author Mark Paluch + * @since 2.0 + */ +@RequiredArgsConstructor +class DefaultRedisElementWriter implements RedisElementWriter { + + private final RedisSerializer serializer; + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.RedisElementWriter#write(java.lang.Object) + */ + @Override + public ByteBuffer write(T value) { + + if (serializer == null) { + + if (value instanceof byte[]) { + return ByteBuffer.wrap((byte[]) value); + } + + if (value instanceof ByteBuffer) { + return (ByteBuffer) value; + } + + throw new IllegalStateException("Cannot serialize value without a serializer"); + } + + return ByteBuffer.wrap(serializer.serialize((T) value)); + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationTuple.java b/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationTuple.java new file mode 100644 index 000000000..23c6e5cdd --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/DefaultSerializationTuple.java @@ -0,0 +1,48 @@ +/* + * 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.serializer; + +import org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple; + +/** + * Default implementation of {@link SerializationTuple}. + * + * @author Mark Paluch + * @since 2.0 + */ +class DefaultSerializationTuple implements SerializationTuple { + + private final RedisElementReader reader; + + private final RedisElementWriter writer; + + @SuppressWarnings("unchecked") + protected DefaultSerializationTuple(RedisElementReader reader, RedisElementWriter writer) { + + this.reader = (RedisElementReader) reader; + this.writer = (RedisElementWriter) writer; + } + + @Override + public RedisElementReader getReader() { + return reader; + } + + @Override + public RedisElementWriter getWriter() { + return writer; + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/ReactiveSerializationContext.java b/src/main/java/org/springframework/data/redis/serializer/ReactiveSerializationContext.java new file mode 100644 index 000000000..67e964c10 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/ReactiveSerializationContext.java @@ -0,0 +1,285 @@ +/* + * 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.serializer; + +import java.nio.ByteBuffer; + +import org.springframework.util.Assert; + +/** + * Serialization context for reactive use. + *

+ * This context provides {@link SerializationTuple}s for key, value, hash-key (field), hash-value and {@link String} + * serialization and deserialization. + * + * @author Mark Paluch + * @since 2.0 + * @see RedisElementWriter + * @see RedisElementReader + */ +public interface ReactiveSerializationContext { + + /** + * Creates a new {@link ReactiveSerializationContextBuilder}. + * + * @param expected key type. + * @param expected value type. + * @return a new {@link ReactiveSerializationContextBuilder}. + */ + static ReactiveSerializationContextBuilder builder() { + return new DefaultReactiveSerializationContextBuilder<>(); + } + + /** + * @return {@link SerializationTuple} for key-typed serialization and deserialization. + */ + SerializationTuple key(); + + /** + * @return {@link SerializationTuple} for value-typed serialization and deserialization. + */ + SerializationTuple value(); + + /** + * @return {@link SerializationTuple} for hash-key-typed serialization and deserialization. + */ + SerializationTuple hashKey(); + + /** + * @return {@link SerializationTuple} for hash-value-typed serialization and deserialization. + */ + SerializationTuple hashValue(); + + /** + * @return {@link SerializationTuple} for {@link String}-typed serialization and deserialization. + */ + SerializationTuple string(); + + /** + * Typed serialization tuple. + */ + interface SerializationTuple { + + /** + * Creates a {@link SerializationTuple} adapter given {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return a {@link SerializationTuple} adapter for {@link RedisSerializer}. + */ + static SerializationTuple fromSerializer(RedisSerializer serializer) { + + Assert.notNull(serializer, "RedisSerializer must not be null!"); + + return new RedisSerializerTupleAdapter(serializer); + } + + /** + * Creates a {@link SerializationTuple} adapter given {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return a {@link SerializationTuple} encapsulating {@link RedisElementReader} and {@link RedisElementWriter}. + */ + static SerializationTuple just(RedisElementReader reader, + RedisElementWriter writer) { + + Assert.notNull(reader, "RedisElementReader must not be null!"); + Assert.notNull(writer, "RedisElementWriter must not be null!"); + + return new DefaultSerializationTuple<>(reader, writer); + } + + /** + * Creates a pass-thru {@link SerializationTuple} to pass-thru {@link ByteBuffer} objects. + * + * @return a pass-thru {@link SerializationTuple}. + */ + static SerializationTuple raw() { + return RedisSerializerTupleAdapter.raw(); + } + + /** + * @return the {@link RedisElementReader}. + */ + RedisElementReader getReader(); + + /** + * Deserialize a {@link ByteBuffer} into the according type. + * + * @param buffer must not be {@literal null}. + * @return the deserialized value. + */ + default T read(ByteBuffer buffer) { + return getReader().read(buffer); + } + + /** + * @return the {@link RedisElementWriter}. + */ + RedisElementWriter getWriter(); + + /** + * Serialize a {@code element} to its {@link ByteBuffer} representation. + * + * @param element + * @return the {@link ByteBuffer} representing {@code element} in its binary form. + */ + default ByteBuffer write(T element) { + return getWriter().write(element); + } + } + + /** + * Builder for {@link ReactiveSerializationContext}. + */ + interface ReactiveSerializationContextBuilder { + + /** + * Set the key {@link SerializationTuple}. + * + * @param tuple must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder key(SerializationTuple tuple); + + /** + * Set the key {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder key(RedisElementReader reader, RedisElementWriter writer); + + /** + * Set the key {@link SerializationTuple} given a {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder key(RedisSerializer serializer); + + /** + * Set the value {@link SerializationTuple}. + * + * @param tuple must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder value(SerializationTuple tuple); + + /** + * Set the value {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder value(RedisElementReader reader, RedisElementWriter writer); + + /** + * Set the value {@link SerializationTuple} given a {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder value(RedisSerializer serializer); + + /** + * Set the hash key {@link SerializationTuple}. + * + * @param tuple must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashKey(SerializationTuple tuple); + + /** + * Set the hash key {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashKey(RedisElementReader reader, + RedisElementWriter writer); + + /** + * Set the hash key {@link SerializationTuple} given a {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashKey(RedisSerializer serializer); + + /** + * Set the hash value {@link SerializationTuple}. + * + * @param tuple must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashValue(SerializationTuple tuple); + + /** + * Set the hash value {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashValue(RedisElementReader reader, + RedisElementWriter writer); + + /** + * Set the hash value {@link SerializationTuple} given a {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder hashValue(RedisSerializer serializer); + + /** + * Set the string {@link SerializationTuple}. + * + * @param tuple must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder string(SerializationTuple tuple); + + /** + * Set the string {@link RedisElementReader} and {@link RedisElementWriter}. + * + * @param reader must not be {@literal null}. + * @param writer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder string(RedisElementReader reader, + RedisElementWriter writer); + + /** + * Set the string {@link SerializationTuple} given a {@link RedisSerializer}. + * + * @param serializer must not be {@literal null}. + * @return {@literal this} builder. + */ + ReactiveSerializationContextBuilder string(RedisSerializer serializer); + + /** + * Builds a {@link ReactiveSerializationContext}. + * + * @return the {@link ReactiveSerializationContext}. + */ + ReactiveSerializationContext build(); + } +} diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java b/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java new file mode 100644 index 000000000..6bb5fc197 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java @@ -0,0 +1,37 @@ +/* + * 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.serializer; + +import java.nio.ByteBuffer; + +/** + * Strategy interface that specifies a deserializer that can deserialize a binary element representation stored in Redis + * into an object. + * + * @author Mark Paluch + * @since 2.0 + */ +@FunctionalInterface +public interface RedisElementReader { + + /** + * Deserialize a {@link ByteBuffer} into the according type. + * + * @param buffer must not be {@literal null}. + * @return the deserialized value. + */ + T read(ByteBuffer buffer); +} diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java b/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java new file mode 100644 index 000000000..82a7f85c8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java @@ -0,0 +1,36 @@ +/* + * 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.serializer; + +import java.nio.ByteBuffer; + +/** + * Strategy interface that specifies a serializer that can serialize an element to its binary representation to be used + * as Redis protocol payload. + * + * @author Mark Paluch + */ +@FunctionalInterface +public interface RedisElementWriter { + + /** + * Serialize a {@code element} to its {@link ByteBuffer} representation. + * + * @param element + * @return the {@link ByteBuffer} representing {@code element} in its binary form. + */ + ByteBuffer write(T element); +} diff --git a/src/main/java/org/springframework/data/redis/serializer/RedisSerializerTupleAdapter.java b/src/main/java/org/springframework/data/redis/serializer/RedisSerializerTupleAdapter.java new file mode 100644 index 000000000..1e34ec2a1 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/serializer/RedisSerializerTupleAdapter.java @@ -0,0 +1,63 @@ +/* + * 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.serializer; + +import org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple; + +/** + * Adapter to delegate serialization/deserialization to {@link RedisSerializer}. + * + * @author Mark Paluch + * @since 2.0 + */ +class RedisSerializerTupleAdapter implements SerializationTuple { + + private final static RedisSerializerTupleAdapter RAW = new RedisSerializerTupleAdapter<>(null); + + private final RedisElementReader reader; + private final RedisElementWriter writer; + + protected RedisSerializerTupleAdapter(RedisSerializer serializer) { + + reader = new DefaultRedisElementReader<>(serializer); + writer = new DefaultRedisElementWriter<>(serializer); + } + + @SuppressWarnings("unchecked") + public static SerializationTuple raw() { + return (SerializationTuple) RAW; + } + + public static SerializationTuple from(RedisSerializer redisSerializer) { + return new RedisSerializerTupleAdapter<>(redisSerializer); + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple#reader() + */ + @Override + public RedisElementReader getReader() { + return reader; + } + + /* (non-Javadoc) + * @see org.springframework.data.redis.serializer.ReactiveSerializationContext.SerializationTuple#writer() + */ + @Override + public RedisElementWriter getWriter() { + return writer; + } +} diff --git a/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java b/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java new file mode 100644 index 000000000..13c671b6a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java @@ -0,0 +1,31 @@ +/* + * 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; + +import java.nio.ByteBuffer; +import java.util.UUID; + +/** + * Implementation of {@link ObjectFactory} that returns random {@link ByteBuffer}s. + * + * @author Mark Paluch + */ +public class ByteBufferObjectFactory implements ObjectFactory { + + public ByteBuffer instance() { + return ByteBuffer.wrap(UUID.randomUUID().toString().getBytes()); + } +} diff --git a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java index de6722d18..b0dfbf83a 100644 --- a/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java +++ b/src/test/java/org/springframework/data/redis/ConnectionFactoryTracker.java @@ -1,12 +1,12 @@ /* * Copyright 2011-2013 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. @@ -24,23 +24,29 @@ import org.springframework.data.redis.connection.RedisConnectionFactory; /** * Basic utility to help with the destruction of {@link RedisConnectionFactory} inside JUnit 4 tests. Simply add the * factory during setup and then call {@link #cleanUp()} through the @AfterClass method. - * + * * @author Costin Leau */ public abstract class ConnectionFactoryTracker { - private static Set connFactories = new LinkedHashSet(); + private static Set connFactories = new LinkedHashSet(); public static void add(RedisConnectionFactory factory) { connFactories.add(factory); } + public static void add(Object factory) { + connFactories.add(factory); + } + public static void cleanUp() { if (connFactories != null) { - for (RedisConnectionFactory connectionFactory : connFactories) { + for (Object connectionFactory : connFactories) { try { - ((DisposableBean) connectionFactory).destroy(); - // System.out.println("Succesfully cleaned up factory " + connectionFactory); + if (connectionFactory instanceof DisposableBean) { + ((DisposableBean) connectionFactory).destroy(); + // System.out.println("Succesfully cleaned up factory " + connectionFactory); + } } catch (Exception ex) { System.err.println("Cannot clean factory " + connectionFactory + ex); } diff --git a/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java b/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java new file mode 100644 index 000000000..c4ec40439 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java @@ -0,0 +1,36 @@ +/* + * 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; + +/** + * @author Mark Paluch + */ +public class PrefixStringObjectFactory implements ObjectFactory { + + private final String prefix; + private final ObjectFactory delegate; + + public PrefixStringObjectFactory(String prefix, ObjectFactory delegate) { + + this.prefix = prefix; + this.delegate = delegate; + } + + @Override + public String instance() { + return prefix.concat(delegate.instance()); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java index c1c6435da..76c732781 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java @@ -15,11 +15,18 @@ */ package org.springframework.data.redis.connection.lettuce; -import static org.hamcrest.CoreMatchers.*; -import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.Matchers.*; import static org.junit.Assert.*; +import static org.junit.Assume.*; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.StepVerifier; +import reactor.test.TestSubscriber; import java.nio.ByteBuffer; +import java.time.Duration; +import java.time.Instant; import java.util.Arrays; import org.junit.Test; @@ -28,12 +35,13 @@ import org.springframework.data.redis.connection.DataType; import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; -import reactor.core.publisher.Flux; -import reactor.core.publisher.Mono; -import reactor.test.TestSubscriber; +import com.lambdaworks.redis.SetArgs; /** + * Integration tests for {@link LettuceReactiveKeyCommands}. + * * @author Christoph Strobl + * @author Mark Paluch */ public class LettuceReactiveKeyCommandsTests extends LettuceReactiveCommandsTestsBase { @@ -181,4 +189,112 @@ public class LettuceReactiveKeyCommandsTests extends LettuceReactiveCommandsTest subscriber.assertValueCount(2); } + @Test // DATAREDIS-602 + public void shouldExpireKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + StepVerifier.create(connection.keyCommands().expire(KEY_1_BBUFFER, Duration.ofSeconds(10))) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1), is(greaterThan(8L))); + } + + @Test // DATAREDIS-602 + public void shouldPreciseExpireKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + StepVerifier.create(connection.keyCommands().pExpire(KEY_1_BBUFFER, Duration.ofSeconds(10))) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1), is(greaterThan(8L))); + } + + @Test // DATAREDIS-602 + public void shouldExpireAtKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + Instant expireAt = Instant.now().plus(Duration.ofSeconds(10)); + + StepVerifier.create(connection.keyCommands().expireAt(KEY_1_BBUFFER, expireAt)) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1), is(greaterThan(8L))); + } + + @Test // DATAREDIS-602 + public void shouldPreciseExpireAtKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + Instant expireAt = Instant.now().plus(Duration.ofSeconds(10)); + + StepVerifier.create(connection.keyCommands().pExpireAt(KEY_1_BBUFFER, expireAt)) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1), is(greaterThan(8L))); + } + + @Test // DATAREDIS-602 + public void shouldReportTimeToLiveCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1, SetArgs.Builder.ex(10)); + + StepVerifier.create(connection.keyCommands().ttl(KEY_1_BBUFFER)) // + .expectNextMatches(actual -> { + assertThat(nativeCommands.ttl(KEY_1), is(greaterThan(8L))); + return true; + }) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void shouldReportPreciseTimeToLiveCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1, SetArgs.Builder.ex(10)); + + StepVerifier.create(connection.keyCommands().pTtl(KEY_1_BBUFFER)) // + .expectNextMatches(actual -> { + assertThat(actual, is(greaterThan(8000L))); + return true; + }) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void shouldPersist() { + + nativeCommands.set(KEY_1, VALUE_1, SetArgs.Builder.ex(10)); + + StepVerifier.create(connection.keyCommands().persist(KEY_1_BBUFFER)) // + .expectNext(true) // + .expectComplete() // + .verify(); + + assertThat(nativeCommands.ttl(KEY_1), is(-1L)); + } + + @Test // DATAREDIS-602 + public void shouldMoveToDatabase() { + + assumeThat(connection, is(not(instanceOf(LettuceReactiveRedisClusterConnection.class)))); + + nativeCommands.set(KEY_1, VALUE_1); + + StepVerifier.create(connection.keyCommands().move(KEY_1_BBUFFER, 5)) // + .expectNext(true) // + .expectComplete() // + .verify(); + assertThat(nativeCommands.exists(KEY_1), is(0L)); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java index 7736bba00..d49f265cc 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java @@ -17,7 +17,7 @@ package org.springframework.data.redis.connection.lettuce; import static org.hamcrest.core.Is.*; import static org.junit.Assert.*; -import static org.junit.Assume.assumeThat; +import static org.junit.Assume.*; import java.nio.ByteBuffer; import java.util.Arrays; @@ -200,7 +200,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 2D, VALUE_2); nativeCommands.zadd(KEY_1, 3D, VALUE_3); - assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(3D, 2D)).block(), + assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D)).block(), IsIterableContainingInOrder.contains(VALUE_3_BBUFFER, VALUE_2_BBUFFER)); } @@ -211,7 +211,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 2D, VALUE_2); nativeCommands.zadd(KEY_1, 3D, VALUE_3); - assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(3D, 2D, true, false)).block(), + assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), IsIterableContainingInOrder.contains(VALUE_3_BBUFFER)); } @@ -222,7 +222,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 2D, VALUE_2); nativeCommands.zadd(KEY_1, 3D, VALUE_3); - assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(3D, 2D, false, true)).block(), + assertThat(connection.zSetCommands().zRevRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), IsIterableContainingInOrder.contains(VALUE_2_BBUFFER)); } @@ -233,7 +233,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 2D, VALUE_2); nativeCommands.zadd(KEY_1, 3D, VALUE_3); - assertThat(connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(3D, 2D)).block(), + assertThat(connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D)).block(), IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_3_BBUFFER.array(), 3D), new DefaultTuple(VALUE_2_BBUFFER.array(), 2D))); } @@ -246,7 +246,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 3D, VALUE_3); assertThat( - connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(3D, 2D, true, false)).block(), + connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_3_BBUFFER.array(), 3D))); } @@ -258,7 +258,7 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 3D, VALUE_3); assertThat( - connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(3D, 2D, false, true)).block(), + connection.zSetCommands().zRevRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D))); } @@ -466,14 +466,14 @@ public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTes nativeCommands.zadd(KEY_1, 0D, "f"); nativeCommands.zadd(KEY_1, 0D, "g"); - assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("c", "")).block(), + assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("", "c")).block(), IsIterableContainingInOrder.contains(ByteBuffer.wrap("c".getBytes()), ByteBuffer.wrap("b".getBytes()), ByteBuffer.wrap("a".getBytes()))); - assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("c", "", false, true)).block(), + assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("", "c", true, false)).block(), IsIterableContainingInOrder.contains(ByteBuffer.wrap("b".getBytes()), ByteBuffer.wrap("a".getBytes()))); - assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("g", "aaa", false, true)).block(), + assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("aaa", "g", true, false)).block(), IsIterableContainingInOrder.contains(ByteBuffer.wrap("f".getBytes()), ByteBuffer.wrap("e".getBytes()), ByteBuffer.wrap("d".getBytes()), ByteBuffer.wrap("c".getBytes()), ByteBuffer.wrap("b".getBytes()))); } diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java new file mode 100644 index 000000000..561b0d0b0 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java @@ -0,0 +1,423 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.test.util.MinimumRedisVersionRule; +import org.springframework.test.annotation.IfProfileValue; + +/** + * Integration tests for {@link DefaultReactiveGeoOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@IfProfileValue(name = "redisVersion", value = "3.2.0+") +public class DefaultReactiveGeoOperationsIntegrationTests { + + public static @ClassRule MinimumRedisVersionRule versionRule = new MinimumRedisVersionRule(); + + private static final Point POINT_ARIGENTO = new Point(13.583333, 37.316667); + private static final Point POINT_CATANIA = new Point(15.087269, 37.502669); + private static final Point POINT_PALERMO = new Point(13.361389, 38.115556); + + private static final double DISTANCE_PALERMO_CATANIA_METERS = 166274.15156960033; + private static final double DISTANCE_PALERMO_CATANIA_KILOMETERS = 166.27415156960033; + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveGeoOperations geoOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveGeoOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.geoOperations = redisTemplate.opsForGeo(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void geoAdd() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(geoOperations.geoAdd(key, POINT_PALERMO, value)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoAddLocation() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(geoOperations.geoAdd(key, new GeoLocation<>(value, POINT_PALERMO))) // + .expectNext(1L) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoAddMapOfLocations() { + + K key = keyFactory.instance(); + + Map memberCoordinateMap = new HashMap<>(); + memberCoordinateMap.put(valueFactory.instance(), POINT_PALERMO); + memberCoordinateMap.put(valueFactory.instance(), POINT_CATANIA); + + StepVerifier.create(geoOperations.geoAdd(key, memberCoordinateMap)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoAddIterableOfLocations() { + + K key = keyFactory.instance(); + + List> geoLocations = Arrays.asList(new GeoLocation<>(valueFactory.instance(), POINT_ARIGENTO), + new GeoLocation<>(valueFactory.instance(), POINT_PALERMO)); + + StepVerifier.create(geoOperations.geoAdd(key, geoLocations)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoAddPublisherOfLocations() { + + K key = keyFactory.instance(); + + List> batch1 = Arrays.asList(new GeoLocation<>(valueFactory.instance(), POINT_ARIGENTO), + new GeoLocation<>(valueFactory.instance(), POINT_PALERMO)); + + List> batch2 = Arrays.asList(new GeoLocation<>(valueFactory.instance(), POINT_CATANIA)); + + Flux>> geoLocations = Flux.just(batch1, batch2); + + StepVerifier.create(geoOperations.geoAdd(key, geoLocations)).expectNext(2L).expectNext(1L).expectComplete() + .verify(); + } + + @Test // DATAREDIS-602 + public void geoDistShouldReturnDistanceInMetersByDefault() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoDist(key, member1, member2)).consumeNextWith(actual -> { + + assertThat(actual.getValue()).isCloseTo(DISTANCE_PALERMO_CATANIA_METERS, offset(0.005)); + assertThat(actual.getUnit()).isEqualTo("m"); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoDistShouldReturnDistanceInKilometersCorrectly() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoDist(key, member1, member2, Metrics.KILOMETERS)).consumeNextWith(actual -> { + + assertThat(actual.getValue()).isCloseTo(DISTANCE_PALERMO_CATANIA_KILOMETERS, offset(0.005)); + assertThat(actual.getUnit()).isEqualTo("km"); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoHash() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, v1).block(); + + StepVerifier.create(geoOperations.geoHash(key, v1)) // + .expectNext("sqc8b49rny0") // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoHashShouldReturnMultipleElements() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, v1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, v2).block(); + + StepVerifier.create(geoOperations.geoHash(key, v1, v3, v2)) // + .expectNext(Arrays.asList("sqc8b49rny0", null, "sqdtr74hyu0")) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoPos() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, v1).block(); + + StepVerifier.create(geoOperations.geoPos(key, v1)) // + .consumeNextWith(actual -> { + + assertThat(actual.getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.005)); + assertThat(actual.getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.005)); + }).expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoPosShouldReturnMultipleElements() { + + K key = keyFactory.instance(); + V v1 = valueFactory.instance(); + V v2 = valueFactory.instance(); + V v3 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, v1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, v2).block(); + + StepVerifier.create(geoOperations.geoPos(key, v1, v3, v2)) // + .consumeNextWith(actual -> { + + assertThat(actual.get(0).getX()).isCloseTo(POINT_PALERMO.getX(), offset(0.005)); + assertThat(actual.get(0).getY()).isCloseTo(POINT_PALERMO.getY(), offset(0.005)); + + assertThat(actual.get(1)).isNull(); + assertThat(actual.get(2)).isNotNull(); + }).expectComplete() // + .verify(); + } + + @Test // DATAREDIS-438 + public void geoRadius() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoRadius(key, new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS)))) + .consumeNextWith(actual -> assertThat(actual).hasSize(2)).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoRadiusShouldReturnLocationsWithDistance() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoRadius(key, new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS)), + newGeoRadiusArgs().includeDistance().sortDescending())).consumeNextWith(actual -> { + assertThat(actual).hasSize(2); + + assertThat(actual.getContent().get(0).getDistance().getValue()).isCloseTo(190.4424d, offset(0.005)); + assertThat(actual.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(actual.getContent().get(1).getDistance().getValue()).isCloseTo(56.4413d, offset(0.005)); + assertThat(actual.getContent().get(1).getContent().getName()).isEqualTo(member2); + }).verifyComplete(); + } + + @Test // DATAREDIS-438 + public void geoRadiusByMemberShouldReturnMembersCorrectly() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoRadius(key, new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS)))) + .consumeNextWith(actual -> assertThat(actual).hasSize(2)) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoRadiusByMemberWithin100_000MetersShouldReturnLocations() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + V member3 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + geoOperations.geoAdd(key, POINT_ARIGENTO, member3).block(); + + StepVerifier.create(geoOperations.geoRadiusByMember(key, member3, 100_000)) // + .consumeNextWith(actual -> { + + assertThat(actual).hasSize(2); + + assertThat(actual.get(0).getName()).isEqualTo(member3); + assertThat(actual.get(1).getName()).isEqualTo(member1); + }) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void geoRadiusByMemberWithin100KMShouldReturnLocations() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + V member3 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + geoOperations.geoAdd(key, POINT_ARIGENTO, member3).block(); + + StepVerifier.create(geoOperations.geoRadiusByMember(key, member3, new Distance(100D, KILOMETERS))) + .consumeNextWith(actual -> { + + assertThat(actual).hasSize(2); + + assertThat(actual.get(0).getName()).isEqualTo(member3); + assertThat(actual.get(1).getName()).isEqualTo(member1); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoRadiusByMemberShouldReturnLocationsWithDistance() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + V member3 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + geoOperations.geoAdd(key, POINT_ARIGENTO, member3).block(); + + StepVerifier.create(geoOperations.geoRadiusByMember(key, member3, new Distance(100D, KILOMETERS), + newGeoRadiusArgs().includeDistance().sortDescending())).consumeNextWith(actual -> { + + assertThat(actual).hasSize(2); + + assertThat(actual.getContent().get(0).getDistance().getValue()).isCloseTo(90.9778, offset(0.005)); + assertThat(actual.getContent().get(0).getContent().getName()).isEqualTo(member1); + + assertThat(actual.getContent().get(1).getDistance().getValue()).isCloseTo(0.0, offset(0.005)); + assertThat(actual.getContent().get(1).getContent().getName()).isEqualTo(member3); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void geoRemove() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + V member2 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + geoOperations.geoAdd(key, POINT_CATANIA, member2).block(); + + StepVerifier.create(geoOperations.geoRemove(key, member1)).expectNext(1L).verifyComplete(); + StepVerifier.create(geoOperations.geoPos(key, member1)).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V member1 = valueFactory.instance(); + + geoOperations.geoAdd(key, POINT_PALERMO, member1).block(); + + StepVerifier.create(geoOperations.delete(key)).expectNext(true).verifyComplete(); + StepVerifier.create(geoOperations.geoPos(key, member1)).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java new file mode 100644 index 000000000..5f633ff7a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java @@ -0,0 +1,333 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.RawObjectFactory; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Integration tests for {@link DefaultReactiveHashOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveHashOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveHashOperations hashOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory hashKeyFactory; + private final ObjectFactory hashValueFactory; + + @Parameters(name = "{4}") + public static Collection testParams() { + + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory rawFactory = new RawObjectFactory(); + + LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(); + lettuceConnectionFactory.setPort(SettingsUtils.getPort()); + lettuceConnectionFactory.setHostName(SettingsUtils.getHost()); + lettuceConnectionFactory.afterPropertiesSet(); + + ReactiveRedisTemplate stringTemplate = new ReactiveRedisTemplate<>(); + stringTemplate.setConnectionFactory(lettuceConnectionFactory); + stringTemplate.setEnableDefaultSerializer(true); + stringTemplate.setDefaultSerializer(new StringRedisSerializer()); + stringTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate rawTemplate = new ReactiveRedisTemplate<>(); + rawTemplate.setConnectionFactory(lettuceConnectionFactory); + rawTemplate.setEnableDefaultSerializer(false); + rawTemplate.afterPropertiesSet(); + + return Arrays.asList(new Object[][] { { stringTemplate, stringFactory, stringFactory, stringFactory, "String" }, + { rawTemplate, rawFactory, rawFactory, rawFactory, "raw" } }); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + public DefaultReactiveHashOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory hashKeyFactory, ObjectFactory hashValueFactory, + String testName) { + + this.redisTemplate = redisTemplate; + this.hashOperations = redisTemplate.opsForHash(); + this.keyFactory = keyFactory; + this.hashKeyFactory = hashKeyFactory; + this.hashValueFactory = hashValueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void remove() { + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + StepVerifier.create(hashOperations.remove(key, hashkey1, hashkey2)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void hasKey() { + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = hashValueFactory.instance(); + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + + StepVerifier.create(hashOperations.hasKey(key, hashkey)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.hasKey(key, hashKeyFactory.instance())).expectNext(false).expectComplete() + .verify(); + } + + @Test // DATAREDIS-602 + public void get() { + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = hashValueFactory.instance(); + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + + StepVerifier.create(hashOperations.get(key, hashkey)).expectNextCount(1).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void multiGet() { + + assumeTrue(hashKeyFactory instanceof StringObjectFactory && hashValueFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.multiGet(key, Arrays.asList(hashkey1, hashkey2))).consumeNextWith(actual -> { + assertThat(actual).hasSize(2).containsSequence(hashvalue1, hashvalue2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void increment() { + + assumeTrue(hashValueFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = (HV) "1"; + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.increment(key, hashkey, 1L)).expectNext(2L).verifyComplete(); + StepVerifier.create(hashOperations.get(key, hashkey)).expectNext((HV) "2").verifyComplete(); + } + + @Test // DATAREDIS-602 + @SuppressWarnings("unchecked") + public void incrementDouble() { + + assumeTrue(hashValueFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = (HV) "1"; + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.increment(key, hashkey, 1.1d)).expectNext(2.1d).verifyComplete(); + StepVerifier.create(hashOperations.get(key, hashkey)).expectNext((HV) "2.1").verifyComplete(); + } + + @Test // DATAREDIS-602 + public void keys() { + + assumeTrue(hashKeyFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.keys(key)).consumeNextWith(actual -> { + assertThat(actual).hasSize(2).contains(hashkey1, hashkey2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void size() { + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.size(key)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void putAll() { + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.hasKey(key, hashkey1)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.hasKey(key, hashkey2)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void put() { + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = hashValueFactory.instance(); + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void putIfAbsent() { + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = hashValueFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + StepVerifier.create(hashOperations.putIfAbsent(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.putIfAbsent(key, hashkey, hashvalue2)).expectNext(false).expectComplete() + .verify(); + } + + @Test // DATAREDIS-602 + public void values() { + + assumeTrue(hashValueFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.values(key)).consumeNextWith(actual -> { + assertThat(actual).hasSize(2).contains(hashvalue1, hashvalue2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void entries() { + + assumeTrue(hashKeyFactory instanceof StringObjectFactory && hashValueFactory instanceof StringObjectFactory); + + K key = keyFactory.instance(); + HK hashkey1 = hashKeyFactory.instance(); + HV hashvalue1 = hashValueFactory.instance(); + + HK hashkey2 = hashKeyFactory.instance(); + HV hashvalue2 = hashValueFactory.instance(); + + putAll(key, hashkey1, hashvalue1, hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.entries(key)).consumeNextWith(actual -> { + assertThat(actual).hasSize(2).containsEntry(hashkey1, hashvalue1).containsEntry(hashkey2, hashvalue2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + HK hashkey = hashKeyFactory.instance(); + HV hashvalue = hashValueFactory.instance(); + + StepVerifier.create(hashOperations.put(key, hashkey, hashvalue)).expectNext(true).verifyComplete(); + StepVerifier.create(hashOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(hashOperations.size(key)).expectNext(0L).verifyComplete(); + } + + private void putAll(K key, HK hashkey1, HV hashvalue1, HK hashkey2, HV hashvalue2) { + + Map map = new HashMap<>(); + map.put(hashkey1, hashvalue1); + map.put(hashkey2, hashvalue2); + + StepVerifier.create(hashOperations.putAll(key, map)).expectNext(true).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java new file mode 100644 index 000000000..c277934c3 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java @@ -0,0 +1,127 @@ +/* + * 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.core; + +import reactor.test.StepVerifier; + +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * Integration tests for {@link DefaultReactiveHyperLogLogOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveHyperLogLogOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveHyperLogLogOperations hyperLogLogOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveHyperLogLogOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.hyperLogLogOperations = redisTemplate.opsForHyperLogLog(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void add() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(hyperLogLogOperations.add(key, value1, value2)).expectNext(1L).verifyComplete(); + + StepVerifier.create(hyperLogLogOperations.size(key)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void union() { + + K mergedKey = keyFactory.instance(); + V sharedValue = valueFactory.instance(); + + K key1 = keyFactory.instance(); + V value1 = valueFactory.instance(); + + K key2 = keyFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(hyperLogLogOperations.add(key1, value1, sharedValue)).expectNext(1L).verifyComplete(); + StepVerifier.create(hyperLogLogOperations.add(key2, value2, sharedValue)).expectNext(1L).verifyComplete(); + + StepVerifier.create(hyperLogLogOperations.union(mergedKey, key1, key2)).expectNext(true).verifyComplete(); + StepVerifier.create(hyperLogLogOperations.size(mergedKey)).expectNext(3L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(hyperLogLogOperations.add(key, value1, value2)).expectNext(1L).verifyComplete(); + StepVerifier.create(hyperLogLogOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(hyperLogLogOperations.size(key)).expectNext(0L).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java new file mode 100644 index 000000000..a15e62c9b --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java @@ -0,0 +1,397 @@ +/* + * 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.core; + +import static org.junit.Assume.*; + +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ByteBufferObjectFactory; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * Integration tests for {@link DefaultReactiveListOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveListOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveListOperations listOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveListOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.listOperations = redisTemplate.opsForList(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void trim() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.trim(key, 0, 0)).expectNext(true).verifyComplete(); + + StepVerifier.create(listOperations.size(key)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void size() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + + StepVerifier.create(listOperations.size(key)).expectNext(0L).verifyComplete(); + StepVerifier.create(listOperations.rightPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.size(key)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPush() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.leftPush(key, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value2) + .expectNext(value1).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPushAll() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value2) + .expectNext(value1).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPushIfPresent() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPushIfPresent(key, value1)).expectNext(0L).verifyComplete(); + StepVerifier.create(listOperations.leftPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.leftPushIfPresent(key, value2)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPushWithPivot() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.leftPush(key, value1, value3)).expectNext(3L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value2) + .expectNext(value3).expectNext(value1).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPush() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.rightPush(key, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value1) + .expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPushAll() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value1) + .expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPushIfPresent() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushIfPresent(key, value1)).expectNext(0L).verifyComplete(); + StepVerifier.create(listOperations.rightPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.rightPushIfPresent(key, value2)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPushWithPivot() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V value3 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.rightPush(key, value1, value3)).expectNext(3L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value1) + .expectNext(value3).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void set() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.set(key, 1, value1)).expectNext(true).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value1) + .expectNext(value1).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void remove() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.remove(key, 1, value1)).expectNext(1L).verifyComplete(); + + StepVerifier.create(listOperations.range(key, 0, -1).flatMap(Flux::fromIterable)).expectNext(value2) + .verifyComplete(); + } + + @Test // DATAREDIS-602 + public void index() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.index(key, 1)).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPop() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.leftPop(key)).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPop() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.rightPop(key)).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void leftPopWithTimeout() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.leftPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.leftPop(key, Duration.ZERO)).expectNext(value2).verifyComplete(); + } + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-602 + public void leftPopWithMillisecondTimeoutShouldFail() { + + K key = keyFactory.instance(); + + listOperations.leftPop(key, Duration.ofMillis(1001)); + } + + @Test // DATAREDIS-602 + public void rightPopWithTimeout() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPushAll(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(listOperations.rightPop(key, Duration.ZERO)).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPopAndLeftPush() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPush(source, value)).expectNext(1L).verifyComplete(); + + StepVerifier.create(listOperations.rightPopAndLeftPush(source, target)).expectNext(value).verifyComplete(); + + StepVerifier.create(listOperations.size(source)).expectNext(0L).verifyComplete(); + StepVerifier.create(listOperations.size(target)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rightPopAndLeftPushWithTimeout() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K source = keyFactory.instance(); + K target = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPopAndLeftPush(source, target, Duration.ofSeconds(1))).expectComplete() + .verify(); + + StepVerifier.create(listOperations.rightPush(source, value)).expectNext(1L).verifyComplete(); + + StepVerifier.create(listOperations.rightPopAndLeftPush(source, target, Duration.ZERO)).expectNext(value) + .verifyComplete(); + + StepVerifier.create(listOperations.size(source)).expectNext(0L).verifyComplete(); + StepVerifier.create(listOperations.size(target)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + + StepVerifier.create(listOperations.rightPush(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(listOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(listOperations.size(key)).expectNext(0L).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java new file mode 100644 index 000000000..50d88774a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java @@ -0,0 +1,354 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ByteBufferObjectFactory; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; + +/** + * Integration tests for {@link DefaultReactiveSetOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveSetOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveSetOperations setOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveSetOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.setOperations = redisTemplate.opsForSet(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void add() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1)).expectNext(1L).verifyComplete(); + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void remove() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.size(key)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.remove(key, value2)).expectNext(1L).verifyComplete(); + StepVerifier.create(setOperations.size(key)).expectNext(1L).verifyComplete(); + StepVerifier.create(setOperations.remove(key, value1, value2)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void pop() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.pop(key)).consumeNextWith(actual -> { + assertThat(actual).isIn(value1, value2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void move() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.move(key, value1, otherKey)).expectNext(true).verifyComplete(); + + StepVerifier.create(setOperations.size(otherKey)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void isMember() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.isMember(key, value1)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void intersect() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.intersect(key, otherKey)).consumeNextWith(actual -> { + assertThat(actual).contains(shared); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void intersectAndStore() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + K destKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.intersectAndStore(key, otherKey, destKey)).expectNext(1L).expectComplete() + .verify(); + + StepVerifier.create(setOperations.isMember(destKey, shared)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void difference() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.difference(key, otherKey)).consumeNextWith(actual -> { + assertThat(actual).contains(onlyInKey); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void differenceAndStore() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + K destKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.differenceAndStore(key, otherKey, destKey)).expectNext(1L).expectComplete() + .verify(); + + StepVerifier.create(setOperations.isMember(destKey, onlyInKey)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void union() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.union(key, otherKey)).consumeNextWith(actual -> { + assertThat(actual).contains(onlyInKey, shared, onlyInOtherKey); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void unionAndStore() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + K destKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, onlyInKey, shared)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.add(otherKey, onlyInOtherKey, shared)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.unionAndStore(key, otherKey, destKey)).expectNext(3L).verifyComplete(); + + StepVerifier.create(setOperations.isMember(destKey, onlyInKey)).expectNext(true).verifyComplete(); + StepVerifier.create(setOperations.isMember(destKey, shared)).expectNext(true).verifyComplete(); + StepVerifier.create(setOperations.isMember(destKey, onlyInOtherKey)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void members() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + StepVerifier.create(setOperations.members(key)).expectNext(new HashSet(Arrays.asList(value1, value2))) + .verifyComplete(); + } + + @Test // DATAREDIS-602 + public void randomMember() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.randomMember(key)).consumeNextWith(actual -> { + assertThat(actual).isIn(value1, value2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void randomMembers() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.randomMembers(key, 3)).consumeNextWith(actual -> { + assertThat(actual).hasSize(3); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void distinctRandomMembers() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value1, value2)).expectNext(2L).verifyComplete(); + + StepVerifier.create(setOperations.distinctRandomMembers(key, 2)).consumeNextWith(actual -> { + assertThat(actual).hasSize(2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(setOperations.add(key, value)).expectNext(1L).verifyComplete(); + + StepVerifier.create(setOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(setOperations.size(key)).expectNext(0L).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java new file mode 100644 index 000000000..847d2d646 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java @@ -0,0 +1,333 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import reactor.test.StepVerifier; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collection; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Integration tests for {@link DefaultReactiveValueOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveValueOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveValueOperations valueOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveValueOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.valueOperations = redisTemplate.opsForValue(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void set() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext(value).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void setWithExpiry() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value, Duration.ofSeconds(10))).expectNext(true).expectComplete() + .verify(); + + StepVerifier.create(valueOperations.get(key)).expectNext(value).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void setIfAbsent() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.setIfAbsent(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.setIfAbsent(key, value)).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void setIfPresent() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + V laterValue = valueFactory.instance(); + + StepVerifier.create(valueOperations.setIfPresent(key, value)).verifyComplete(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.setIfPresent(key, laterValue)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext(laterValue).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void multiSet() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + Map map = new LinkedHashMap(); + map.put(key1, value1); + map.put(key2, value2); + + StepVerifier.create(valueOperations.multiSet(map)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.get(key1)).expectNext(value1).verifyComplete(); + StepVerifier.create(valueOperations.get(key2)).expectNext(value2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void multiSetIfAbsent() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + Map map = new LinkedHashMap(); + + map.put(key1, value1); + + StepVerifier.create(valueOperations.multiSetIfAbsent(map)).expectNext(true).verifyComplete(); + + map.put(key2, value2); + StepVerifier.create(valueOperations.multiSetIfAbsent(map)).expectNext(false).verifyComplete(); + + StepVerifier.create(valueOperations.get(key1)).expectNext(value1).verifyComplete(); + StepVerifier.create(valueOperations.get(key2)).expectNextCount(0).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void get() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext(value).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void getAndSet() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + V nextValue = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.getAndSet(key, nextValue)).expectNext(value).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext(nextValue).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void multiGet() { + + K key1 = keyFactory.instance(); + K key2 = keyFactory.instance(); + K absent = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + V absentValue = null; + + if (redisTemplate.getValueSerializer() instanceof StringRedisSerializer) { + absentValue = (V) ""; + } + if (value1 instanceof ByteBuffer) { + absentValue = (V) ByteBuffer.wrap(new byte[0]); + } + + Map map = new LinkedHashMap(); + map.put(key1, value1); + map.put(key2, value2); + + StepVerifier.create(valueOperations.multiSet(map)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.multiGet(Arrays.asList(key2, key1, absent))) + .expectNext(Arrays.asList(value2, value1, absentValue)).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void append() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.append(key, "foo")).expectNextCount(1).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext((V) (value + "foo")).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void getRange() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + String substring = value.toString().substring(1, 5); + + StepVerifier.create(valueOperations.get(key, 1, 4)).expectNext(substring).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void setRange() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + StepVerifier.create(valueOperations.set(key, (V) "boo", 2)).expectNextCount(1).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).consumeNextWith(actual -> { + + String string = (String) actual; + String prefix = value.toString().substring(0, 2); + + assertThat(string).startsWith(prefix + "boo"); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void size() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + StepVerifier.create(valueOperations.size(key)).expectNext((long) value.toString().length()).expectComplete() + .verify(); + } + + @Test // DATAREDIS-602 + public void setBit() { + + K key = keyFactory.instance(); + + StepVerifier.create(valueOperations.setBit(key, 0, true)).expectNext(false).expectComplete(); + StepVerifier.create(valueOperations.setBit(key, 2, true)).expectNext(false).expectComplete(); + } + + @Test // DATAREDIS-602 + public void getBit() { + + K key = keyFactory.instance(); + + StepVerifier.create(valueOperations.setBit(key, 0, true)).expectNext(false).expectComplete(); + StepVerifier.create(valueOperations.getBit(key, 0)).expectNext(true).expectComplete(); + StepVerifier.create(valueOperations.getBit(key, 1)).expectNext(false).expectComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.size(key)).expectNext(0L).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java new file mode 100644 index 000000000..6309ccb45 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java @@ -0,0 +1,617 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import reactor.test.StepVerifier; + +import java.util.Arrays; +import java.util.Collection; +import java.util.List; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.ByteBufferObjectFactory; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisZSetCommands.Limit; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Integration tests for {@link DefaultReactiveZSetOperations}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +@SuppressWarnings("unchecked") +public class DefaultReactiveZSetOperationsIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + private final ReactiveZSetOperations zSetOperations; + + private final ObjectFactory keyFactory; + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public DefaultReactiveZSetOperationsIntegrationTests(ReactiveRedisTemplate redisTemplate, + ObjectFactory keyFactory, ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.zSetOperations = redisTemplate.opsForZSet(); + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void add() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value, 42.1)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void addAll() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + List> tuples = Arrays.asList(new DefaultTypedTuple<>(value1, 42.1d), + new DefaultTypedTuple<>(value2, 10d)); + + StepVerifier.create(zSetOperations.addAll(key, tuples)).expectNext(2L).verifyComplete(); + + List> updated = Arrays.asList(new DefaultTypedTuple<>(value1, 52.1d), + new DefaultTypedTuple<>(value2, 10d)); + + StepVerifier.create(zSetOperations.addAll(key, updated)).expectNext(0L).verifyComplete(); + StepVerifier.create(zSetOperations.score(key, value1)).expectNext(52.1d).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void remove() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value, 42.1)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.remove(key, value)).expectNext(1L).verifyComplete(); + + StepVerifier.create(zSetOperations.remove(key, value)).expectNext(0L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void incrementScore() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value, 42.1)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.incrementScore(key, value, 1.1)).expectNext(43.2).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rank() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rank(key, value1)).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRank() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRank(key, value1)).expectNext(0L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void range() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.range(key, new Range<>(0L, 0L))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value2); + }).verifyComplete(); + + } + + @Test // DATAREDIS-602 + public void rangeWithScores() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeWithScores(key, new Range<>(0L, 0L))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple<>(value2, 10d)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rangeByScore() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeByScore(key, new Range<>(9d, 11d))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rangeByScoreWithScores() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeByScoreWithScores(key, new Range<>(9d, 11d))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple<>(value2, 10d)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rangeByScoreWithLimit() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier + .create(zSetOperations.rangeByScore(key, new Range<>(0d, 100d), // + Limit.limit().offset(1).count(10))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value1); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rangeByScoreWithScoresWithLimit() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier + .create(zSetOperations.rangeByScoreWithScores(key, new Range<>(0d, 100d), // + Limit.limit().offset(1).count(10))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple<>(value1, 42.1)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRange() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRange(key, new Range<>(0L, 0L))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value1); + }).verifyComplete(); + + } + + @Test // DATAREDIS-602 + public void reverseRangeWithScores() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeWithScores(key, new Range<>(0L, 0L))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple<>(value1, 42.1)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByScore() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeByScore(key, new Range<>(9d, 11d))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByScoreWithScores() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeByScoreWithScores(key, new Range<>(9d, 11d))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple(value2, 10d)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByScoreWithLimit() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier + .create(zSetOperations.reverseRangeByScore(key, new Range<>(0d, 100d), // + Limit.limit().offset(1).count(10))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value2); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByScoreWithScoresWithLimit() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier + .create(zSetOperations.reverseRangeByScoreWithScores(key, new Range<>(0d, 100d), // + Limit.limit().offset(1).count(10))) // + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(new DefaultTypedTuple<>(value2, 10d)); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void count() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.count(key, new Range(0d, 100d))).expectNext(2L).expectComplete() + .verify(); + StepVerifier.create(zSetOperations.count(key, new Range(0d, 10d))).expectNext(1L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void size() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.size(key)).expectNext(2L).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void score() { + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.score(key, value1)).expectNext(42.1d).verifyComplete(); + StepVerifier.create(zSetOperations.score(key, value2)).expectNext(10d).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void removeRange() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.removeRange(key, new Range<>(0L, 0L))).expectNext(1L).verifyComplete(); + StepVerifier.create(zSetOperations.range(key, new Range<>(0L, 5L))).consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value1); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void removeRangeByScore() { + + assumeFalse(valueFactory instanceof ByteBufferObjectFactory); + + K key = keyFactory.instance(); + V value1 = valueFactory.instance(); + V value2 = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value1, 42.1)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, value2, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.removeRangeByScore(key, new Range<>(9d, 11d))).expectNext(1L).expectComplete() + .verify(); + StepVerifier.create(zSetOperations.range(key, new Range<>(0L, 5L))).consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(value1); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void unionAndStore() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + K destKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, onlyInKey, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, shared, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.add(otherKey, onlyInOtherKey, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(otherKey, shared, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.unionAndStore(key, otherKey, destKey)).expectNext(3L).verifyComplete(); + StepVerifier.create(zSetOperations.range(destKey, new Range<>(0L, 100L))).consumeNextWith(actual -> { + assertThat(actual).hasSize(3); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void intersectAndStore() { + + K key = keyFactory.instance(); + K otherKey = keyFactory.instance(); + K destKey = keyFactory.instance(); + + V onlyInKey = valueFactory.instance(); + V shared = valueFactory.instance(); + V onlyInOtherKey = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, onlyInKey, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, shared, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.add(otherKey, onlyInOtherKey, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(otherKey, shared, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.intersectAndStore(key, otherKey, destKey)).expectNext(1L).expectComplete() + .verify(); + StepVerifier.create(zSetOperations.range(destKey, new Range<>(0L, 5L))).consumeNextWith(actual -> { + assertThat(actual).hasSize(1); + }).verifyComplete(); + + } + + @Test // DATAREDIS-602 + public void rangeByLex() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V a = (V) "a"; + V b = (V) "b"; + + StepVerifier.create(zSetOperations.add(key, a, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, b, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeByLex(key, new Range<>("a", "a"))).consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(a); + }).verifyComplete(); + + } + + @Test // DATAREDIS-602 + public void rangeByLexWithLimit() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V a = (V) "a"; + V b = (V) "b"; + + StepVerifier.create(zSetOperations.add(key, a, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, b, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeByLex(key, new Range<>("a", "z"), Limit.limit().offset(0).count(10))) + .consumeNextWith(actual -> { + assertThat(actual).hasSize(2).contains(a, b); + }).verifyComplete(); + + StepVerifier.create(zSetOperations.rangeByLex(key, new Range<>("a", "z"), Limit.limit().offset(1).count(10))) + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(b); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByLex() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V a = (V) "a"; + V b = (V) "b"; + + StepVerifier.create(zSetOperations.add(key, a, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, b, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeByLex(key, new Range<>("a", "a"))).consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(a); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void reverseRangeByLexLimit() { + + assumeTrue(redisTemplate.getValueSerializer() instanceof StringRedisSerializer); + + K key = keyFactory.instance(); + V a = (V) "a"; + V b = (V) "b"; + + StepVerifier.create(zSetOperations.add(key, a, 10)).expectNext(true).verifyComplete(); + StepVerifier.create(zSetOperations.add(key, b, 11)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeByLex(key, new Range<>("a", "z"), Limit.limit().offset(0).count(10))) + .consumeNextWith(actual -> { + assertThat(actual).hasSize(2).contains(b, a); + }).verifyComplete(); + + StepVerifier.create(zSetOperations.reverseRangeByLex(key, new Range<>("a", "z"), Limit.limit().offset(1).count(10))) + .consumeNextWith(actual -> { + assertThat(actual).hasSize(1).contains(a); + }).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void delete() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(zSetOperations.add(key, value, 10)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.delete(key)).expectNext(true).verifyComplete(); + + StepVerifier.create(zSetOperations.size(key)).expectNext(0L).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java new file mode 100644 index 000000000..f0ee78d88 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java @@ -0,0 +1,153 @@ +/* + * 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.core; + +import static org.springframework.data.redis.connection.ClusterTestVariables.*; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; + +import org.springframework.data.redis.ByteBufferObjectFactory; +import org.springframework.data.redis.DoubleObjectFactory; +import org.springframework.data.redis.LongObjectFactory; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.Person; +import org.springframework.data.redis.PersonObjectFactory; +import org.springframework.data.redis.PrefixStringObjectFactory; +import org.springframework.data.redis.SettingsUtils; +import org.springframework.data.redis.StringObjectFactory; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.GenericToStringSerializer; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.OxmSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; +import org.springframework.oxm.xstream.XStreamMarshaller; + +/** + * Parameters for testing implementations of {@link ReactiveRedisTemplate} + * + * @author Mark Paluch + */ +abstract public class ReactiveOperationsTestParams { + + public static Collection testParams() { + + ObjectFactory stringFactory = new StringObjectFactory(); + ObjectFactory clusterKeyStringFactory = new PrefixStringObjectFactory("{u1}.", stringFactory); + ObjectFactory longFactory = new LongObjectFactory(); + ObjectFactory doubleFactory = new DoubleObjectFactory(); + ObjectFactory rawFactory = new ByteBufferObjectFactory(); + ObjectFactory personFactory = new PersonObjectFactory(); + + // XStream serializer + XStreamMarshaller xstream = new XStreamMarshaller(); + try { + xstream.afterPropertiesSet(); + } catch (Exception ex) { + throw new RuntimeException("Cannot init XStream", ex); + } + + LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(); + lettuceConnectionFactory.setPort(SettingsUtils.getPort()); + lettuceConnectionFactory.setHostName(SettingsUtils.getHost()); + lettuceConnectionFactory.afterPropertiesSet(); + + RedisClusterConfiguration clusterConfiguration = new RedisClusterConfiguration(); + clusterConfiguration.addClusterNode(new RedisClusterNode(CLUSTER_HOST, MASTER_NODE_1_PORT)); + + LettuceConnectionFactory lettuceClusterConnectionFactory = new LettuceConnectionFactory(clusterConfiguration); + lettuceClusterConnectionFactory.setPort(SettingsUtils.getPort()); + lettuceClusterConnectionFactory.setHostName(SettingsUtils.getHost()); + lettuceClusterConnectionFactory.afterPropertiesSet(); + + ReactiveRedisTemplate objectTemplate = new ReactiveRedisTemplate<>(); + objectTemplate.setConnectionFactory(lettuceConnectionFactory); + objectTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate clusterStringTemplate = new ReactiveRedisTemplate<>(); + clusterStringTemplate.setConnectionFactory(lettuceClusterConnectionFactory); + clusterStringTemplate.setDefaultSerializer(new StringRedisSerializer()); + clusterStringTemplate.setEnableDefaultSerializer(true); + clusterStringTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate stringTemplate = new ReactiveRedisTemplate<>(); + stringTemplate.setDefaultSerializer(new StringRedisSerializer()); + stringTemplate.setEnableDefaultSerializer(true); + stringTemplate.setConnectionFactory(lettuceConnectionFactory); + stringTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate longTemplate = new ReactiveRedisTemplate<>(); + longTemplate.setKeySerializer(new StringRedisSerializer()); + longTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); + longTemplate.setConnectionFactory(lettuceConnectionFactory); + longTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate doubleTemplate = new ReactiveRedisTemplate<>(); + doubleTemplate.setKeySerializer(new StringRedisSerializer()); + doubleTemplate.setValueSerializer(new GenericToStringSerializer<>(Double.class)); + doubleTemplate.setConnectionFactory(lettuceConnectionFactory); + doubleTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate rawTemplate = new ReactiveRedisTemplate<>(); + rawTemplate.setEnableDefaultSerializer(false); + rawTemplate.setConnectionFactory(lettuceConnectionFactory); + rawTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate personTemplate = new ReactiveRedisTemplate<>(); + personTemplate.setConnectionFactory(lettuceConnectionFactory); + personTemplate.afterPropertiesSet(); + + OxmSerializer serializer = new OxmSerializer(xstream, xstream); + ReactiveRedisTemplate xstreamStringTemplate = new ReactiveRedisTemplate<>(); + xstreamStringTemplate.setConnectionFactory(lettuceConnectionFactory); + xstreamStringTemplate.setDefaultSerializer(serializer); + xstreamStringTemplate.afterPropertiesSet(); + + ReactiveRedisTemplate xstreamPersonTemplate = new ReactiveRedisTemplate<>(); + xstreamPersonTemplate.setConnectionFactory(lettuceConnectionFactory); + xstreamPersonTemplate.setValueSerializer(serializer); + xstreamPersonTemplate.afterPropertiesSet(); + + Jackson2JsonRedisSerializer jackson2JsonSerializer = new Jackson2JsonRedisSerializer<>(Person.class); + ReactiveRedisTemplate jackson2JsonPersonTemplate = new ReactiveRedisTemplate<>(); + jackson2JsonPersonTemplate.setConnectionFactory(lettuceConnectionFactory); + jackson2JsonPersonTemplate.setValueSerializer(jackson2JsonSerializer); + jackson2JsonPersonTemplate.afterPropertiesSet(); + + GenericJackson2JsonRedisSerializer genericJackson2JsonSerializer = new GenericJackson2JsonRedisSerializer(); + ReactiveRedisTemplate genericJackson2JsonPersonTemplate = new ReactiveRedisTemplate<>(); + genericJackson2JsonPersonTemplate.setConnectionFactory(lettuceConnectionFactory); + genericJackson2JsonPersonTemplate.setValueSerializer(genericJackson2JsonSerializer); + genericJackson2JsonPersonTemplate.afterPropertiesSet(); + + return Arrays.asList(new Object[][] { // + { stringTemplate, stringFactory, stringFactory , "String"}, // + { clusterStringTemplate, clusterKeyStringFactory, stringFactory, "Cluster String" }, // + { objectTemplate, personFactory, personFactory , "Person/JDK"}, // + { longTemplate, stringFactory, longFactory , "Long"}, // + { doubleTemplate, stringFactory, doubleFactory , "Double"}, // + { rawTemplate, rawFactory, rawFactory , "raw"}, // + { personTemplate, stringFactory, personFactory , "String/Person/JDK"}, // + { xstreamStringTemplate, stringFactory, stringFactory , "String/OXM"}, // + { xstreamPersonTemplate, stringFactory, personFactory, "String/Person/OXM"}, // + { jackson2JsonPersonTemplate, stringFactory, personFactory, "Jackson2"}, // + { genericJackson2JsonPersonTemplate, stringFactory, personFactory, "Generic Jackson 2" } }); + } +} diff --git a/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java new file mode 100644 index 000000000..593b3c538 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java @@ -0,0 +1,309 @@ +/* + * 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.core; + +import static org.assertj.core.api.Assertions.*; +import static org.junit.Assume.*; + +import reactor.test.StepVerifier; + +import java.time.Duration; +import java.time.Instant; +import java.util.Collection; + +import org.junit.AfterClass; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.ConnectionFactoryTracker; +import org.springframework.data.redis.ObjectFactory; +import org.springframework.data.redis.Person; +import org.springframework.data.redis.PersonObjectFactory; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; +import org.springframework.data.redis.connection.RedisConnection; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer; +import org.springframework.data.redis.serializer.ReactiveSerializationContext; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * Integration tests for {@link ReactiveRedisTemplate}. + * + * @author Mark Paluch + */ +@RunWith(Parameterized.class) +public class ReactiveRedisTemplateIntegrationTests { + + private final ReactiveRedisTemplate redisTemplate; + + private final ObjectFactory keyFactory; + + private final ObjectFactory valueFactory; + + @Parameters(name = "{3}") + public static Collection testParams() { + return ReactiveOperationsTestParams.testParams(); + } + + @AfterClass + public static void cleanUp() { + ConnectionFactoryTracker.cleanUp(); + } + + /** + * @param redisTemplate + * @param keyFactory + * @param valueFactory + * @param label parameterized test label, no further use besides that. + */ + public ReactiveRedisTemplateIntegrationTests(ReactiveRedisTemplate redisTemplate, ObjectFactory keyFactory, + ObjectFactory valueFactory, String label) { + + this.redisTemplate = redisTemplate; + this.keyFactory = keyFactory; + this.valueFactory = valueFactory; + + ConnectionFactoryTracker.add(redisTemplate.getConnectionFactory()); + } + + @Before + public void before() { + + RedisConnectionFactory connectionFactory = (RedisConnectionFactory) redisTemplate.getConnectionFactory(); + RedisConnection connection = connectionFactory.getConnection(); + connection.flushAll(); + connection.close(); + } + + @Test // DATAREDIS-602 + public void exists() { + + K key = keyFactory.instance(); + + StepVerifier.create(redisTemplate.hasKey(key)).expectNext(false).verifyComplete(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, valueFactory.instance())).expectNext(true) + .verifyComplete(); + + StepVerifier.create(redisTemplate.hasKey(key)).expectNext(true).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void type() { + + K key = keyFactory.instance(); + + StepVerifier.create(redisTemplate.type(key)).expectNext(DataType.NONE).verifyComplete(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, valueFactory.instance())).expectNext(true) + .verifyComplete(); + + StepVerifier.create(redisTemplate.type(key)).expectNext(DataType.STRING).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void rename() { + + K oldName = keyFactory.instance(); + K newName = keyFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(oldName, valueFactory.instance())).expectNext(true) + .verifyComplete(); + + StepVerifier.create(redisTemplate.rename(oldName, newName)) // + .expectNext(true) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void renameNx() { + + K oldName = keyFactory.instance(); + K existing = keyFactory.instance(); + K newName = keyFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(oldName, valueFactory.instance())).expectNext(true) + .verifyComplete(); + StepVerifier.create(redisTemplate.opsForValue().set(existing, valueFactory.instance())).expectNext(true) + .verifyComplete(); + + StepVerifier.create(redisTemplate.renameIfAbsent(oldName, newName)) // + .expectNext(true) // + .expectComplete() // + .verify(); + + StepVerifier.create(redisTemplate.opsForValue().set(existing, valueFactory.instance())).expectNext(true) + .verifyComplete(); + + StepVerifier.create(redisTemplate.renameIfAbsent(newName, existing)).expectNext(false) // + .expectComplete() // + .verify(); + } + + @Test // DATAREDIS-602 + public void expire() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.expire(key, Duration.ofSeconds(10))).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void preciseExpire() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.expire(key, Duration.ofMillis(10_001))).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void expireAt() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + Instant expireAt = Instant.ofEpochSecond(Instant.now().plus(Duration.ofSeconds(10)).getEpochSecond()); + + StepVerifier.create(redisTemplate.expireAt(key, expireAt)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))) // + .verifyComplete(); + } + + @Test // DATAREDIS-602 + public void preciseExpireAt() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + Instant expireAt = Instant.ofEpochSecond(Instant.now().plus(Duration.ofSeconds(10)).getEpochSecond(), 5); + + StepVerifier.create(redisTemplate.expireAt(key, expireAt)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)) // + .consumeNextWith(actual -> assertThat(actual).isGreaterThan(Duration.ofSeconds(8))) // + .verifyComplete(); + } + + @Test // DATAREDIS-602 + public void getTtlForAbsentKeyShouldCompleteWithoutValue() { + + K key = keyFactory.instance(); + + StepVerifier.create(redisTemplate.getExpire(key)).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void getTtlForKeyWithoutExpiryShouldCompleteWithZeroDuration() { + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.getExpire(key)).expectNext(Duration.ZERO).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void move() { + + ReactiveRedisClusterConnection connection = null; + try { + connection = redisTemplate.getConnectionFactory().getReactiveClusterConnection(); + assumeTrue(connection == null); + } catch (InvalidDataAccessApiUsageException e) {} finally { + if (connection != null) { + connection.close(); + } + } + + K key = keyFactory.instance(); + V value = valueFactory.instance(); + + StepVerifier.create(redisTemplate.opsForValue().set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.move(key, 5)).expectNext(true).verifyComplete(); + + StepVerifier.create(redisTemplate.hasKey(key)).expectNext(false).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void shouldApplyCustomSerializationContextToValues() { + + Person key = new PersonObjectFactory().instance(); + Person value = new PersonObjectFactory().instance(); + + JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(); + ReactiveSerializationContext objectSerializers = ReactiveSerializationContext.builder() + .key(jdkSerializer) // + .value(jdkSerializer) // + .hashKey(jdkSerializer) // + .hashValue(jdkSerializer) // + .build(); + + ReactiveValueOperations valueOperations = redisTemplate.opsForValue(objectSerializers); + + StepVerifier.create(valueOperations.set(key, value)).expectNext(true).verifyComplete(); + + StepVerifier.create(valueOperations.get(key)).expectNext(value).verifyComplete(); + } + + @Test // DATAREDIS-602 + public void shouldApplyCustomSerializationContextToHash() { + + ReactiveSerializationContext serializationContext = redisTemplate.getSerializationContext(); + + K key = keyFactory.instance(); + String hashField = "foo"; + Person hashValue = new PersonObjectFactory().instance(); + + JdkSerializationRedisSerializer jdkSerializer = new JdkSerializationRedisSerializer(); + ReactiveSerializationContext objectSerializers = ReactiveSerializationContext. builder() + .key(serializationContext.key()) // + .value(serializationContext.value()) // + .hashKey(new StringRedisSerializer()) // + .hashValue(jdkSerializer) // + .build(); + + ReactiveHashOperations hashOperations = redisTemplate.opsForHash(objectSerializers); + + StepVerifier.create(hashOperations.put(key, hashField, hashValue)).expectNext(true).verifyComplete(); + + StepVerifier.create(hashOperations.get(key, hashField)).expectNext(hashValue).verifyComplete(); + } +} diff --git a/src/test/java/org/springframework/data/redis/serializer/ReactiveSerializationContextUnitTests.java b/src/test/java/org/springframework/data/redis/serializer/ReactiveSerializationContextUnitTests.java new file mode 100644 index 000000000..3d69d7e0a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/serializer/ReactiveSerializationContextUnitTests.java @@ -0,0 +1,112 @@ +/* + * 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.serializer; + +import static org.assertj.core.api.Assertions.*; + +import java.nio.ByteBuffer; + +import org.junit.Test; + +/** + * Unit tests for {@link ReactiveSerializationContext}. + * + * @author Mark Paluch + */ +public class ReactiveSerializationContextUnitTests { + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-602 + public void shouldRejectBuildIfKeySerializerIsNotSet() { + + ReactiveSerializationContext. builder() // + .value(new StringRedisSerializer()) // + .hashKey(new StringRedisSerializer()) // + .hashValue(new StringRedisSerializer()) // + .build(); + } + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-602 + public void shouldRejectBuildIfValueSerializerIsNotSet() { + + ReactiveSerializationContext. builder() // + .key(new StringRedisSerializer()) // + .hashKey(new StringRedisSerializer()) // + .hashValue(new StringRedisSerializer()) // + .build(); + } + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-602 + public void shouldRejectBuildIfHashKeySerializerIsNotSet() { + + ReactiveSerializationContext. builder() // + .key(new StringRedisSerializer()) // + .value(new StringRedisSerializer()) // + .hashValue(new StringRedisSerializer()) // + .build(); + } + + @Test(expected = IllegalArgumentException.class) // DATAREDIS-602 + public void shouldRejectBuildIfHashValueSerializerIsNotSet() { + + ReactiveSerializationContext. builder() // + .key(new StringRedisSerializer()) // + .value(new StringRedisSerializer()) // + .hashKey(new StringRedisSerializer()) // + .build(); + } + + @Test // DATAREDIS-602 + public void shouldBuildSerializationContext() { + + ReactiveSerializationContext serializationContext = createSerializationContext(); + + assertThat(serializationContext.key()).isNotNull(); + assertThat(serializationContext.value()).isNotNull(); + assertThat(serializationContext.hashKey()).isNotNull(); + assertThat(serializationContext.hashValue()).isNotNull(); + assertThat(serializationContext.string()).isNotNull(); + } + + @Test // DATAREDIS-602 + public void shouldEncodeAndDecodeKey() { + + ReactiveSerializationContext serializationContext = createSerializationContext(); + + String deserialized = serializationContext.key().read(serializationContext.key().write("foo")); + + assertThat(deserialized).isEqualTo("foo"); + } + + @Test // DATAREDIS-602 + public void shouldEncodeAndDecodeValue() { + + ReactiveSerializationContext serializationContext = createSerializationContext(); + + long deserialized = serializationContext.value().read(serializationContext.value().write(42L)); + + assertThat(deserialized).isEqualTo(42); + } + + private ReactiveSerializationContext createSerializationContext() { + + return ReactiveSerializationContext. builder() // + .key(new StringRedisSerializer()) // + .value(ByteBuffer::getLong, value -> (ByteBuffer) ByteBuffer.allocate(8).putLong(value).flip()) // + .hashKey(new StringRedisSerializer()) // + .hashValue(new StringRedisSerializer()) // + .build(); + } +}