From 81657c530ba3526f4a2613853c568245dc61273f Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Thu, 9 Feb 2017 17:59:31 +0100 Subject: [PATCH] DATAREDIS-602 - Provide Reactive Redis Template. Add ReactiveRedisTemplate, add tests abd add SerializerFunction and DeserializerFunction for functional Redis argument serialization. Introduce ReactiveRedisConnectionFactory and remove reactive RedisConnectionFactory methods to break the dependency to reactive types. We now no longer require Project Reactor for blocking Redis use. ReactiveRedisTemplate accepts ReactiveRedisConnectionFactory to operate on a reactive connection. The only implementation of ReactiveRedisConnectionFactory is LettuceConnectionFactory which requires by default Project Reactor. Original Pull Request: #239 --- pom.xml | 14 +- .../redis/connection/ReactiveGeoCommands.java | 5 +- .../redis/connection/ReactiveKeyCommands.java | 351 +++++++ .../connection/ReactiveRedisConnection.java | 4 + .../ReactiveRedisConnectionFactory.java | 44 + .../connection/ReactiveZSetCommands.java | 18 +- .../connection/RedisConnectionFactory.java | 23 +- .../jedis/JedisConnectionFactory.java | 46 +- .../lettuce/LettuceConnectionFactory.java | 22 +- .../LettuceReactiveClusterKeyCommands.java | 14 +- .../lettuce/LettuceReactiveKeyCommands.java | 196 +++- .../lettuce/LettuceReactiveZSetCommands.java | 18 +- .../CloseSuppressingInvocationHandler.java | 4 +- .../core/DefaultReactiveGeoOperations.java | 395 ++++++++ .../core/DefaultReactiveHashOperations.java | 299 ++++++ .../DefaultReactiveHyperLogLogOperations.java | 137 +++ .../core/DefaultReactiveListOperations.java | 368 +++++++ .../core/DefaultReactiveSetOperations.java | 451 +++++++++ .../core/DefaultReactiveValueOperations.java | 298 ++++++ .../core/DefaultReactiveZSetOperations.java | 537 ++++++++++ .../redis/core/ReactiveGeoOperations.java | 230 +++++ .../redis/core/ReactiveHashOperations.java | 153 +++ .../core/ReactiveHyperLogLogOperations.java | 59 ++ .../redis/core/ReactiveListOperations.java | 269 +++++ .../redis/core/ReactiveRedisCallback.java | 49 + .../redis/core/ReactiveRedisOperations.java | 296 ++++++ .../redis/core/ReactiveRedisTemplate.java | 957 ++++++++++++++++++ .../redis/core/ReactiveSetOperations.java | 261 +++++ .../redis/core/ReactiveValueOperations.java | 173 ++++ .../redis/core/ReactiveZSetOperations.java | 383 +++++++ .../core/convert/KeyspaceConfiguration.java | 40 +- .../data/redis/core/types/Expiration.java | 47 +- ...ltReactiveSerializationContextBuilder.java | 204 ++++ .../serializer/DefaultRedisElementReader.java | 49 + .../serializer/DefaultRedisElementWriter.java | 54 + .../serializer/DefaultSerializationTuple.java | 48 + .../ReactiveSerializationContext.java | 285 ++++++ .../redis/serializer/RedisElementReader.java | 37 + .../redis/serializer/RedisElementWriter.java | 36 + .../RedisSerializerTupleAdapter.java | 63 ++ .../data/redis/ByteBufferObjectFactory.java | 31 + .../data/redis/ConnectionFactoryTracker.java | 22 +- .../data/redis/PrefixStringObjectFactory.java | 36 + .../LettuceReactiveKeyCommandsTests.java | 126 ++- .../LettuceReactiveZSetCommandsTests.java | 20 +- ...ReactiveGeoOperationsIntegrationTests.java | 423 ++++++++ ...eactiveHashOperationsIntegrationTests.java | 333 ++++++ ...HyperLogLogOperationsIntegrationTests.java | 127 +++ ...eactiveListOperationsIntegrationTests.java | 397 ++++++++ ...ReactiveSetOperationsIntegrationTests.java | 354 +++++++ ...activeValueOperationsIntegrationTests.java | 333 ++++++ ...eactiveZSetOperationsIntegrationTests.java | 617 +++++++++++ .../core/ReactiveOperationsTestParams.java | 153 +++ ...ReactiveRedisTemplateIntegrationTests.java | 309 ++++++ ...ReactiveSerializationContextUnitTests.java | 112 ++ 55 files changed, 10172 insertions(+), 158 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnectionFactory.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveGeoOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveHashOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveListOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveSetOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveValueOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/DefaultReactiveZSetOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveGeoOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveHashOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveHyperLogLogOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveListOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveRedisCallback.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveRedisOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveRedisTemplate.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveSetOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveValueOperations.java create mode 100644 src/main/java/org/springframework/data/redis/core/ReactiveZSetOperations.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultReactiveSerializationContextBuilder.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementReader.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultRedisElementWriter.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/DefaultSerializationTuple.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/ReactiveSerializationContext.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/RedisElementReader.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/RedisElementWriter.java create mode 100644 src/main/java/org/springframework/data/redis/serializer/RedisSerializerTupleAdapter.java create mode 100644 src/test/java/org/springframework/data/redis/ByteBufferObjectFactory.java create mode 100644 src/test/java/org/springframework/data/redis/PrefixStringObjectFactory.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveGeoOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveHashOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveHyperLogLogOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveListOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveSetOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveValueOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/DefaultReactiveZSetOperationsIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/core/ReactiveOperationsTestParams.java create mode 100644 src/test/java/org/springframework/data/redis/core/ReactiveRedisTemplateIntegrationTests.java create mode 100644 src/test/java/org/springframework/data/redis/serializer/ReactiveSerializationContextUnitTests.java 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(); + } +}