From d257ae1074ec8c9d90eebddcde5455917c916eda Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Wed, 29 Jun 2016 21:11:17 +0200 Subject: [PATCH] DATAREDIS-525 - Add support for ReactiveRedisConnection & ReactiveRedisClusterConnection. We introduced reactor based reactive connection support via ReactiveRedisConnection and ReactiveRedisClusterConnection. A reactive connection can be obtained via the ConnectionFactory. This is currently only possible when using the Lettuce Redis driver. Original pull request: #229. --- pom.xml | 31 +- .../redis/connection/ClusterSlotHashUtil.java | 31 + .../ReactiveClusterGeoCommands.java | 25 + .../ReactiveClusterHashCommands.java | 25 + .../ReactiveClusterHyperLogLogCommands.java | 25 + .../ReactiveClusterKeyCommands.java | 63 + .../ReactiveClusterListCommands.java | 25 + .../ReactiveClusterNumberCommands.java | 25 + .../ReactiveClusterSetCommands.java | 25 + .../ReactiveClusterStringCommands.java | 25 + .../ReactiveClusterZSetCommands.java | 25 + .../redis/connection/ReactiveGeoCommands.java | 771 ++++++++++ .../connection/ReactiveHashCommands.java | 414 ++++++ .../ReactiveHyperLogLogCommands.java | 222 +++ .../redis/connection/ReactiveKeyCommands.java | 215 +++ .../connection/ReactiveListCommands.java | 820 +++++++++++ .../connection/ReactiveNumberCommands.java | 248 ++++ .../ReactiveRedisClusterConnection.java | 51 + .../connection/ReactiveRedisConnection.java | 256 ++++ .../redis/connection/ReactiveSetCommands.java | 700 +++++++++ .../connection/ReactiveStringCommands.java | 752 ++++++++++ .../connection/ReactiveZSetCommands.java | 1254 +++++++++++++++++ .../connection/RedisConnectionFactory.java | 16 +- .../redis/connection/RedisGeoCommands.java | 12 +- .../redis/connection/convert/Converters.java | 6 +- .../jedis/JedisConnectionFactory.java | 25 +- .../lettuce/LettuceConnectionFactory.java | 18 + .../connection/lettuce/LettuceConverters.java | 4 + .../LettuceReactiveClusterGeoCommands.java | 36 + .../LettuceReactiveClusterHashCommands.java | 36 + ...uceReactiveClusterHyperLogLogCommands.java | 86 ++ .../LettuceReactiveClusterKeyCommands.java | 143 ++ .../LettuceReactiveClusterListCommands.java | 85 ++ .../LettuceReactiveClusterNumberCommands.java | 36 + .../LettuceReactiveClusterSetCommands.java | 250 ++++ .../LettuceReactiveClusterStringCommands.java | 78 + .../LettuceReactiveClusterZSetCommands.java | 77 + .../lettuce/LettuceReactiveGeoCommands.java | 224 +++ .../lettuce/LettuceReactiveHashCommands.java | 227 +++ .../LettuceReactiveHyperLogLogCommands.java | 107 ++ .../lettuce/LettuceReactiveKeyCommands.java | 183 +++ .../lettuce/LettuceReactiveListCommands.java | 315 +++++ .../LettuceReactiveNumberCommands.java | 161 +++ ...LettuceReactiveRedisClusterConnection.java | 128 ++ .../LettuceReactiveRedisConnection.java | 162 +++ .../lettuce/LettuceReactiveSetCommands.java | 316 +++++ .../LettuceReactiveStringCommands.java | 415 ++++++ .../lettuce/LettuceReactiveZSetCommands.java | 542 +++++++ .../data/redis/cache/RedisCacheTest.java | 3 +- ...lRedisCacheManagerWithCommitUnitTests.java | 2 +- ...edisCacheManagerWithRollbackUnitTests.java | 2 +- .../AbstractTransactionalTestBase.java | 5 +- .../LettuceConnectionFactoryTests.java | 17 + ...ttuceReactiveClusterCommandsTestsBase.java | 58 + ...activeClusterHyperLogLogCommandsTests.java | 78 + ...ettuceReactiveClusterKeyCommandsTests.java | 72 + ...ttuceReactiveClusterListCommandsTests.java | 83 ++ ...uceReactiveClusterStringCommandsTests.java | 110 ++ ...ttuceReactiveClusterZSetCommandsTests.java | 80 ++ .../LettuceReactiveCommandsTestsBase.java | 129 ++ .../LettuceReactiveGeoCommandsTests.java | 275 ++++ .../LettuceReactiveHashCommandsTests.java | 244 ++++ ...ttuceReactiveHyperLogLogCommandsTests.java | 97 ++ .../LettuceReactiveKeyCommandsTests.java | 226 +++ .../LettuceReactiveListCommandTests.java | 320 +++++ .../LettuceReactiveNumberCommandsTests.java | 82 ++ .../LettuceReactiveSetCommandsTests.java | 286 ++++ .../LettuceReactiveStringCommandsTests.java | 448 ++++++ .../LettuceReactiveZSetCommandsTests.java | 594 ++++++++ .../test/util/LettuceRedisClientProvider.java | 68 + .../LettuceRedisClusterClientProvider.java | 101 ++ .../java/reactor/test/TestSubscriber.java | 1129 +++++++++++++++ 72 files changed, 14189 insertions(+), 36 deletions(-) create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java create mode 100644 src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsTestsBase.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java create mode 100644 src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java create mode 100644 src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java create mode 100644 src/test/java/reactor/test/TestSubscriber.java diff --git a/pom.xml b/pom.xml index 82f6399a9..b22b17954 100644 --- a/pom.xml +++ b/pom.xml @@ -90,6 +90,14 @@ true + + + io.projectreactor + reactor-core + ${reactor} + true + + @@ -120,6 +128,11 @@ true + + io.reactivex + rxjava + 1.1.1 + @@ -206,7 +219,6 @@ **/*Tests.java **/*Test.java - false true ${basedir}/src/test/resources/trusted.keystore @@ -232,23 +244,6 @@ - - - release - - - - - org.jfrog.buildinfo - artifactory-maven-plugin - false - - - - - - - spring-libs-snapshot diff --git a/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java index 2ddc22412..cd8199daf 100644 --- a/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java +++ b/src/main/java/org/springframework/data/redis/connection/ClusterSlotHashUtil.java @@ -15,6 +15,10 @@ */ package org.springframework.data.redis.connection; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collection; + import org.springframework.util.Assert; /** @@ -52,6 +56,33 @@ public final class ClusterSlotHashUtil { } + /** + * @param keys must not be {@literal null}. + * @return + * @since 2.0 + */ + public static boolean isSameSlotForAllKeys(Collection keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + if (keys.size() <= 1) { + return true; + } + + return isSameSlotForAllKeys((byte[][]) keys.stream().map(ByteBuffer::array).toArray(size -> new byte[size][])); + } + + /** + * @param keys must not be {@literal null}. + * @return + * @since 2.0 + */ + public static boolean isSameSlotForAllKeys(ByteBuffer... keys) { + + Assert.notNull(keys, "Keys must not be null!"); + return isSameSlotForAllKeys(Arrays.asList(keys)); + } + /** * @param keys must not be {@literal null}. * @return diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java new file mode 100644 index 000000000..b30b44940 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterGeoCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterGeoCommands extends ReactiveGeoCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java new file mode 100644 index 000000000..2ee203a94 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHashCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterHashCommands extends ReactiveHashCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java new file mode 100644 index 000000000..290bf742c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterHyperLogLogCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterHyperLogLogCommands extends ReactiveHyperLogLogCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java new file mode 100644 index 000000000..b933ab411 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterKeyCommands.java @@ -0,0 +1,63 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.List; + +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterKeyCommands extends ReactiveKeyCommands { + + /** + * Retrieve all {@literal keys} for a given {@literal pattern} from {@link RedisNode}. + * + * @param node must not be {@literal null}. + * @param pattern must not be {@literal null}. + * @return + */ + Mono> keys(RedisClusterNode node, ByteBuffer pattern); + + /** + * Retrieve a random {@literal key} from {@link RedisNode}. + * + * @param node must not be {@literal null}. + * @return + */ + Mono randomKey(RedisClusterNode node); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java new file mode 100644 index 000000000..f5dbd4848 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterListCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterListCommands extends ReactiveListCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java new file mode 100644 index 000000000..e923a72d1 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterNumberCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterNumberCommands extends ReactiveNumberCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java new file mode 100644 index 000000000..9aa35dd16 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterSetCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterSetCommands extends ReactiveSetCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java new file mode 100644 index 000000000..a36b119c3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterStringCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterStringCommands extends ReactiveStringCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java new file mode 100644 index 000000000..f3f4e147f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveClusterZSetCommands.java @@ -0,0 +1,25 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveClusterZSetCommands extends ReactiveZSetCommands { + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java new file mode 100644 index 000000000..2e67557dc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveGeoCommands.java @@ -0,0 +1,771 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort.Direction; +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.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.Flag; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveGeoCommands { + + /** + * @author Christoph Strobl + */ + class GeoAddCommand extends KeyCommand { + + private final List> geoLocations; + + public GeoAddCommand(ByteBuffer key, List> geoLocations) { + + super(key); + this.geoLocations = geoLocations; + } + + public static GeoAddCommand location(GeoLocation geoLocation) { + return new GeoAddCommand(null, Collections.singletonList(geoLocation)); + } + + public static GeoAddCommand locations(List> geoLocations) { + return new GeoAddCommand(null, new ArrayList<>(geoLocations)); + } + + public GeoAddCommand to(ByteBuffer key) { + return new GeoAddCommand(key, geoLocations); + } + + public List> getGeoLocations() { + return geoLocations; + } + + } + + /** + * Add {@link Point} with given {@literal member} 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 + */ + default Mono geoAdd(ByteBuffer key, Point point, ByteBuffer 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 geoAdd(key, new GeoLocation<>(member, point)); + } + + /** + * Add {@link GeoLocation} to {@literal key}. + * + * @param key must not be {@literal null}. + * @param location must not be {@literal null}. + * @return + */ + default Mono geoAdd(ByteBuffer key, GeoLocation location) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(location, "location must not be null"); + + return geoAdd(key, Collections.singletonList(location)); + } + + /** + * Add {@link GeoLocation} to {@literal key}. + * + * @param key must not be {@literal null}. + * @param locations must not be {@literal null}. + * @return + */ + default Mono geoAdd(ByteBuffer key, List> locations) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(locations, "locations must not be null"); + + return geoAdd(Mono.just(GeoAddCommand.locations(locations).to(key))).next().map(NumericResponse::getOutput); + } + + /** + * Add {@link GeoLocation}s to {@literal key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> geoAdd(Publisher commands); + + /** + * @author Christoph Strobl + */ + class GeoDistCommand extends KeyCommand { + + private final ByteBuffer from; + private final ByteBuffer to; + private final Metric metric; + + private GeoDistCommand(ByteBuffer key, ByteBuffer from, ByteBuffer to, Metric metric) { + super(key); + this.from = from; + this.to = to; + this.metric = metric; + } + + static GeoDistCommand units(Metric unit) { + return new GeoDistCommand(null, null, null, unit); + } + + public static GeoDistCommand meters() { + return units(DistanceUnit.METERS); + } + + public static GeoDistCommand kiometers() { + return units(DistanceUnit.KILOMETERS); + } + + public static GeoDistCommand miles() { + return units(DistanceUnit.MILES); + } + + public static GeoDistCommand feet() { + return units(DistanceUnit.FEET); + } + + public GeoDistCommand between(ByteBuffer from) { + return new GeoDistCommand(getKey(), from, to, metric); + } + + public GeoDistCommand and(ByteBuffer to) { + return new GeoDistCommand(getKey(), from, to, metric); + } + + public GeoDistCommand forKey(ByteBuffer key) { + return new GeoDistCommand(key, from, to, metric); + } + + public ByteBuffer getFrom() { + return from; + } + + public ByteBuffer getTo() { + return to; + } + + public Optional getMetric() { + return Optional.ofNullable(metric); + } + } + + /** + * Get the {@link Distance} between {@literal from} and {@literal to}. + * + * @param key must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @return + */ + default Mono geoDist(ByteBuffer key, ByteBuffer from, ByteBuffer to) { + return geoDist(key, from, to, null); + } + + /** + * Get the {@link Distance} between {@literal from} and {@literal to}. + * + * @param key must not be {@literal null}. + * @param from must not be {@literal null}. + * @param to must not be {@literal null}. + * @param metric can be {@literal null} and defaults to {@link DistanceUnit#METERS}. + * @return + */ + default Mono geoDist(ByteBuffer key, ByteBuffer from, ByteBuffer to, Metric metric) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(from, "from must not be null"); + Assert.notNull(to, "to must not be null"); + + return geoDist(Mono.just(GeoDistCommand.units(metric).between(from).and(to).forKey(key))).next() + .map(CommandResponse::getOutput); + } + + /** + * Get the {@link Distance} between {@literal from} and {@literal to}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> geoDist(Publisher commands); + + /** + * @author Christoph Strobl + */ + class GeoHashCommand extends KeyCommand { + + private final List members; + + private GeoHashCommand(ByteBuffer key, List members) { + + super(key); + this.members = members; + } + + public static GeoHashCommand member(ByteBuffer member) { + return new GeoHashCommand(null, Collections.singletonList(member)); + } + + public static GeoHashCommand members(List members) { + return new GeoHashCommand(null, new ArrayList<>(members)); + } + + public GeoHashCommand of(ByteBuffer key) { + return new GeoHashCommand(key, members); + } + + public List getMembers() { + return members; + } + } + + /** + * Get geohash representation of the position for the one {@literal member}. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @return + */ + default Mono geoHash(ByteBuffer key, ByteBuffer member) { + + Assert.notNull(member, "member must not be null"); + + return geoHash(key, Collections.singletonList(member)).map(vals -> vals.isEmpty() ? null : vals.iterator().next()); + } + + /** + * 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 + */ + default Mono> geoHash(ByteBuffer key, List members) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(members, "members must not be null"); + + return geoHash(Mono.just(GeoHashCommand.members(members).of(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get geohash representation of the position for one or more {@literal member}s. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> geoHash(Publisher commands); + + /** + * @author Christoph Strobl + */ + class GeoPosCommand extends KeyCommand { + + private final List members; + + private GeoPosCommand(ByteBuffer key, List members) { + + super(key); + this.members = members; + } + + public static GeoPosCommand member(ByteBuffer member) { + return new GeoPosCommand(null, Collections.singletonList(member)); + } + + public static GeoPosCommand members(List members) { + return new GeoPosCommand(null, new ArrayList<>(members)); + } + + public GeoPosCommand of(ByteBuffer key) { + return new GeoPosCommand(key, members); + } + + public List getMembers() { + return members; + } + } + + /** + * Get the {@link Point} representation of positions for the {@literal member}s. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @return + */ + default Mono geoPos(ByteBuffer key, ByteBuffer member) { + + Assert.notNull(member, "member must not be null"); + + return geoPos(key, Collections.singletonList(member)).map(vals -> vals.isEmpty() ? null : vals.iterator().next()); + } + + /** + * 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 + */ + default Mono> geoPos(ByteBuffer key, List members) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(members, "members must not be null"); + + return geoPos(Mono.just(GeoPosCommand.members(members).of(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get the {@link Point} representation of positions for one or more {@literal member}s. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> geoPos(Publisher commands); + + /** + * @author Christoph Strobl + */ + class GeoRadiusCommand extends KeyCommand { + + private final Distance distance; + private final Point point; + private final GeoRadiusCommandArgs args; + private final ByteBuffer store; + private final ByteBuffer storeDist; + + private GeoRadiusCommand(ByteBuffer key, Point point, Distance distance, GeoRadiusCommandArgs args, + ByteBuffer store, ByteBuffer storeDist) { + super(key); + this.distance = distance; + this.point = point; + this.args = args == null ? GeoRadiusCommandArgs.newGeoRadiusArgs() : args; + this.store = store; + this.storeDist = storeDist; + } + + public static GeoRadiusCommand within(Distance distance) { + return new GeoRadiusCommand(null, null, distance, null, null, null); + } + + public static GeoRadiusCommand withinMeters(Double distance) { + return within(new Distance(distance, DistanceUnit.METERS)); + } + + public static GeoRadiusCommand withinKiometers(Double distance) { + return within(new Distance(distance, DistanceUnit.KILOMETERS)); + } + + public static GeoRadiusCommand withinMiles(Double distance) { + return within(new Distance(distance, DistanceUnit.MILES)); + } + + public static GeoRadiusCommand withinFeet(Double distance) { + return within(new Distance(distance, DistanceUnit.FEET)); + } + + public static GeoRadiusCommand within(Circle circle) { + return within(circle.getRadius()).from(circle.getCenter()); + } + + public GeoRadiusCommand from(Point center) { + return new GeoRadiusCommand(getKey(), center, distance, args, store, storeDist); + } + + public GeoRadiusCommand withFlag(Flag flag) { + + GeoRadiusCommandArgs args = cloneArgs(); + args.flags.add(flag); + + return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); + } + + public GeoRadiusCommand withCoord() { + return withFlag(Flag.WITHCOORD); + } + + public GeoRadiusCommand withDist() { + return withFlag(Flag.WITHDIST); + } + + public GeoRadiusCommand withArgs(GeoRadiusCommandArgs args) { + return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); + } + + public GeoRadiusCommand limitTo(Long limit) { + + GeoRadiusCommandArgs args = cloneArgs(); + if (limit != null) { + args = args.limit(limit); + } + + return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); + } + + public GeoRadiusCommand sort(Direction direction) { + + GeoRadiusCommandArgs args = cloneArgs(); + args.sortDirection = direction; + + return new GeoRadiusCommand(getKey(), point, distance, args, store, storeDist); + } + + public GeoRadiusCommand orderByDistanceAsc() { + return sort(Direction.ASC); + } + + public GeoRadiusCommand orderByDistanceDesc() { + return sort(Direction.DESC); + } + + public GeoRadiusCommand forKey(ByteBuffer key) { + return new GeoRadiusCommand(key, point, distance, args, store, storeDist); + } + + /** + * NOTE: STORE option is not compatible with WITHDIST, WITHHASH and WITHCOORDS options. + * + * @param key + * @return + */ + public GeoRadiusCommand storeAt(ByteBuffer key) { + return new GeoRadiusCommand(getKey(), point, distance, args, key, storeDist); + } + + /** + * NOTE: STOREDIST option is not compatible with WITHDIST, WITHHASH and WITHCOORDS options. + * + * @param key + * @return + */ + public GeoRadiusCommand storeDistAt(ByteBuffer key) { + return new GeoRadiusCommand(getKey(), point, distance, args, store, key); + } + + public Optional getDirection() { + return Optional.ofNullable(args.getSortDirection()); + } + + public Distance getDistance() { + return distance; + } + + public Set getFlags() { + return args.getFlags(); + } + + public Optional getLimit() { + return Optional.ofNullable(args.getLimit()); + } + + public Point getPoint() { + return point; + } + + public Optional getStore() { + return Optional.ofNullable(store); + } + + public Optional getStoreDist() { + return Optional.ofNullable(storeDist); + } + + public Optional getArgs() { + return Optional.ofNullable(args); + } + + private GeoRadiusCommandArgs cloneArgs() { + + if (args == null) { + return GeoRadiusCommandArgs.newGeoRadiusArgs(); + } + + return args.clone(); + } + } + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle}. + * + * @param key must not be {@literal null}. + * @param circle must not be {@literal null}. + * @return + */ + default Mono>> geoRadius(ByteBuffer key, Circle circle) { + return geoRadius(key, circle, null) + .map(res -> res.getContent().stream().map(val -> val.getContent()).collect(Collectors.toList())); + } + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} applying given parameters. + * + * @param key must not be {@literal null}. + * @param circle must not be {@literal null}. + * @param geoRadiusArgs can be {@literal null}. + * @return + */ + default Mono>> geoRadius(ByteBuffer key, Circle circle, + GeoRadiusCommandArgs geoRadiusArgs) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(circle, "circle must not be null"); + + return geoRadius(Mono.just(GeoRadiusCommand.within(circle).withArgs(geoRadiusArgs).forKey(key))).next() + .map(CommandResponse::getOutput); + } + + /** + * Get the {@literal member}s within the boundaries of a given {@link Circle} applying given parameters. + * + * @param commands + * @return + */ + Flux>>> geoRadius( + Publisher commands); + + /** + * @author Christoph Strobl + */ + class GeoRadiusByMemberCommand extends KeyCommand { + + private final Distance distance; + private final ByteBuffer member; + private final GeoRadiusCommandArgs args; + private final ByteBuffer store; + private final ByteBuffer storeDist; + + private GeoRadiusByMemberCommand(ByteBuffer key, ByteBuffer member, Distance distance, GeoRadiusCommandArgs args, + ByteBuffer store, ByteBuffer storeDist) { + + super(key); + this.distance = distance; + this.member = member; + this.args = args == null ? GeoRadiusCommandArgs.newGeoRadiusArgs() : args; + this.store = store; + this.storeDist = storeDist; + } + + public static GeoRadiusByMemberCommand within(Distance distance) { + return new GeoRadiusByMemberCommand(null, null, distance, GeoRadiusCommandArgs.newGeoRadiusArgs(), null, null); + } + + public static GeoRadiusByMemberCommand withinMeters(Double distance) { + return within(new Distance(distance, DistanceUnit.METERS)); + } + + public static GeoRadiusByMemberCommand withinKiometers(Double distance) { + return within(new Distance(distance, DistanceUnit.KILOMETERS)); + } + + public static GeoRadiusByMemberCommand withinMiles(Double distance) { + return within(new Distance(distance, DistanceUnit.MILES)); + } + + public static GeoRadiusByMemberCommand withinFeet(Double distance) { + return within(new Distance(distance, DistanceUnit.FEET)); + } + + public GeoRadiusByMemberCommand from(ByteBuffer member) { + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); + } + + public GeoRadiusByMemberCommand withArgs(GeoRadiusCommandArgs args) { + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); + } + + public GeoRadiusByMemberCommand withFlag(Flag flag) { + + GeoRadiusCommandArgs args = cloneArgs(); + args.flags.add(flag); + + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); + } + + public GeoRadiusByMemberCommand withCoord() { + return withFlag(Flag.WITHCOORD); + } + + public GeoRadiusByMemberCommand withDist() { + return withFlag(Flag.WITHDIST); + } + + public GeoRadiusByMemberCommand limitTo(Long limit) { + + GeoRadiusCommandArgs args = cloneArgs(); + if (limit != null) { + args = args.limit(limit); + } + + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); + } + + public GeoRadiusByMemberCommand sort(Direction direction) { + + GeoRadiusCommandArgs args = cloneArgs(); + args.sortDirection = direction; + + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, storeDist); + } + + public GeoRadiusByMemberCommand orderByDistanceAsc() { + return sort(Direction.ASC); + } + + public GeoRadiusByMemberCommand orderByDistanceDesc() { + return sort(Direction.DESC); + } + + public GeoRadiusByMemberCommand forKey(ByteBuffer key) { + return new GeoRadiusByMemberCommand(key, member, distance, args, store, storeDist); + } + + /** + * NOTE: STORE option is not compatible with WITHDIST, WITHHASH and WITHCOORDS options. + * + * @param key + * @return + */ + public GeoRadiusByMemberCommand storeAt(ByteBuffer key) { + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, key, storeDist); + } + + /** + * NOTE: STOREDIST option is not compatible with WITHDIST, WITHHASH and WITHCOORDS options. + * + * @param key + * @return + */ + public GeoRadiusByMemberCommand storeDistAt(ByteBuffer key) { + return new GeoRadiusByMemberCommand(getKey(), member, distance, args, store, key); + } + + public Optional getDirection() { + return Optional.ofNullable(args.getSortDirection()); + } + + public Distance getDistance() { + return distance; + } + + public Set getFlags() { + return args.getFlags(); + } + + public Optional getLimit() { + return Optional.ofNullable(args.getLimit()); + } + + public ByteBuffer getMember() { + return member; + } + + public Optional getStore() { + return Optional.ofNullable(store); + } + + public Optional getStoreDist() { + return Optional.ofNullable(storeDist); + } + + public Optional getArgs() { + return Optional.ofNullable(args); + } + + private GeoRadiusCommandArgs cloneArgs() { + + if (args == null) { + return GeoRadiusCommandArgs.newGeoRadiusArgs(); + } + + return args.clone(); + } + + } + + /** + * Get the {@literal member}s within given {@link Distance} from {@literal member} applying given parameters. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @return + */ + default Mono>> geoRadiusByMember(ByteBuffer key, ByteBuffer member, Distance distance) { + return geoRadiusByMember(key, member, distance, null) + .map(res -> res.getContent().stream().map(val -> val.getContent()).collect(Collectors.toList())); + } + + /** + * Get the {@literal member}s within given {@link Distance} from {@literal member} applying given parameters. + * + * @param key must not be {@literal null}. + * @param member must not be {@literal null}. + * @param geoRadiusArgs can be {@literal null}. + * @return + */ + default Mono>> geoRadiusByMember(ByteBuffer key, ByteBuffer member, + Distance distance, GeoRadiusCommandArgs geoRadiusArgs) { + + 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 geoRadiusByMember( + Mono.just(GeoRadiusByMemberCommand.within(distance).from(member).forKey(key).withArgs(geoRadiusArgs))).next() + .map(CommandResponse::getOutput); + } + + /** + * Get the {@literal member}s within given {@link Distance} from {@literal member} applying given parameters. + * + * @param commands + * @return + */ + Flux>>> geoRadiusByMember( + Publisher commands); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java new file mode 100644 index 000000000..47969e569 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHashCommands.java @@ -0,0 +1,414 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveHashCommands { + + /** + * @author Christoph Strobl + */ + class HSetCommand extends KeyCommand { + + private static final ByteBuffer SINGLE_VALUE_KEY = ByteBuffer.allocate(0); + private final Map fieldValueMap; + private final Boolean upsert; + + private HSetCommand(ByteBuffer key, Map keyValueMap, Boolean upsert) { + + super(key); + this.fieldValueMap = keyValueMap; + this.upsert = upsert; + } + + public static HSetCommand value(ByteBuffer value) { + return new HSetCommand(null, Collections.singletonMap(SINGLE_VALUE_KEY, value), Boolean.TRUE); + } + + public static HSetCommand fieldValues(Map fieldValueMap) { + return new HSetCommand(null, fieldValueMap, Boolean.TRUE); + } + + public HSetCommand ofField(ByteBuffer field) { + + if (!fieldValueMap.containsKey(SINGLE_VALUE_KEY)) { + throw new InvalidDataAccessApiUsageException("Value has not been set."); + } + + return new HSetCommand(getKey(), Collections.singletonMap(field, fieldValueMap.get(SINGLE_VALUE_KEY)), upsert); + } + + public HSetCommand forKey(ByteBuffer key) { + return new HSetCommand(key, fieldValueMap, upsert); + } + + public HSetCommand ifValueNotExists() { + return new HSetCommand(getKey(), fieldValueMap, Boolean.FALSE); + } + + public Boolean isUpsert() { + return upsert; + } + + public Map getFieldValueMap() { + return fieldValueMap; + } + } + + /** + * Set the {@code value} of a hash {@code field}. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono hSet(ByteBuffer key, ByteBuffer field, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(field, "field must not be null"); + Assert.notNull(value, "value must not be null"); + + return hSet(Mono.just(HSetCommand.value(value).ofField(field).forKey(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Set the {@code value} of a hash {@code field}. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono hSetNX(ByteBuffer key, ByteBuffer field, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(field, "field must not be null"); + Assert.notNull(value, "value must not be null"); + + return hSet(Mono.just(HSetCommand.value(value).ofField(field).forKey(key).ifValueNotExists())).next() + .map(BooleanResponse::getOutput); + } + + /** + * Set multiple hash fields to multiple values using data provided in {@code fieldValueMap}. + * + * @param key must not be {@literal null}. + * @param fieldValueMap must not be {@literal null}. + * @return + */ + default Mono hMSet(ByteBuffer key, Map fieldValueMap) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(fieldValueMap, "field must not be null"); + + return hSet(Mono.just(HSetCommand.fieldValues(fieldValueMap).forKey(key).ifValueNotExists())).next() + .map(BooleanResponse::getOutput); + } + + /** + * Set the {@code value} of a hash {@code field}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hSet(Publisher commands); + + /** + * @author Christoph Strobl + */ + class HGetCommand extends KeyCommand { + + private List fields; + + private HGetCommand(ByteBuffer key, List fields) { + + super(key); + this.fields = fields; + } + + public static HGetCommand field(ByteBuffer field) { + return new HGetCommand(null, Collections.singletonList(field)); + } + + public static HGetCommand fields(List fields) { + return new HGetCommand(null, new ArrayList<>(fields)); + } + + public HGetCommand from(ByteBuffer key) { + return new HGetCommand(key, fields); + } + + public List getFields() { + return fields; + } + } + + /** + * Get value for given {@code field} from hash at {@code key}. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return + */ + default Mono hGet(ByteBuffer key, ByteBuffer field) { + return hMGet(key, Collections.singletonList(field)).map(val -> val.isEmpty() ? null : val.iterator().next()); + } + + /** + * Get values for given {@code fields} from hash at {@code key}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return + */ + default Mono> hMGet(ByteBuffer key, List fields) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(fields, "fields must not be null"); + + return hMGet(Mono.just(HGetCommand.fields(fields).from(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get values for given {@code fields} from hash at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hMGet(Publisher commands); + + /** + * @author Christoph Strobl + */ + class HExistsCommand extends KeyCommand { + + private final ByteBuffer field; + + private HExistsCommand(ByteBuffer key, ByteBuffer field) { + + super(key); + this.field = field; + } + + public static HExistsCommand field(ByteBuffer field) { + return new HExistsCommand(null, field); + } + + public HExistsCommand in(ByteBuffer key) { + return new HExistsCommand(key, field); + } + + public ByteBuffer getField() { + return field; + } + } + + /** + * Determine if given hash {@code field} exists. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return + */ + default Mono hExists(ByteBuffer key, ByteBuffer field) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(field, "field must not be null"); + + return hExists(Mono.just(HExistsCommand.field(field).in(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Determine if given hash {@code field} exists. + * + * @param commands + * @return + */ + Flux> hExists(Publisher commands); + + /** + * @author Christoph Strobl + */ + class HDelCommand extends KeyCommand { + + private final List fields; + + private HDelCommand(ByteBuffer key, List fields) { + super(key); + this.fields = fields; + } + + public static HDelCommand field(ByteBuffer field) { + return new HDelCommand(null, Collections.singletonList(field)); + } + + public static HDelCommand fields(List fields) { + return new HDelCommand(null, new ArrayList<>(fields)); + } + + public HDelCommand from(ByteBuffer key) { + return new HDelCommand(key, fields); + } + + public List getFields() { + return fields; + } + } + + /** + * Delete given hash {@code field}. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @return + */ + default Mono hDel(ByteBuffer key, ByteBuffer field) { + + Assert.notNull(field, "field must not be null"); + + return hDel(key, Collections.singletonList(field)).map(val -> val > 0 ? Boolean.TRUE : Boolean.FALSE); + } + + /** + * Delete given hash {@code fields}. + * + * @param key must not be {@literal null}. + * @param fields must not be {@literal null}. + * @return + */ + default Mono hDel(ByteBuffer key, List fields) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(fields, "fields must not be null"); + + return hDel(Mono.just(HDelCommand.fields(fields).from(key))).next().map(NumericResponse::getOutput); + } + + /** + * Delete given hash {@code fields}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hDel(Publisher commands); + + /** + * Get size of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono hLen(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return hLen(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get size of hash at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hLen(Publisher commands); + + /** + * Get key set (fields) of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono> hKeys(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return hKeys(Mono.just(new KeyCommand(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get key set (fields) of hash at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hKeys(Publisher commands); + + /** + * Get entry set (values) of hash at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono> hVals(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return hVals(Mono.just(new KeyCommand(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get entry set (values) of hash at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> hVals(Publisher commands); + + /** + * Get entire hash stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono> hGetAll(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return hGetAll(Mono.just(new KeyCommand(key))).next().map(CommandResponse::getOutput); + } + + /** + * Get entire hash stored at {@code key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux>> hGetAll(Publisher commands); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java new file mode 100644 index 000000000..b93bee6b5 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveHyperLogLogCommands.java @@ -0,0 +1,222 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.Command; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveHyperLogLogCommands { + + /** + * @author Christoph Strobl + */ + class PfAddCommand extends KeyCommand { + + private final List values; + + private PfAddCommand(ByteBuffer key, List values) { + + super(key); + this.values = values; + } + + public static PfAddCommand value(ByteBuffer value) { + return values(Collections.singletonList(value)); + } + + public static PfAddCommand values(List values) { + return new PfAddCommand(null, new ArrayList<>(values)); + } + + public PfAddCommand to(ByteBuffer key) { + return new PfAddCommand(key, values); + } + + public List getValues() { + return values; + } + } + + /** + * Adds given {@literal value} to the HyperLogLog stored at given {@literal key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono pfAdd(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(value, "value must not be null"); + + return pfAdd(key, Collections.singletonList(value)); + } + + /** + * Adds given {@literal values} to the HyperLogLog stored at given {@literal key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono pfAdd(ByteBuffer key, List values) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(values, "values must not be null"); + + return pfAdd(Mono.just(PfAddCommand.values(values).to(key))).next().map(NumericResponse::getOutput); + } + + /** + * Adds given {@literal values} to the HyperLogLog stored at given {@literal key}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> pfAdd(Publisher commands); + + /** + * @author Christoph Strobl + */ + class PfCountCommand implements Command { + + private final List keys; + + private PfCountCommand(List keys) { + + super(); + this.keys = keys; + } + + public static PfCountCommand valueIn(ByteBuffer key) { + return valuesIn(Collections.singletonList(key)); + } + + public static PfCountCommand valuesIn(List keys) { + return new PfCountCommand(keys); + } + + public List getKeys() { + return keys; + } + + @Override + public ByteBuffer getKey() { + return null; + } + + } + + /** + * Return the approximated cardinality of the structures observed by the HyperLogLog at {@literal key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono pfCount(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return pfCount(Collections.singletonList(key)); + } + + /** + * Return the approximated cardinality of the structures observed by the HyperLogLog at {@literal key(s)}. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono pfCount(List keys) { + + Assert.notNull(keys, "keys must not be null"); + + return pfCount(Mono.just(PfCountCommand.valuesIn(keys))).next().map(NumericResponse::getOutput); + } + + /** + * Return the approximated cardinality of the structures observed by the HyperLogLog at {@literal key(s)}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> pfCount(Publisher commands); + + /** + * @author Christoph Strobl + */ + class PfMergeCommand extends KeyCommand { + + private final List sourceKeys; + + private PfMergeCommand(ByteBuffer key, List sourceKeys) { + + super(key); + this.sourceKeys = sourceKeys; + } + + public static PfMergeCommand valuesIn(List sourceKeys) { + return new PfMergeCommand(null, sourceKeys); + } + + public PfMergeCommand into(ByteBuffer destinationKey) { + return new PfMergeCommand(destinationKey, sourceKeys); + } + + public List getSourceKeys() { + return sourceKeys; + } + } + + /** + * Merge N different HyperLogLogs at {@literal sourceKeys} into a single {@literal destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param sourceKeys must not be {@literal null}. + * @return + */ + default Mono pfMerge(ByteBuffer destinationKey, List sourceKeys) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(sourceKeys, "sourceKeys must not be null"); + + return pfMerge(Mono.just(PfMergeCommand.valuesIn(sourceKeys).into(destinationKey))).next() + .map(BooleanResponse::getOutput); + } + + /** + * Merge N different HyperLogLogs at {@literal sourceKeys} into a single {@literal destinationKey}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> pfMerge(Publisher commands); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java new file mode 100644 index 000000000..46bd456f2 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveKeyCommands.java @@ -0,0 +1,215 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveKeyCommands { + + /** + * Determine if given {@code key} exists. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono exists(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return exists(Mono.just(new KeyCommand(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Determine if given {@code key} exists. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux> exists(Publisher keys); + + /** + * Determine the type stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono type(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return type(Mono.just(new KeyCommand(key))).next().map(CommandResponse::getOutput); + } + + /** + * Determine the type stored at {@code key}. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux> type(Publisher keys); + + /** + * Find all keys matching the given {@code pattern}. + * + * @param pattern must not be {@literal null}. + * @return + */ + default Mono> keys(ByteBuffer pattern) { + + Assert.notNull(pattern, "pattern must not be null"); + + return keys(Mono.just(pattern)).next().map(MultiValueResponse::getOutput); + } + + /** + * Return a random key from the keyspace. + * + * @return + */ + Mono randomKey(); + + /** + * Find all keys matching the given {@code pattern}. + * + * @param patterns must not be {@literal null}. + * @return + */ + Flux> keys(Publisher patterns); + + /** + * @author Christoph Strobl + */ + class RenameCommand extends KeyCommand { + + private ByteBuffer newName; + + private RenameCommand(ByteBuffer key, ByteBuffer newName) { + + super(key); + this.newName = newName; + } + + public static ReactiveKeyCommands.RenameCommand key(ByteBuffer key) { + return new RenameCommand(key, null); + } + + public ReactiveKeyCommands.RenameCommand to(ByteBuffer newName) { + return new RenameCommand(getKey(), newName); + } + + public ByteBuffer getNewName() { + return newName; + } + } + + /** + * Rename key {@code oleName} to {@code newName}. + * + * @param key must not be {@literal null}. + * @param newName must not be {@literal null}. + * @return + */ + default Mono rename(ByteBuffer key, ByteBuffer newName) { + + Assert.notNull(key, "key must not be null"); + + return rename(Mono.just(RenameCommand.key(key).to(newName))).next().map(BooleanResponse::getOutput); + } + + Flux> rename(Publisher cmd); + + /** + * Rename key {@code oleName} to {@code newName} only if {@code newName} does not exist. + * + * @param key must not be {@literal null}. + * @param newName must not be {@literal null}. + * @return + */ + default Mono renameNX(ByteBuffer key, ByteBuffer newName) { + + Assert.notNull(key, "key must not be null"); + + return renameNX(Mono.just(RenameCommand.key(key).to(newName))).next().map(BooleanResponse::getOutput); + } + + /** + * Rename key {@code oleName} to {@code newName} only if {@code newName} does not exist. + * + * @param keys must not be {@literal null}. + * @param newName must not be {@literal null}. + * @return + */ + Flux> renameNX( + Publisher command); + + /** + * Delete {@literal key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono del(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return del(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Delete {@literal keys} one by one. + * + * @param keys must not be {@literal null}. + * @return {@link Flux} of {@link DelResponse} holding the {@literal key} removed along with the deletion result. + */ + Flux> del(Publisher keys); + + /** + * Delete multiple {@literal keys} one in one batch. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono mDel(List keys) { + + Assert.notEmpty(keys, "Keys must not be empty or null!"); + + return mDel(Mono.just(keys)).next().map(NumericResponse::getOutput); + } + + /** + * Delete multiple {@literal keys} in batches. + * + * @param keys must not be {@literal null}. + * @return {@link Flux} of {@link MDelResponse} holding the {@literal keys} removed along with the deletion result. + */ + Flux, Long>> mDel(Publisher> keys); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java new file mode 100644 index 000000000..844a40ec4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveListCommands.java @@ -0,0 +1,820 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Collections; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.Command; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveListCommands { + + /** + * @author Christoph Strobl + */ + enum Direction { + LEFT, RIGHT + } + + /** + * @author Christoph Strobl + */ + class PushCommand extends KeyCommand { + + private List values; + private boolean upsert; + private Direction direction; + + private PushCommand(ByteBuffer key, List values, Direction direction, boolean upsert) { + + super(key); + this.values = values; + this.upsert = upsert; + this.direction = direction; + } + + public static PushCommand right() { + return new PushCommand(null, null, Direction.RIGHT, true); + } + + public static PushCommand left() { + return new PushCommand(null, null, Direction.LEFT, true); + } + + public PushCommand value(ByteBuffer value) { + return new PushCommand(null, Collections.singletonList(value), direction, upsert); + } + + public PushCommand values(List values) { + return new PushCommand(null, values, direction, upsert); + } + + public PushCommand to(ByteBuffer key) { + return new PushCommand(key, values, direction, upsert); + } + + public PushCommand ifExists() { + return new PushCommand(getKey(), values, direction, false); + } + + public List getValues() { + return values; + } + + public boolean getUpsert() { + return upsert; + } + + public Direction getDirection() { + return direction; + } + } + + /** + * Append {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono rPush(ByteBuffer key, List values) { + + Assert.notNull(key, "command must not be null!"); + Assert.notNull(values, "Values must not be null!"); + + return push(Mono.just(PushCommand.right().values(values).to(key))).next().map(NumericResponse::getOutput); + } + + /** + * Append {@code values} to {@code key} only if {@code key} already exists. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono rPushX(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "command must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return push(Mono.just(PushCommand.right().value(value).to(key).ifExists())).next().map(NumericResponse::getOutput); + } + + /** + * Prepend {@code values} to {@code key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono lPush(ByteBuffer key, List values) { + + Assert.notNull(key, "command must not be null!"); + Assert.notNull(values, "Values must not be null!"); + + return push(Mono.just(PushCommand.left().values(values).to(key))).next().map(NumericResponse::getOutput); + } + + /** + * Prepend {@code value} to {@code key} if {@code key} already exists. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono lPushX(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "command must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return push(Mono.just(PushCommand.left().value(value).to(key).ifExists())).next().map(NumericResponse::getOutput); + } + + /** + * Prepend {@link PushCommand#getValues()} to {@link PushCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> push(Publisher commands); + + /** + * Get the size of list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono lLen(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return lLen(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get the size of list stored at {@link KeyCommand#getKey()} + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lLen(Publisher commands); + + /** + * 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 + */ + default Mono> lRange(ByteBuffer key, long start, long end) { + + Assert.notNull(key, "key must not be null"); + + return lRange(Mono.just(RangeCommand.key(key).fromIndex(start).toIndex(end))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get elements in {@link RangeCommand#getRange()} from list at {@link RangeCommand#getKey()} + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lRange(Publisher commands); + + /** + * Trim list at {@code key} to elements between {@code begin} and {@code end}. + * + * @param key must not be {@literal null}. + * @param start + * @param end + * @return + */ + default Mono lTrim(ByteBuffer key, long start, long end) { + + Assert.notNull(key, "key must not be null"); + + return lTrim(Mono.just(RangeCommand.key(key).fromIndex(start).toIndex(end))).next().map(BooleanResponse::getOutput); + } + + /** + * Trim list at {@link RangeCommand#getKey()} to elements within {@link RangeCommand#getRange()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lTrim(Publisher commands); + + /** + * @author Christoph Strobl + */ + class LIndexCommand extends KeyCommand { + + private final Long index; + + private LIndexCommand(ByteBuffer key, Long index) { + + super(key); + this.index = index; + } + + public static LIndexCommand elementAt(Long index) { + return new LIndexCommand(null, index); + } + + public LIndexCommand from(ByteBuffer key) { + return new LIndexCommand(key, index); + } + + public Long getIndex() { + return index; + } + } + + /** + * Get element at {@code index} form list at {@code key}. + * + * @param key must not be {@literal null}. + * @param index + * @return + */ + default Mono lIndex(ByteBuffer key, long index) { + + Assert.notNull(key, "key must not be null"); + + return lIndex(Mono.just(LIndexCommand.elementAt(index).from(key))).next().map(ByteBufferResponse::getOutput); + } + + /** + * Get element at {@link LIndexCommand#getIndex()} form list at {@link LIndexCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lIndex(Publisher commands); + + /** + * @author Christoph Strobl + */ + class LInsertCommand extends KeyCommand { + + private final Position position; + private final ByteBuffer pivot; + private final ByteBuffer value; + + public LInsertCommand(ByteBuffer key, Position position, ByteBuffer pivot, ByteBuffer value) { + + super(key); + this.position = position; + this.pivot = pivot; + this.value = value; + } + + public static LInsertCommand value(ByteBuffer value) { + return new LInsertCommand(null, null, null, value); + } + + public LInsertCommand before(ByteBuffer pivot) { + return new LInsertCommand(getKey(), Position.BEFORE, pivot, value); + } + + public LInsertCommand after(ByteBuffer pivot) { + return new LInsertCommand(getKey(), Position.AFTER, pivot, value); + } + + public LInsertCommand forKey(ByteBuffer key) { + return new LInsertCommand(key, position, pivot, value); + } + + public ByteBuffer getValue() { + return value; + } + + public Position getPosition() { + return position; + } + + public ByteBuffer getPivot() { + return pivot; + } + } + + /** + * Insert {@code value} {@link Position#BEFORE} or {@link Position#AFTER} existing {@code pivot} for {@code key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono lInsert(ByteBuffer key, Position position, ByteBuffer pivot, ByteBuffer value) { + + Assert.notNull(key, "key must not be null!"); + Assert.notNull(position, "position must not be null!"); + Assert.notNull(pivot, "pivot must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + LInsertCommand command = LInsertCommand.value(value); + command = Position.BEFORE.equals(position) ? command.before(pivot) : command.after(pivot); + command = command.forKey(key); + return lInsert(Mono.just(command)).next().map(NumericResponse::getOutput); + } + + /** + * Insert {@link LInsertCommand#getValue()} {@link Position#BEFORE} or {@link Position#AFTER} existing + * {@link LInsertCommand#getPivot()} for {@link LInsertCommand#getKey()} + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lInsert(Publisher commands); + + /** + * @author Christoph Strobl + */ + class LSetCommand extends KeyCommand { + + private final Long index; + private final ByteBuffer value; + + private LSetCommand(ByteBuffer key, Long index, ByteBuffer value) { + + super(key); + this.index = index; + this.value = value; + } + + public static LSetCommand elementAt(Long index) { + return new LSetCommand(null, index, null); + } + + public LSetCommand to(ByteBuffer value) { + return new LSetCommand(getKey(), index, value); + } + + public LSetCommand forKey(ByteBuffer key) { + return new LSetCommand(key, index, value); + } + + public ByteBuffer getValue() { + return value; + } + + public Long getIndex() { + return index; + } + } + + /** + * Set the {@code value} list element at {@code index}. + * + * @param key must not be {@literal null}. + * @param index + * @param value must not be {@literal null}. + * @return + */ + default Mono lSet(ByteBuffer key, long index, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return lSet(Mono.just(LSetCommand.elementAt(index).to(value).forKey(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Set the {@link LSetCommand#getValue()} list element at {@link LSetCommand#getKey()}. + * + * @param commands + * @return + */ + Flux> lSet(Publisher commands); + + /** + * @author Christoph Strobl + */ + class LRemCommand extends KeyCommand { + + private final Long count; + private final ByteBuffer value; + + private LRemCommand(ByteBuffer key, Long count, ByteBuffer value) { + super(key); + this.count = count; + this.value = value; + } + + public static LRemCommand all() { + return new LRemCommand(null, 0L, null); + } + + public static LRemCommand first(Long count) { + return new LRemCommand(null, count, null); + } + + public static LRemCommand last(Long count) { + + Long value = count < 0L ? count : Math.negateExact(count); + return new LRemCommand(null, value, null); + } + + public LRemCommand occurancesOf(ByteBuffer value) { + return new LRemCommand(getKey(), count, value); + } + + public LRemCommand from(ByteBuffer key) { + return new LRemCommand(key, count, value); + } + + public Long getCount() { + return count; + } + + public ByteBuffer getValue() { + return value; + } + } + + /** + * Removes all occurrences of {@code value} from the list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono lRem(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return lRem(Mono.just(LRemCommand.all().occurancesOf(value).from(key))).next().map(NumericResponse::getOutput); + } + + /** + * 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 must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono lRem(ByteBuffer key, Long count, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(count, "count must not be null"); + Assert.notNull(value, "value must not be null"); + + return lRem(Mono.just(LRemCommand.first(count).occurancesOf(value).from(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Removes the {@link LRemCommand#getCount()} occurrences of {@link LRemCommand#getValue()} from the list stored at + * {@link LRemCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> lRem(Publisher commands); + + /** + * @author Christoph Strobl + */ + class PopCommand extends KeyCommand { + + private final Direction direction; + + private PopCommand(ByteBuffer key, Direction direction) { + + super(key); + this.direction = direction; + } + + public static PopCommand right() { + return new PopCommand(null, Direction.RIGHT); + } + + public static PopCommand left() { + return new PopCommand(null, Direction.LEFT); + } + + public PopCommand from(ByteBuffer key) { + return new PopCommand(key, direction); + } + + public Direction getDirection() { + return direction; + } + + } + + /** + * Removes and returns first element in list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono lPop(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return pop(Mono.just(PopCommand.left().from(key))).next().map(ByteBufferResponse::getOutput); + } + + /** + * Removes and returns last element in list stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono rPop(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return pop(Mono.just(PopCommand.right().from(key))).next().map(ByteBufferResponse::getOutput); + } + + /** + * Removes and returns last element in list stored at {@link KeyCommand#getKey()} + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> pop(Publisher commands); + + /** + * @author Christoph Strobl + */ + class BPopCommand implements Command { + + private final List keys; + private final Duration timeout; + private final Direction direction; + + private BPopCommand(List keys, Duration timeout, Direction direction) { + this.keys = keys; + this.timeout = timeout; + this.direction = direction; + } + + public static BPopCommand right() { + return new BPopCommand(null, Duration.ZERO, Direction.RIGHT); + } + + public static BPopCommand left() { + return new BPopCommand(null, Duration.ZERO, Direction.LEFT); + } + + public BPopCommand from(List keys) { + return new BPopCommand(keys, Duration.ZERO, direction); + } + + public BPopCommand blockingFor(Duration timeout) { + return new BPopCommand(keys, timeout, direction); + } + + @Override + public ByteBuffer getKey() { + return null; + } + + public List getKeys() { + return keys; + } + + public Duration getTimeout() { + return timeout; + } + + public Direction getDirection() { + return direction; + } + + } + + /** + * @author Christoph Strobl + */ + class PopResult { + + private final List result; + + public PopResult(List result) { + this.result = result; + } + + public ByteBuffer getKey() { + return ObjectUtils.isEmpty(result) ? null : result.get(0); + } + + public ByteBuffer getValue() { + return ObjectUtils.isEmpty(result) ? null : result.get(1); + } + + public List getRaw() { + return Collections.unmodifiableList(result); + } + } + + /** + * @author Christoph Strobl + */ + class PopResponse extends CommandResponse { + + public PopResponse(BPopCommand input, PopResult output) { + super(input, output); + } + + } + + /** + * Removes and returns first element from lists stored at {@code keys}.
+ * Blocks connection until element available or {@code timeout} reached. + * + * @param keys must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return + */ + default Mono blPop(List keys, Duration timeout) { + + Assert.notNull(keys, "keys must not be null."); + Assert.notNull(timeout, "timeout must not be null."); + + return bPop(Mono.just(BPopCommand.left().from(keys).blockingFor(timeout))).next().map(PopResponse::getOutput); + } + + /** + * Removes and returns last element from lists stored at {@code keys}.
+ * Blocks connection until element available or {@code timeout} reached. + * + * @param keys must not be {@literal null}. + * @param timeout must not be {@literal null}. + * @return + */ + default Mono brPop(List keys, Duration timeout) { + + Assert.notNull(keys, "keys must not be null."); + Assert.notNull(timeout, "timeout must not be null."); + + return bPop(Mono.just(BPopCommand.right().from(keys).blockingFor(timeout))).next().map(PopResponse::getOutput); + } + + /** + * Removes and returns the top {@link BPopCommand#getDirection()} element from lists stored at + * {@link BPopCommand#getKeys()}.
+ * Blocks connection until element available or {@link BPopCommand#getTimeout()} reached. + * + * @param commands + * @return + */ + Flux bPop(Publisher commands); + + /** + * @author Christoph Strobl + */ + class RPopLPushCommand extends KeyCommand { + + private final ByteBuffer destination; + + private RPopLPushCommand(ByteBuffer key, ByteBuffer destination) { + + super(key); + this.destination = destination; + } + + public static RPopLPushCommand from(ByteBuffer sourceKey) { + return new RPopLPushCommand(sourceKey, null); + } + + public RPopLPushCommand to(ByteBuffer destinationKey) { + return new RPopLPushCommand(getKey(), destinationKey); + } + + public ByteBuffer getDestination() { + return destination; + } + + } + + /** + * Remove the last element from list at {@code source}, append it to {@code destination} and return its value. + * + * @param source must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + default Mono rPopLPush(ByteBuffer source, ByteBuffer destination) { + + Assert.notNull(source, "source must not be null"); + Assert.notNull(destination, "destination must not be null"); + + return rPopLPush(Mono.just(RPopLPushCommand.from(source).to(destination))).next() + .map(ByteBufferResponse::getOutput); + } + + /** + * Remove the last element from list at {@link RPopLPushCommand#getKey()}, append it to + * {@link RPopLPushCommand#getDestination()} and return its value. + * + * @param source must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + Flux> rPopLPush(Publisher commands); + + /** + * @author Christoph Strobl + */ + class BRPopLPushCommand extends KeyCommand { + + private final ByteBuffer destination; + private final Duration timeout; + + private BRPopLPushCommand(ByteBuffer key, ByteBuffer destination, Duration timeout) { + + super(key); + this.destination = destination; + this.timeout = timeout; + } + + public static BRPopLPushCommand from(ByteBuffer sourceKey) { + return new BRPopLPushCommand(sourceKey, null, null); + } + + public BRPopLPushCommand to(ByteBuffer destinationKey) { + return new BRPopLPushCommand(getKey(), destinationKey, timeout); + } + + public BRPopLPushCommand blockingFor(Duration timeout) { + return new BRPopLPushCommand(getKey(), destination, timeout); + } + + public ByteBuffer getDestination() { + return destination; + } + + public Duration getTimeout() { + return timeout; + } + } + + /** + * Remove the last element from list at {@code source}, append it to {@code destination} and return its value. + * Blocks connection until element available or {@code timeout} reached.
+ * + * @param source must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + default Mono bRPopLPush(ByteBuffer source, ByteBuffer destination, Duration timeout) { + + Assert.notNull(source, "source must not be null"); + Assert.notNull(destination, "destination must not be null"); + + return bRPopLPush(Mono.just(BRPopLPushCommand.from(source).to(destination).blockingFor(timeout))).next() + .map(ByteBufferResponse::getOutput); + } + + /** + * Remove the last element from list at {@link BRPopLPushCommand#getKey()}, append it to + * {@link BRPopLPushCommand#getDestination()} and return its value.
+ * Blocks connection until element available or {@link BRPopLPushCommand#getTimeout()} reached. + * + * @param source must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + Flux> bRPopLPush(Publisher commands); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java new file mode 100644 index 000000000..7c4957c5e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveNumberCommands.java @@ -0,0 +1,248 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveNumberCommands { + + /** + * Increment value of {@code key} by 1. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono incr(ByteBuffer key) { + + + Assert.notNull(key, "key must not be null"); + + return incr(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Increment value of {@code key} by 1. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux> incr(Publisher keys); + + /** + * @author Christoph Strobl + */ + class IncrByCommand extends KeyCommand { + + private T value; + + private IncrByCommand(ByteBuffer key, T value) { + super(key); + this.value = value; + } + + public static IncrByCommand incr(ByteBuffer key) { + return new IncrByCommand(key, null); + } + + public IncrByCommand by(T value) { + return new IncrByCommand(getKey(), value); + } + + public T getValue() { + return value; + } + + } + + /** + * Increment value of {@code key} by {@code value}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono incrBy(ByteBuffer key, T value) { + + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + + return incrBy(Mono.just(IncrByCommand. incr(key).by(value))).next().map(NumericResponse::getOutput); + } + + /** + * Increment value of {@code key} by {@code value}. + * + * @param keys must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + Flux, T>> incrBy( + Publisher> commands); + + /** + * @author Christoph Strobl + */ + class DecrByCommand extends KeyCommand { + + private T value; + + private DecrByCommand(ByteBuffer key, T value) { + super(key); + this.value = value; + } + + public static ReactiveNumberCommands.DecrByCommand decr(ByteBuffer key) { + return new DecrByCommand(key, null); + } + + public ReactiveNumberCommands.DecrByCommand by(T value) { + return new DecrByCommand(getKey(), value); + } + + public T getValue() { + return value; + } + + } + + /** + * Decrement value of {@code key} by 1. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono decr(ByteBuffer key) { + + + Assert.notNull(key, "key must not be null"); + + + return decr(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Decrement value of {@code key} by 1. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux> decr(Publisher keys); + + /** + * Decrement value of {@code key} by {@code value}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono decrBy(ByteBuffer key, T value) { + + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + + return decrBy(Mono.just(DecrByCommand. decr(key).by(value))).next().map(NumericResponse::getOutput); + } + + /** + * Decrement value of {@code key} by {@code value}. + * + * @param keys must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + Flux, T>> decrBy( + Publisher> commands); + + /** + * @author Christoph Strobl + */ + class HIncrByCommand extends KeyCommand { + + private final ByteBuffer field; + private final T value; + + private HIncrByCommand(ByteBuffer key, ByteBuffer field, T value) { + + super(key); + this.field = field; + this.value = value; + } + + public static HIncrByCommand incr(ByteBuffer field) { + return new HIncrByCommand(null, field, null); + } + + public HIncrByCommand by(T value) { + return new HIncrByCommand(getKey(), field, value); + } + + public HIncrByCommand forKey(ByteBuffer key) { + return new HIncrByCommand(key, field, value); + } + + public T getValue() { + return value; + } + + public ByteBuffer getField() { + return field; + } + } + + /** + * Increment {@code value} of a hash {@code field} by the given {@code value}. + * + * @param key must not be {@literal null}. + * @param field must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono hIncrBy(ByteBuffer key, ByteBuffer field, T value) { + + + Assert.notNull(key, "key must not be null"); + Assert.notNull(field, "field must not be null"); + Assert.notNull(value, "value must not be null"); + + + return hIncrBy(Mono.just(HIncrByCommand. incr(field).by(value).forKey(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Increment {@code value} of a hash {@code field} by the given {@code value}. + * + * @return + */ + Flux, T>> hIncrBy(Publisher> commands); + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java new file mode 100644 index 000000000..848525f49 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisClusterConnection.java @@ -0,0 +1,51 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveRedisClusterConnection extends ReactiveRedisConnection { + + @Override + ReactiveClusterKeyCommands keyCommands(); + + @Override + ReactiveClusterStringCommands stringCommands(); + + @Override + ReactiveClusterNumberCommands numberCommands(); + + @Override + ReactiveClusterListCommands listCommands(); + + @Override + ReactiveClusterSetCommands setCommands(); + + @Override + ReactiveClusterZSetCommands zSetCommands(); + + @Override + ReactiveClusterHashCommands hashCommands(); + + @Override + ReactiveClusterGeoCommands geoCommands(); + + @Override + ReactiveClusterHyperLogLogCommands hyperLogLogCommands(); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java new file mode 100644 index 000000000..d380341c8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveRedisConnection.java @@ -0,0 +1,256 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.DirectFieldAccessor; +import org.springframework.data.domain.Range; + +import lombok.Data; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveRedisConnection extends Closeable { + + /** + * Get {@link ReactiveKeyCommands}. + * + * @return never {@literal null}. + */ + ReactiveKeyCommands keyCommands(); + + /** + * Get {@link ReactiveStringCommands}. + * + * @return never {@literal null}. + */ + ReactiveStringCommands stringCommands(); + + /** + * Get {@link ReactiveNumberCommands} + * + * @return never {@literal null}. + */ + ReactiveNumberCommands numberCommands(); + + /** + * Get {@link ReactiveListCommands}. + * + * @return never {@literal null}. + */ + ReactiveListCommands listCommands(); + + /** + * Get {@link ReactiveSetCommands}. + * + * @return never {@literal null}. + */ + ReactiveSetCommands setCommands(); + + /** + * Get {@link ReactiveZSetCommands}. + * + * @return never {@literal null}. + */ + ReactiveZSetCommands zSetCommands(); + + /** + * Get {@link ReactiveHashCommands}. + * + * @return + */ + ReactiveHashCommands hashCommands(); + + /** + * Get {@link ReacktiveGeoCommands} + * + * @return never {@literal null}. + */ + ReactiveGeoCommands geoCommands(); + + /** + * Get {@link ReactiveHyperLogLogCommands}. + * + * @return never {@literal null}. + */ + ReactiveHyperLogLogCommands hyperLogLogCommands(); + + interface Command { + + ByteBuffer getKey(); + + default String getName() { + return getClass().getSimpleName().replace("Command", "").toUpperCase(); + } + + static Builder create(Class type) { + return new CommandBuilder(type); + } + + interface Builder extends Consumer { + + default Builder forKey(String key) { + return forKey(key.getBytes(Charset.forName("UTF-8"))); + } + + default Builder forKey(byte[] key) { + return forKey(ByteBuffer.wrap(key)); + } + + default Builder forKey(ByteBuffer key) { + return forKey(() -> key); + } + + Builder forKey(Supplier keySupplier); + + T build(); + } + + class CommandBuilder implements Builder { + + List argumentList = new ArrayList<>(); + + Class type; + Supplier key; + + public CommandBuilder(Class type) { + this.type = type; + } + + public Builder forKey(Supplier key) { + this.key = key; + return this; + } + + @Override + public void accept(Object t) { + argumentList.add(t); + } + + @Override + public T build() { + + try { + T x = BeanUtils.instantiateClass(type); + + DirectFieldAccessor dfa = new DirectFieldAccessor(x); + dfa.setPropertyValue("key", key); + return x; + + } catch (IllegalArgumentException | SecurityException e) { + throw new IllegalArgumentException(" ¯\\_(ツ)_/¯", e); + } + } + } + } + + /** + * @author Christoph Strobl + */ + class KeyCommand implements Command { + + private ByteBuffer key; + + public KeyCommand(ByteBuffer key) { + this.key = key; + } + + @Override + public ByteBuffer getKey() { + return key; + } + } + + /** + * @author Christoph Strobl + */ + class RangeCommand extends KeyCommand { + + Range range; + + public RangeCommand(ByteBuffer key, Range range) { + + super(key); + this.range = range != null ? range : new Range<>(0L, Long.MAX_VALUE); + } + + public static RangeCommand key(ByteBuffer key) { + return new RangeCommand(key, null); + } + + public RangeCommand within(Range range) { + return new RangeCommand(getKey(), range); + } + + public RangeCommand fromIndex(Long start) { + return new RangeCommand(getKey(), new Range<>(start, range.getUpperBound())); + } + + public RangeCommand toIndex(Long end) { + return new RangeCommand(getKey(), new Range<>(range.getLowerBound(), end)); + } + + public Range getRange() { + return range; + } + } + + @Data + class CommandResponse { + + private final I input; + private final O output; + } + + class BooleanResponse extends CommandResponse { + + public BooleanResponse(I input, Boolean output) { + super(input, output); + } + } + + class ByteBufferResponse extends CommandResponse { + + public ByteBufferResponse(I input, ByteBuffer output) { + super(input, output); + } + } + + class MultiValueResponse extends CommandResponse> { + + public MultiValueResponse(I input, List output) { + super(input, output); + } + } + + class NumericResponse extends CommandResponse { + + public NumericResponse(I input, O output) { + super(input, output); + } + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java new file mode 100644 index 000000000..4801874e4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveSetCommands.java @@ -0,0 +1,700 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.Command; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveSetCommands { + + /** + * @author Christoph Strobl + */ + class SAddCommand extends KeyCommand { + + private List values; + + private SAddCommand(ByteBuffer key, List values) { + + super(key); + this.values = values; + } + + public static SAddCommand value(ByteBuffer values) { + return values(Collections.singletonList(values)); + } + + public static SAddCommand values(List values) { + return new SAddCommand(null, values); + } + + public SAddCommand to(ByteBuffer key) { + return new SAddCommand(key, values); + } + + public List getValues() { + return values; + } + } + + /** + * Add given {@code value} to set at {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono sAdd(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(value, "value must not be null"); + + return sAdd(key, Collections.singletonList(value)); + } + + /** + * Add given {@code values} to set at {@code key}. + * + * @param key must not be {@literal null}. + * @param values must not be {@literal null}. + * @return + */ + default Mono sAdd(ByteBuffer key, List values) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(values, "values must not be null"); + + return sAdd(Mono.just(SAddCommand.values(values).to(key))).next().map(NumericResponse::getOutput); + } + + /** + * Add given {@link SAddCommand#getValues()} to set at {@link SAddCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sAdd(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SRemCommand extends KeyCommand { + + private final List values; + + public SRemCommand(ByteBuffer key, List values) { + + super(key); + this.values = values; + } + + public static SRemCommand value(ByteBuffer values) { + return values(Collections.singletonList(values)); + } + + public static SRemCommand values(List values) { + return new SRemCommand(null, values); + } + + public SRemCommand from(ByteBuffer key) { + return new SRemCommand(key, values); + } + + public List getValues() { + return values; + } + } + + /** + * Remove given {@code value} from set at {@code key} and return the number of removed elements. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono sRem(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(value, "value must not be null"); + + return sRem(key, Collections.singletonList(value)); + } + + /** + * 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 must not be {@literal null}. + * @return + */ + default Mono sRem(ByteBuffer key, List values) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(values, "values must not be null"); + + return sRem(Mono.just(SRemCommand.values(values).from(key))).next().map(NumericResponse::getOutput); + } + + /** + * Remove given {@link SRemCommand#getValues()} from set at {@link SRemCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sRem(Publisher commands); + + /** + * Remove and return a random member from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono sPop(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return sPop(Mono.just(new KeyCommand(key))).next().map(ByteBufferResponse::getOutput); + } + + /** + * Remove and return a random member from set at {@link KeyCommand#getKey()} + * + * @param commands + * @return + */ + Flux> sPop(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SMoveCommand extends KeyCommand { + + private final ByteBuffer destination; + private final ByteBuffer value; + + private SMoveCommand(ByteBuffer key, ByteBuffer destination, ByteBuffer value) { + + super(key); + this.destination = destination; + this.value = value; + } + + public static SMoveCommand value(ByteBuffer value) { + return new SMoveCommand(null, null, value); + } + + public SMoveCommand from(ByteBuffer source) { + return new SMoveCommand(source, destination, value); + } + + public SMoveCommand to(ByteBuffer destination) { + return new SMoveCommand(getKey(), destination, value); + } + + public ByteBuffer getDestination() { + return destination; + } + + public ByteBuffer getValue() { + return value; + } + } + + /** + * Move {@code value} from {@code sourceKey} to {@code destinationKey} + * + * @param sourceKey must not be {@literal null}. + * @param destinationKey must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono sMove(ByteBuffer sourceKey, ByteBuffer destinationKey, ByteBuffer value) { + + Assert.notNull(sourceKey, "sourceKey must not be null"); + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(value, "value must not be null"); + + return sMove(Mono.just(SMoveCommand.value(value).from(sourceKey).to(destinationKey))).next() + .map(BooleanResponse::getOutput); + } + + /** + * Move {@link SMoveCommand#getValue()} from {@link SMoveCommand#getKey()} to {@link SMoveCommand#getDestination()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sMove(Publisher commands); + + /** + * Get size of set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono sCard(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return sCard(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get size of set at {@link KeyCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sCard(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SIsMemberCommand extends KeyCommand { + + private final ByteBuffer value; + + private SIsMemberCommand(ByteBuffer key, ByteBuffer value) { + + super(key); + this.value = value; + } + + public static SIsMemberCommand value(ByteBuffer value) { + return new SIsMemberCommand(null, value); + } + + public SIsMemberCommand of(ByteBuffer set) { + return new SIsMemberCommand(set, value); + } + + public ByteBuffer getValue() { + return value; + } + } + + /** + * Check if set at {@code key} contains {@code value}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono sIsMember(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return sIsMember(Mono.just(SIsMemberCommand.value(value).of(key))).next().map(BooleanResponse::getOutput); + } + + /** + * Check if set at {@link SIsMemberCommand#getKey()} contains {@link SIsMemberCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sIsMember(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SInterCommand implements Command { + + private final List keys; + + private SInterCommand(List keys) { + this.keys = keys; + } + + public static SInterCommand keys(List keys) { + return new SInterCommand(keys); + } + + @Override + public ByteBuffer getKey() { + return null; + } + + public List getKeys() { + return keys; + } + } + + /** + * Returns the members intersecting all given sets at {@code keys}. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono> sInter(List keys) { + + Assert.notNull(keys, "keys must not be null"); + + return sInter(Mono.just(SInterCommand.keys(keys))).next().map(MultiValueResponse::getOutput); + } + + /** + * Returns the members intersecting all given sets at {@link SInterCommand#getKeys()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sInter(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SInterStoreCommand extends KeyCommand { + + private final List keys; + + private SInterStoreCommand(ByteBuffer key, List keys) { + + super(key); + this.keys = keys; + } + + public static SInterStoreCommand keys(List keys) { + return new SInterStoreCommand(null, keys); + } + + public SInterStoreCommand storeAt(ByteBuffer key) { + return new SInterStoreCommand(key, keys); + } + + public List getKeys() { + return keys; + } + } + + /** + * Intersect all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param keys must not be {@literal null}. + * @return size of set stored a {@code destinationKey}. + */ + default Mono sInterStore(ByteBuffer destinationKey, List keys) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(keys, "keys must not be null"); + + return sInterStore(Mono.just(SInterStoreCommand.keys(keys).storeAt(destinationKey))).next() + .map(NumericResponse::getOutput); + } + + /** + * Intersect all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sInterStore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SUnionCommand implements Command { + + private final List keys; + + private SUnionCommand(List keys) { + this.keys = keys; + } + + public static SUnionCommand keys(List keys) { + return new SUnionCommand(keys); + } + + @Override + public ByteBuffer getKey() { + return null; + } + + public List getKeys() { + return keys; + } + } + + /** + * Returns the members intersecting all given sets at {@code keys}. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono> sUnion(List keys) { + + Assert.notNull(keys, "keys must not be null"); + + return sUnion(Mono.just(SUnionCommand.keys(keys))).next().map(MultiValueResponse::getOutput); + } + + /** + * Returns the members intersecting all given sets at {@link SInterCommand#getKeys()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sUnion(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SUnionStoreCommand extends KeyCommand { + + private final List keys; + + private SUnionStoreCommand(ByteBuffer key, List keys) { + + super(key); + this.keys = keys; + } + + public static SUnionStoreCommand keys(List keys) { + return new SUnionStoreCommand(null, keys); + } + + public SUnionStoreCommand storeAt(ByteBuffer key) { + return new SUnionStoreCommand(key, keys); + } + + public List getKeys() { + return keys; + } + } + + /** + * Union all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param keys must not be {@literal null}. + * @return size of set stored a {@code destinationKey}. + */ + default Mono sUnionStore(ByteBuffer destinationKey, List keys) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(keys, "keys must not be null"); + + return sUnionStore(Mono.just(SUnionStoreCommand.keys(keys).storeAt(destinationKey))).next() + .map(NumericResponse::getOutput); + } + + /** + * Union all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sUnionStore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SDiffCommand implements Command { + + private final List keys; + + private SDiffCommand(List keys) { + this.keys = keys; + } + + public static SDiffCommand keys(List keys) { + return new SDiffCommand(keys); + } + + @Override + public ByteBuffer getKey() { + return null; + } + + public List getKeys() { + return keys; + } + } + + /** + * Returns the diff of the members of all given sets at {@code keys}. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono> sDiff(List keys) { + + Assert.notNull(keys, "keys must not be null"); + + return sDiff(Mono.just(SDiffCommand.keys(keys))).next().map(MultiValueResponse::getOutput); + } + + /** + * Returns the diff of the members of all given sets at {@link SInterCommand#getKeys()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sDiff(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SDiffStoreCommand extends KeyCommand { + + private final List keys; + + private SDiffStoreCommand(ByteBuffer key, List keys) { + + super(key); + this.keys = keys; + } + + public static SDiffStoreCommand keys(List keys) { + return new SDiffStoreCommand(null, keys); + } + + public SDiffStoreCommand storeAt(ByteBuffer key) { + return new SDiffStoreCommand(key, keys); + } + + public List getKeys() { + return keys; + } + } + + /** + * Diff all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param keys must not be {@literal null}. + * @return size of set stored a {@code destinationKey}. + */ + default Mono sDiffStore(ByteBuffer destinationKey, List keys) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(keys, "keys must not be null"); + + return sDiffStore(Mono.just(SDiffStoreCommand.keys(keys).storeAt(destinationKey))).next() + .map(NumericResponse::getOutput); + } + + /** + * Diff all given sets at {@code keys} and store result in {@code destinationKey}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sDiffStore(Publisher commands); + + /** + * Get all elements of set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono> sMembers(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return sMembers(Mono.just(new KeyCommand(key))).next().map(MultiValueResponse::getOutput); + } + + /** + * Get all elements of set at {@link KeyCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sMembers(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SRandMembersCommand extends KeyCommand { + + private final Long count; + + private SRandMembersCommand(ByteBuffer key, Long count) { + + super(key); + this.count = count; + } + + public static SRandMembersCommand valueCount(Long nrValuesToRetrieve) { + return new SRandMembersCommand(null, nrValuesToRetrieve); + } + + public static SRandMembersCommand singleValue() { + return new SRandMembersCommand(null, null); + } + + public SRandMembersCommand from(ByteBuffer key) { + return new SRandMembersCommand(key, count); + } + + public Optional getCount() { + return Optional.ofNullable(count); + } + } + + /** + * Get random element from set at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono sRandMember(ByteBuffer key) { + return sRandMember(key, 1L).map(vals -> vals.isEmpty() ? null : vals.iterator().next()); + } + + /** + * Get {@code count} random elements from set at {@code key}. + * + * @param key must not be {@literal null}. + * @param count must not be {@literal null}. + * @return + */ + default Mono> sRandMember(ByteBuffer key, Long count) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(count, "count must not be null"); + + return sRandMember(Mono.just(SRandMembersCommand.valueCount(count).from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get {@link SRandMembersCommand#getCount()} random elements from set at {@link SRandMembersCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> sRandMember(Publisher commands); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java new file mode 100644 index 000000000..d2726555c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveStringCommands.java @@ -0,0 +1,752 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.Command; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.connection.RedisStringCommands.SetOption; +import org.springframework.data.redis.core.types.Expiration; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveStringCommands { + + /** + * @author Christoph Strobl + */ + class SetCommand extends KeyCommand { + + private ByteBuffer value; + private Expiration expiration; + private SetOption option; + + private SetCommand(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) { + + super(key); + this.value = value; + this.expiration = expiration; + this.option = option; + } + + public static ReactiveStringCommands.SetCommand set(ByteBuffer key) { + return new SetCommand(key, null, null, null); + } + + public ReactiveStringCommands.SetCommand value(ByteBuffer value) { + return new SetCommand(getKey(), value, expiration, option); + } + + public ReactiveStringCommands.SetCommand expiring(Expiration expiration) { + return new SetCommand(getKey(), value, expiration, option); + } + + public ReactiveStringCommands.SetCommand withSetOption(SetOption option) { + return new SetCommand(getKey(), value, expiration, option); + } + + public ByteBuffer getValue() { + return value; + } + + public Optional getExpiration() { + return Optional.ofNullable(expiration); + } + + public Optional getOption() { + return Optional.ofNullable(option); + } + } + + /** + * Set {@literal value} for {@literal key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono set(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return set(Mono.just(SetCommand.set(key).value(value))).next().map(BooleanResponse::getOutput); + } + + /** + * Set {@literal value} for {@literal key} with {@literal expiration} and {@literal options}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expiration must not be {@literal null}. + * @param option must not be {@literal null}. + * @return + */ + default Mono set(ByteBuffer key, ByteBuffer value, Expiration expiration, SetOption option) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return set(Mono.just(SetCommand.set(key).value(value).withSetOption(option).expiring(expiration))).next() + .map(BooleanResponse::getOutput); + } + + /** + * Set each and every {@link KeyValue} item separately. + * + * @param values must not be {@literal null}. + * @return {@link Flux} of {@link SetResponse} holding the {@link KeyValue} pair to set along with the command result. + */ + Flux> set(Publisher commands); + + /** + * Get single element stored at {@literal key}. + * + * @param key must not be {@literal null}. + * @return empty {@link ByteBuffer} in case {@literal key} does not exist. + */ + default Mono get(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null!"); + + return get(Mono.just(new KeyCommand(key))).next().map((result) -> result.getOutput()); + } + + /** + * Get elements one by one. + * + * @param keys must not be {@literal null}. + * @return {@link Flux} of {@link GetResponse} holding the {@literal key} to get along with the value retrieved. + */ + Flux> get(Publisher keys); + + /** + * Set {@literal value} for {@literal key} and return the existing value. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono getSet(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return getSet(Mono.just(SetCommand.set(key).value(value))).next().map(ByteBufferResponse::getOutput); + } + + /** + * Set {@literal value} for {@literal key} and return the existing value one by one. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return {@link Flux} of {@link GetSetResponse} holding the {@link KeyValue} pair to set along with the previously + * existing value. + */ + Flux> getSet( + Publisher command); + + /** + * Get multiple values in one batch. + * + * @param keys must not be {@literal null}. + * @return + */ + default Mono> mGet(List keys) { + + Assert.notNull(keys, "Keys must not be null!"); + + return mGet(Mono.just(keys)).next().map(MultiValueResponse::getOutput); + } + + /** + * Get multiple values at in batches. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux, ByteBuffer>> mGet(Publisher> keysets); + + /** + * Set {@code value} for {@code key}, only if {@code key} does not exist. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono setNX(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Keys must not be null!"); + Assert.notNull(value, "Keys must not be null!"); + + return setNX(Mono.just(SetCommand.set(key).value(value))).next().map(BooleanResponse::getOutput); + } + + /** + * Set {@code key value} pairs, only if {@code key} does not exist. + * + * @param values must not be {@literal null}. + * @return + */ + Flux> setNX(Publisher values); + + /** + * Set {@code key value} pair and {@link Expiration}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expireTimeout must not be {@literal null}. + * @return + */ + default Mono setEX(ByteBuffer key, ByteBuffer value, Expiration expireTimeout) { + + Assert.notNull(key, "Keys must not be null!"); + Assert.notNull(value, "Keys must not be null!"); + Assert.notNull(key, "ExpireTimeout must not be null!"); + + return setEX(Mono.just(SetCommand.set(key).value(value).expiring(expireTimeout))).next() + .map(BooleanResponse::getOutput); + } + + /** + * Set {@code key value} pairs and {@link Expiration}. + * + * @param source must not be {@literal null}. + * @param expireTimeout must not be {@literal null}. + * @return + */ + Flux> setEX(Publisher command); + + /** + * Set {@code key value} pair and {@link Expiration}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param expireTimeout must not be {@literal null}. + * @return + */ + default Mono pSetEX(ByteBuffer key, ByteBuffer value, Expiration expireTimeout) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + Assert.notNull(key, "ExpireTimeout must not be null!"); + + return pSetEX(Mono.just(SetCommand.set(key).value(value).expiring(expireTimeout))).next() + .map(BooleanResponse::getOutput); + } + + /** + * Set {@code key value} pairs and {@link Expiration}. + * + * @param source must not be {@literal null}. + * @param expireTimeout must not be {@literal null}. + * @return + */ + Flux> pSetEX(Publisher command); + + /** + * @author Christoph Strobl + */ + class MSetCommand implements Command { + + private Map keyValuePairs; + + private MSetCommand(Map keyValuePairs) { + this.keyValuePairs = keyValuePairs; + } + + @Override + public ByteBuffer getKey() { + return null; + } + + public static ReactiveStringCommands.MSetCommand mset(Map keyValuePairs) { + return new MSetCommand(keyValuePairs); + } + + public Map getKeyValuePairs() { + return keyValuePairs; + } + } + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code tuple}. + * + * @param tuples must not be {@literal null}. + * @return + */ + default Mono mSet(Map tuples) { + + Assert.notNull(tuples, "Tuples must not be null!"); + + return mSet(Mono.just(MSetCommand.mset(tuples))).next().map(BooleanResponse::getOutput); + } + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code source}. + * + * @param source must not be {@literal null}. + * @return + */ + Flux> mSet(Publisher source); + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code tuples} only if the provided key does + * not exist. + * + * @param tuples must not be {@literal null}. + * @return + */ + default Mono mSetNX(Map tuples) { + + Assert.notNull(tuples, "Tuples must not be null!"); + + return mSetNX(Mono.just(MSetCommand.mset(tuples))).next().map(BooleanResponse::getOutput); + } + + /** + * Set multiple keys to multiple values using key-value pairs provided in {@code tuples} only if the provided key does + * not exist. + * + * @param source must not be {@literal null}. + * @return + */ + Flux> mSetNX( + Publisher source); + + /** + * @author Christoph Strobl + */ + class AppendCommand extends KeyCommand { + + private ByteBuffer value; + + private AppendCommand(ByteBuffer key, ByteBuffer value) { + + super(key); + this.value = value; + } + + public static ReactiveStringCommands.AppendCommand key(ByteBuffer key) { + return new AppendCommand(key, null); + } + + public ReactiveStringCommands.AppendCommand append(ByteBuffer value) { + return new AppendCommand(getKey(), value); + } + + public ByteBuffer getValue() { + return value; + } + + } + + /** + * Append a {@code value} to {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono append(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return append(Mono.just(AppendCommand.key(key).append(value))).next().map(NumericResponse::getOutput); + } + + /** + * Append a {@link KeyValue#value} to {@link KeyValue#key} + * + * @param source must not be {@literal null}. + * @return + */ + Flux> append( + Publisher source); + + /** + * Get a substring of value of {@code key} between {@code begin} and {@code end}. + * + * @param key must not be {@literal null}. + * @param begin + * @param end + * @return + */ + default Mono getRange(ByteBuffer key, long begin, long end) { + + Assert.notNull(key, "Key must not be null!"); + + return getRange(Mono.just(RangeCommand.key(key).fromIndex(begin).toIndex(end))).next() + .map(ByteBufferResponse::getOutput); + } + + /** + * Get a substring of value of {@code key} between {@code begin} and {@code end}. + * + * @param keys must not be {@literal null}. + * @param begin + * @param end + * @return + */ + Flux> getRange(Publisher commands); + + /** + * @author Christoph Strobl + */ + class SetRangeCommand extends KeyCommand { + + private ByteBuffer value; + private Long offset; + + private SetRangeCommand(ByteBuffer key, ByteBuffer value, Long offset) { + + super(key); + this.value = value; + this.offset = offset; + } + + public static ReactiveStringCommands.SetRangeCommand overwrite(ByteBuffer key) { + return new SetRangeCommand(key, null, null); + } + + public ReactiveStringCommands.SetRangeCommand withValue(ByteBuffer value) { + return new SetRangeCommand(getKey(), value, offset); + } + + public ReactiveStringCommands.SetRangeCommand atPosition(Long index) { + return new SetRangeCommand(getKey(), value, index); + } + + public ByteBuffer getValue() { + return value; + } + + public Long getOffset() { + return offset; + } + } + + /** + * Overwrite parts of {@code key} starting at the specified {@code offset} with given {@code value}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @param offset + * @return + */ + default Mono setRange(ByteBuffer key, ByteBuffer value, long offset) { + + Assert.notNull(key, "Key must not be null!"); + Assert.notNull(value, "Value must not be null!"); + + return setRange(Mono.just(SetRangeCommand.overwrite(key).withValue(value).atPosition(offset))).next() + .map(NumericResponse::getOutput); + } + + /** + * Overwrite parts of {@link KeyValue#key} starting at the specified {@code offset} with given {@link KeyValue#value}. + * + * @param keys must not be {@literal null}. + * @param offset must not be {@literal null}. + * @return + */ + Flux> setRange( + Publisher commands); + + class GetBitCommand extends KeyCommand { + + public Long offset; + + public GetBitCommand(ByteBuffer key, Long offset) { + + super(key); + this.offset = offset; + } + + public static ReactiveStringCommands.GetBitCommand bit(ByteBuffer key) { + return new GetBitCommand(key, null); + } + + public ReactiveStringCommands.GetBitCommand atOffset(Long offset) { + return new GetBitCommand(getKey(), offset); + } + + public Long getOffset() { + return offset; + } + } + + /** + * Get the bit value at {@code offset} of value at {@code key}. + * + * @param key must not be {@literal null}. + * @param offset + * @return + */ + default Mono getBit(ByteBuffer key, long offset) { + + Assert.notNull(key, "Key must not be null!"); + + return getBit(Mono.just(GetBitCommand.bit(key).atOffset(offset))).next().map(BooleanResponse::getOutput); + } + + /** + * Get the bit value at {@code offset} of value at {@code key}. + * + * @param keys must not be {@literal null}. + * @param offset must not be {@literal null}. + * @return + */ + Flux> getBit( + Publisher commands); + + class SetBitCommand extends KeyCommand { + + private Long offset; + private Boolean value; + + private SetBitCommand(ByteBuffer key, Long offset, Boolean value) { + + super(key); + this.offset = offset; + this.value = value; + } + + public static ReactiveStringCommands.SetBitCommand bit(ByteBuffer key) { + return new SetBitCommand(key, null, null); + } + + public ReactiveStringCommands.SetBitCommand atOffset(Long index) { + return new ReactiveStringCommands.SetBitCommand(getKey(), index, value); + } + + public ReactiveStringCommands.SetBitCommand to(Boolean bit) { + return new ReactiveStringCommands.SetBitCommand(getKey(), offset, bit); + } + + public Long getOffset() { + return offset; + } + + public Boolean getValue() { + return value; + } + } + + /** + * Sets the bit at {@code offset} in value stored at {@code key} and return the original value. + * + * @param key must not be {@literal null}. + * @param offset + * @param value + * @return + */ + default Mono setBit(ByteBuffer key, long offset, boolean value) { + + Assert.notNull(key, "Key must not be null!"); + + return setBit(Mono.just(SetBitCommand.bit(key).atOffset(offset).to(value))).next().map(BooleanResponse::getOutput); + } + + /** + * Sets the bit at {@code offset} in value stored at {@code key} and return the original value. + * + * @param keys must not be {@literal null}. + * @param offset must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + Flux> setBit( + Publisher commands); + + /** + * @author Christoph Strobl + */ + class BitCountCommand extends KeyCommand { + + private Range range; + + public BitCountCommand(ByteBuffer key, Range range) { + + super(key); + this.range = range; + } + + public static ReactiveStringCommands.BitCountCommand bitCount(ByteBuffer key) { + return new ReactiveStringCommands.BitCountCommand(key, null); + } + + public ReactiveStringCommands.BitCountCommand within(Range range) { + return new ReactiveStringCommands.BitCountCommand(getKey(), range); + } + + public Range getRange() { + return range; + } + + } + + /** + * Count the number of set bits (population counting) in value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono bitCount(ByteBuffer key) { + + Assert.notNull(key, "Key must not be null"); + + return bitCount(Mono.just(BitCountCommand.bitCount(key))).next().map(NumericResponse::getOutput); + } + + /** + * Count the number of set bits (population counting) of value stored at {@code key} between {@code begin} and + * {@code end}. + * + * @param key must not be {@literal null}. + * @param begin + * @param end + * @return + */ + default Mono bitCount(ByteBuffer key, long begin, long end) { + + Assert.notNull(key, "Key must not be null"); + + return bitCount(Mono.just(BitCountCommand.bitCount(key).within(new Range<>(begin, end)))).next() + .map(NumericResponse::getOutput); + } + + /** + * Count the number of set bits (population counting) of value stored at {@code key} between {@code begin} and + * {@code end}. + * + * @param keys must not be {@literal null}. + * @param begin must not be {@literal null}. + * @param end must not be {@literal null}. + * @return + */ + Flux> bitCount( + Publisher commands); + + /** + * @author Christoph Strobl + */ + class BitOpCommand { + + private List keys; + private BitOperation bitOp; + private ByteBuffer destinationKey; + + private BitOpCommand(List keys, BitOperation bitOp, ByteBuffer destinationKey) { + + this.keys = keys; + this.bitOp = bitOp; + this.destinationKey = destinationKey; + } + + public static ReactiveStringCommands.BitOpCommand perform(BitOperation bitOp) { + return new ReactiveStringCommands.BitOpCommand(null, bitOp, null); + } + + public BitOperation getBitOp() { + return bitOp; + } + + public ReactiveStringCommands.BitOpCommand onKeys(List keys) { + return new ReactiveStringCommands.BitOpCommand(keys, bitOp, destinationKey); + } + + public List getKeys() { + return keys; + } + + public ReactiveStringCommands.BitOpCommand andSaveAs(ByteBuffer destinationKey) { + return new ReactiveStringCommands.BitOpCommand(keys, bitOp, destinationKey); + } + + public ByteBuffer getDestinationKey() { + return destinationKey; + } + + } + + /** + * Perform bitwise operations between strings. + * + * @param keys must not be {@literal null}. + * @param bitOp must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + default Mono bitOp(List keys, BitOperation bitOp, ByteBuffer destination) { + + Assert.notNull(keys, "keys must not be null"); + + return bitOp(Mono.just(BitOpCommand.perform(bitOp).onKeys(keys).andSaveAs(destination))).next() + .map(NumericResponse::getOutput); + } + + /** + * Perform bitwise operations between strings. + * + * @param keys must not be {@literal null}. + * @param bitOp must not be {@literal null}. + * @param destination must not be {@literal null}. + * @return + */ + Flux> bitOp( + Publisher commands); + + /** + * Get the length of the value stored at {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono strLen(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return strLen(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get the length of the value stored at {@code key}. + * + * @param keys must not be {@literal null}. + * @return + */ + Flux> strLen(Publisher keys); +} diff --git a/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java new file mode 100644 index 000000000..7c8fdb0bf --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/ReactiveZSetCommands.java @@ -0,0 +1,1254 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Range; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; +import org.springframework.data.redis.connection.RedisZSetCommands.Limit; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public interface ReactiveZSetCommands { + + /** + * @author Christoph Strobl + */ + class AgrumentConverters { + + public static Object lowerBoundArgOf(Range range) { + return rangeToLowerBoundArgumentConverter(false).convert(range); + } + + public static Object upperBoundArgOf(Range range) { + return rangeToLowerBoundArgumentConverter(true).convert(range); + } + + public static Converter, Object> rangeToLowerBoundArgumentConverter(Boolean upper) { + + return (source) -> { + + // TODO: fix range exclusion pattern when DATACMNS-920 is resolved + DirectFieldAccessFallbackBeanWrapper bw = new DirectFieldAccessFallbackBeanWrapper(source); + + Boolean inclusive = upper ? Boolean.valueOf(bw.getPropertyValue("upperInclusive").toString()) + : Boolean.valueOf(bw.getPropertyValue("lowerInclusive").toString()); + Object value = upper ? source.getUpperBound() : source.getLowerBound(); + + if (value instanceof Double) { + + Object converted = doubleToRangeConverter().convert((Double) value); + if (!(converted instanceof String) && !inclusive) { + return "(" + converted.toString(); + } + return converted; + } + + if (value instanceof String) { + if (!StringUtils.hasText((String) value)) { + return upper ? "+" : "-"; + } + if (ObjectUtils.nullSafeEquals(value, "+") || ObjectUtils.nullSafeEquals(value, "-")) { + return value; + } + return (inclusive ? "[" : "(") + value.toString(); + } + + return inclusive ? value : "(" + value; + }; + } + + public static Converter doubleToRangeConverter() { + + return (source) -> { + if (source.equals(Double.NEGATIVE_INFINITY)) { + return "-inf"; + } + if (source.equals(Double.POSITIVE_INFINITY)) { + return "+inf"; + } + return source; + }; + } + + } + + /** + * @author Christoph Strobl + */ + class ZAddCommand extends KeyCommand { + + private final List tuples; + private final Boolean upsert; + private final Boolean returnTotalChanged; + private final Boolean incr; + + private ZAddCommand(ByteBuffer key, List tuples, Boolean upsert, Boolean returnTotalChanged, Boolean incr) { + + super(key); + this.tuples = tuples; + this.upsert = upsert; + this.returnTotalChanged = returnTotalChanged; + this.incr = incr; + } + + public static ZAddCommand tuple(Tuple tuple) { + return tuples(Collections.singletonList(tuple)); + } + + public static ZAddCommand tuples(List tuples) { + return new ZAddCommand(null, tuples, null, null, null); + } + + public ZAddCommand to(ByteBuffer key) { + return new ZAddCommand(key, tuples, upsert, returnTotalChanged, incr); + } + + public ZAddCommand xx() { + return new ZAddCommand(getKey(), tuples, false, returnTotalChanged, incr); + } + + public ZAddCommand nx() { + return new ZAddCommand(getKey(), tuples, true, returnTotalChanged, incr); + } + + public ZAddCommand ch() { + return new ZAddCommand(getKey(), tuples, upsert, true, incr); + } + + public ZAddCommand incr() { + return new ZAddCommand(getKey(), tuples, upsert, upsert, true); + } + + public List getTuples() { + return tuples; + } + + public Optional getUpsert() { + return Optional.ofNullable(upsert); + } + + public Optional getIncr() { + return Optional.ofNullable(incr); + } + + public Optional getReturnTotalChanged() { + return Optional.ofNullable(returnTotalChanged); + } + } + + /** + * 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 must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono zAdd(ByteBuffer key, Double score, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(score, "score must not be null"); + Assert.notNull(value, "value must not be null"); + + return zAdd(Mono.just(ZAddCommand.tuple(new DefaultTuple(value.array(), score)).to(key))).next() + .map(resp -> resp.getOutput().longValue()); + } + + /** + * Add {@link ZAddCommand#getTuple()} to a sorted set at {@link ZAddCommand#getKey()}, or update its {@code score} if + * it already exists. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zAdd(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRemCommand extends KeyCommand { + + private final List values; + + private ZRemCommand(ByteBuffer key, List values) { + + super(key); + this.values = values; + } + + public static ZRemCommand values(List values) { + return new ZRemCommand(null, values); + } + + public ZRemCommand from(ByteBuffer key) { + return new ZRemCommand(key, values); + } + + public List getValues() { + return values; + } + } + + /** + * Remove {@code value} from sorted set. Return number of removed elements. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono zRem(ByteBuffer key, ByteBuffer value) { + return zRem(key, Collections.singletonList(value)); + } + + /** + * 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 + */ + default Mono zRem(ByteBuffer key, List values) { + + Assert.notNull(values, "values must not be null"); + + return zRem(Mono.just(ZRemCommand.values(values).from(key))).next().map(NumericResponse::getOutput); + } + + /** + * Remove {@link ZRemCommand#getValues()} from sorted set. Return number of removed elements. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRem(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZIncrByCommand extends KeyCommand { + + private final ByteBuffer value; + private final Number increment; + + public ZIncrByCommand(ByteBuffer key, ByteBuffer value, Number increment) { + + super(key); + this.value = value; + this.increment = increment; + } + + public static ZIncrByCommand scoreOf(ByteBuffer member) { + return new ZIncrByCommand(null, member, null); + } + + public ZIncrByCommand by(Number increment) { + return new ZIncrByCommand(getKey(), value, increment); + } + + public ZIncrByCommand storedWithin(ByteBuffer key) { + return new ZIncrByCommand(key, value, increment); + } + + public ByteBuffer getValue() { + return value; + } + + public Number getIncrement() { + return increment; + } + } + + /** + * Increment the score of element with {@code value} in sorted set by {@code increment}. + * + * @param key must not be {@literal null}. + * @param increment must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono zIncrBy(ByteBuffer key, Number increment, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(increment, "increment must not be null"); + Assert.notNull(value, "value must not be null"); + + return zIncrBy(Mono.just(ZIncrByCommand.scoreOf(value).by(increment).storedWithin(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Increment the score of element with {@link ZIncrByCommand#getValue()} in sorted set by + * {@link ZIncrByCommand#getIncrement()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zIncrBy(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRankCommand extends KeyCommand { + + private final ByteBuffer value; + private final Direction direction; + + private ZRankCommand(ByteBuffer key, ByteBuffer value, Direction direction) { + + super(key); + this.value = value; + this.direction = direction; + } + + public static ZRankCommand indexOf(ByteBuffer member) { + return new ZRankCommand(null, member, Direction.ASC); + } + + public static ZRankCommand reverseIndexOf(ByteBuffer member) { + return new ZRankCommand(null, member, Direction.DESC); + } + + public ZRankCommand storedWithin(ByteBuffer key) { + return new ZRankCommand(key, value, direction); + } + + public ByteBuffer getValue() { + return value; + } + + public Direction getDirection() { + return direction; + } + } + + /** + * Determine the index of element with {@code value} in a sorted set. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono zRank(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return zRank(Mono.just(ZRankCommand.indexOf(value).storedWithin(key))).next().map(NumericResponse::getOutput); + } + + /** + * 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 value must not be {@literal null}. + * @return + */ + default Mono zRevRank(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return zRank(Mono.just(ZRankCommand.reverseIndexOf(value).storedWithin(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Determine the index of element with {@code value} in a sorted set when scored by + * {@link ZRankCommand#getDirection()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRank(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRangeCommand extends KeyCommand { + + private final Range range; + private final Boolean withScores; + private final Direction direction; + + public ZRangeCommand(ByteBuffer key, Range range, Direction direction, Boolean withScores) { + super(key); + this.range = range; + this.withScores = withScores; + this.direction = direction; + } + + public static ZRangeCommand reverseValuesWithin(Range range) { + return new ZRangeCommand(null, range, Direction.DESC, null); + } + + public static ZRangeCommand valuesWithin(Range range) { + return new ZRangeCommand(null, range, Direction.ASC, null); + } + + public ZRangeCommand withScores() { + return new ZRangeCommand(getKey(), range, direction, Boolean.TRUE); + } + + public ZRangeCommand from(ByteBuffer key) { + return new ZRangeCommand(key, range, direction, withScores); + } + + public Range getRange() { + return range; + } + + public Optional getWithScores() { + return Optional.ofNullable(withScores); + } + + public Direction getDirection() { + return direction; + } + } + + /** + * Get elements in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRange(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRange(Mono.just(ZRangeCommand.valuesWithin(range).from(key))).next().map( + resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())).collect(Collectors.toList())); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRangeWithScores(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRange(Mono.just(ZRangeCommand.valuesWithin(range).withScores().from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get elements in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRevRange(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRange(Mono.just(ZRangeCommand.reverseValuesWithin(range).from(key))).next().map( + resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())).collect(Collectors.toList())); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRevRangeWithScores(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRange(Mono.just(ZRangeCommand.reverseValuesWithin(range).withScores().from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRange(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRangeByScoreCommand extends KeyCommand { + + private final Range range; + private final Boolean withScores; + private final Direction direction; + private final Limit limit; + + private ZRangeByScoreCommand(ByteBuffer key, Range range, Direction direction, Boolean withScores, + Limit limit) { + + super(key); + this.range = range; + this.withScores = withScores; + this.direction = direction; + this.limit = limit; + } + + public static ZRangeByScoreCommand reverseScoresWithin(Range range) { + return new ZRangeByScoreCommand(null, range, Direction.DESC, null, null); + } + + public static ZRangeByScoreCommand scoresWithin(Range range) { + return new ZRangeByScoreCommand(null, range, Direction.ASC, null, null); + } + + public ZRangeByScoreCommand withScores() { + return new ZRangeByScoreCommand(getKey(), range, direction, Boolean.TRUE, limit); + } + + public ZRangeByScoreCommand from(ByteBuffer key) { + return new ZRangeByScoreCommand(key, range, direction, withScores, limit); + } + + public ZRangeByScoreCommand limitTo(Limit limit) { + return new ZRangeByScoreCommand(getKey(), range, direction, withScores, limit); + } + + public Range getRange() { + return range; + } + + public Optional getWithScores() { + return Optional.ofNullable(withScores); + } + + public Direction getDirection() { + return direction; + } + + public Optional getLimit() { + return Optional.ofNullable(limit); + } + } + + /** + * Get elements in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRangeByScore(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.scoresWithin(range).from(key))).next().map( + resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())).collect(Collectors.toList())); + } + + /** + * Get elements in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRangeByScore(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.scoresWithin(range).from(key).limitTo(limit))).next().map( + resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())).collect(Collectors.toList())); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRangeByScoreWithScores(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.scoresWithin(range).withScores().from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRangeByScoreWithScores(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.scoresWithin(range).withScores().from(key).limitTo(limit))) + .next().map(MultiValueResponse::getOutput); + } + + /** + * Get elements in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRevRangeByScore(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.reverseScoresWithin(range).from(key))).next().map( + resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())).collect(Collectors.toList())); + } + + /** + * Get elements in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRevRangeByScore(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.reverseScoresWithin(range).from(key).limitTo(limit))).next() + .map(resp -> resp.getOutput().stream().map(tuple -> ByteBuffer.wrap(tuple.getValue())) + .collect(Collectors.toList())); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRevRangeByScoreWithScores(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore(Mono.just(ZRangeByScoreCommand.reverseScoresWithin(range).withScores().from(key))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set in reverse {@code score} ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRevRangeByScoreWithScores(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByScore( + Mono.just(ZRangeByScoreCommand.reverseScoresWithin(range).withScores().from(key).limitTo(limit))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get set of {@link Tuple}s in {@code range} from sorted set. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRangeByScore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZCountCommand extends KeyCommand { + + private final Range range; + + private ZCountCommand(ByteBuffer key, Range range) { + + super(key); + this.range = range; + } + + public static ZCountCommand scoresWithin(Range range) { + return new ZCountCommand(null, range); + } + + public ZCountCommand forKey(ByteBuffer key) { + return new ZCountCommand(key, range); + } + + public Range getRange() { + return range; + } + + } + + /** + * Count number of elements within sorted set with scores within {@link Range}.
+ * NOTE please use {@link Double#NEGATIVE_INFINITY} for {@code -inf} and {@link Double#POSITIVE_INFINITY} for + * {@code +inf}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono zCount(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zCount(Mono.just(ZCountCommand.scoresWithin(range).forKey(key))).next().map(NumericResponse::getOutput); + } + + /** + * Count number of elements within sorted set with scores within {@link Range}.
+ * NOTE please use {@link Double#NEGATIVE_INFINITY} for {@code -inf} and {@link Double#POSITIVE_INFINITY} for + * {@code +inf}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zCount(Publisher commands); + + /** + * Get the size of sorted set with {@code key}. + * + * @param key must not be {@literal null}. + * @return + */ + default Mono zCard(ByteBuffer key) { + + Assert.notNull(key, "key must not be null"); + + return zCard(Mono.just(new KeyCommand(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get the size of sorted set with {@link KeyCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zCard(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZScoreCommand extends KeyCommand { + + private final ByteBuffer value; + + private ZScoreCommand(ByteBuffer key, ByteBuffer value) { + + super(key); + this.value = value; + } + + public static ZScoreCommand scoreOf(ByteBuffer member) { + return new ZScoreCommand(null, member); + } + + public ZScoreCommand forKey(ByteBuffer key) { + return new ZScoreCommand(key, value); + } + + public ByteBuffer getValue() { + return value; + } + + } + + /** + * Get the score of element with {@code value} from sorted set with key {@code key}. + * + * @param key must not be {@literal null}. + * @param value must not be {@literal null}. + * @return + */ + default Mono zScore(ByteBuffer key, ByteBuffer value) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(value, "value must not be null"); + + return zScore(Mono.just(ZScoreCommand.scoreOf(value).forKey(key))).next().map(NumericResponse::getOutput); + } + + /** + * Get the score of element with {@link ZScoreCommand#getValue()} from sorted set with key + * {@link ZScoreCommand#getKey()} + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zScore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRemRangeByRankCommand extends KeyCommand { + + private final Range range; + + private ZRemRangeByRankCommand(ByteBuffer key, Range range) { + super(key); + this.range = range; + } + + public static ZRemRangeByRankCommand valuesWithin(Range range) { + return new ZRemRangeByRankCommand(null, range); + } + + public ZRemRangeByRankCommand from(ByteBuffer key) { + return new ZRemRangeByRankCommand(key, range); + } + + public Range getRange() { + return range; + } + } + + /** + * Remove elements in {@link Range} from sorted set with {@code key}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono zRemRangeByRank(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRemRangeByRank(Mono.just(ZRemRangeByRankCommand.valuesWithin(range).from(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Remove elements in {@link Range} from sorted set with {@link ZRemRangeByRankCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRemRangeByRank(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRemRangeByScoreCommand extends KeyCommand { + + private final Range range; + + private ZRemRangeByScoreCommand(ByteBuffer key, Range range) { + + super(key); + this.range = range; + } + + public static ZRemRangeByScoreCommand scoresWithin(Range range) { + return new ZRemRangeByScoreCommand(null, range); + } + + public ZRemRangeByScoreCommand from(ByteBuffer key) { + return new ZRemRangeByScoreCommand(key, range); + } + + public Range getRange() { + return range; + } + + } + + /** + * Remove elements in {@link Range} from sorted set with {@code key}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono zRemRangeByScore(ByteBuffer key, Range range) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRemRangeByScore(Mono.just(ZRemRangeByScoreCommand.scoresWithin(range).from(key))).next() + .map(NumericResponse::getOutput); + } + + /** + * Remove elements in {@link Range} from sorted set with {@link ZRemRangeByRankCommand#getKey()}. + * + * @param commands must not be {@literal null}. + * @return + */ + Flux> zRemRangeByScore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZUnionStoreCommand extends KeyCommand { + + private final List sourceKeys; + private final List weights; + private final Aggregate aggregateFunction; + + private ZUnionStoreCommand(ByteBuffer key, List sourceKeys, List weights, Aggregate aggregate) { + + super(key); + this.sourceKeys = sourceKeys; + this.weights = weights; + this.aggregateFunction = aggregate; + } + + public static ZUnionStoreCommand sets(List keys) { + return new ZUnionStoreCommand(null, keys, null, null); + } + + public ZUnionStoreCommand applyWeights(List weights) { + return new ZUnionStoreCommand(getKey(), sourceKeys, weights, aggregateFunction); + } + + public ZUnionStoreCommand aggregateUsing(Aggregate aggregateFunction) { + return new ZUnionStoreCommand(getKey(), sourceKeys, weights, aggregateFunction); + } + + public ZUnionStoreCommand storeAs(ByteBuffer key) { + return new ZUnionStoreCommand(key, sourceKeys, weights, aggregateFunction); + } + + public List getSourceKeys() { + return sourceKeys; + } + + public List getWeights() { + return weights == null ? Collections.emptyList() : weights; + } + + public Optional getAggregateFunction() { + return Optional.ofNullable(aggregateFunction); + } + + public Integer getNumKeys() { + return sourceKeys != null ? sourceKeys.size() : null; + } + } + + /** + * Union sorted {@code sets} and store result in destination {@code destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @return + */ + default Mono zUnionStore(ByteBuffer destinationKey, List sets) { + return zUnionStore(destinationKey, sets, null); + } + + /** + * Union sorted {@code sets} and store result in destination {@code destinationKey} and apply weights to individual + * sets. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @param weights can be {@literal null}. + * @return + */ + default Mono zUnionStore(ByteBuffer destinationKey, List sets, List weights) { + return zUnionStore(destinationKey, sets, weights, null); + } + + /** + * Union sorted {@code sets} by applying {@code aggregateFunction} and store result in destination + * {@code destinationKey} and apply weights to individual sets. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @param weights can be {@literal null}. + * @param aggregateFunction can be {@literal null}. + * @return + */ + default Mono zUnionStore(ByteBuffer destinationKey, List sets, List weights, + Aggregate aggregateFunction) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(sets, "sets must not be null"); + + return zUnionStore(Mono.just( + ZUnionStoreCommand.sets(sets).aggregateUsing(aggregateFunction).applyWeights(weights).storeAs(destinationKey))) + .next().map(NumericResponse::getOutput); + } + + /** + * Union sorted {@code sets} by applying {@code aggregateFunction} and store result in destination + * {@code destinationKey} and apply weights to individual sets. + * + * @param commands + * @return + */ + Flux> zUnionStore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZInterStoreCommand extends KeyCommand { + + private final List sourceKeys; + private final List weights; + private final Aggregate aggregateFunction; + + private ZInterStoreCommand(ByteBuffer key, List sourceKeys, List weights, Aggregate aggregate) { + + super(key); + this.sourceKeys = sourceKeys; + this.weights = weights; + this.aggregateFunction = aggregate; + } + + public static ZInterStoreCommand sets(List keys) { + return new ZInterStoreCommand(null, keys, null, null); + } + + public ZInterStoreCommand applyWeights(List weights) { + return new ZInterStoreCommand(getKey(), sourceKeys, weights, aggregateFunction); + } + + public ZInterStoreCommand aggregateUsing(Aggregate aggregateFunction) { + return new ZInterStoreCommand(getKey(), sourceKeys, weights, aggregateFunction); + } + + public ZInterStoreCommand storeAs(ByteBuffer key) { + return new ZInterStoreCommand(key, sourceKeys, weights, aggregateFunction); + } + + public List getSourceKeys() { + return sourceKeys; + } + + public List getWeights() { + return weights == null ? Collections.emptyList() : weights; + } + + public Optional getAggregateFunction() { + return Optional.ofNullable(aggregateFunction); + } + + public Integer getNumKeys() { + return sourceKeys != null ? sourceKeys.size() : null; + } + } + + /** + * Intersect sorted {@code sets} and store result in destination {@code destinationKey}. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @return + */ + default Mono zInterStore(ByteBuffer destinationKey, List sets) { + return zInterStore(destinationKey, sets, null); + } + + /** + * Intersect sorted {@code sets} and store result in destination {@code destinationKey} and apply weights to + * individual sets. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @param weights can be {@literal null}. + * @return + */ + default Mono zInterStore(ByteBuffer destinationKey, List sets, List weights) { + return zInterStore(destinationKey, sets, weights, null); + } + + /** + * Intersect sorted {@code sets} by applying {@code aggregateFunction} and store result in destination + * {@code destinationKey} and apply weights to individual sets. + * + * @param destinationKey must not be {@literal null}. + * @param sets must not be {@literal null}. + * @param weights can be {@literal null}. + * @param aggregateFunction can be {@literal null}. + * @return + */ + default Mono zInterStore(ByteBuffer destinationKey, List sets, List weights, + Aggregate aggregateFunction) { + + Assert.notNull(destinationKey, "destinationKey must not be null"); + Assert.notNull(sets, "sets must not be null"); + + return zInterStore(Mono.just( + ZInterStoreCommand.sets(sets).aggregateUsing(aggregateFunction).applyWeights(weights).storeAs(destinationKey))) + .next().map(NumericResponse::getOutput); + } + + /** + * Intersect sorted {@code sets} by applying {@code aggregateFunction} and store result in destination + * {@code destinationKey} and apply weights to individual sets. + * + * @param commands + * @return + */ + Flux> zInterStore(Publisher commands); + + /** + * @author Christoph Strobl + */ + class ZRangeByLexCommand extends KeyCommand { + + private final Range range; + private final Direction direction; + private final Limit limit; + + private ZRangeByLexCommand(ByteBuffer key, Range range, Direction direction, Limit limit) { + + super(key); + this.range = range; + this.direction = direction; + this.limit = limit; + } + + public static ZRangeByLexCommand reverseStringsWithin(Range range) { + return new ZRangeByLexCommand(null, range, Direction.DESC, null); + } + + public static ZRangeByLexCommand stringsWithin(Range range) { + return new ZRangeByLexCommand(null, range, Direction.ASC, null); + } + + public ZRangeByLexCommand from(ByteBuffer key) { + return new ZRangeByLexCommand(key, range, direction, limit); + } + + public ZRangeByLexCommand limitTo(Limit limit) { + return new ZRangeByLexCommand(getKey(), range, direction, limit); + } + + public Range getRange() { + return range; + } + + public Limit getLimit() { + return limit; + } + + public Direction getDirection() { + return direction; + } + } + + /** + * Get all the elements in {@link Range} from the sorted set at {@literal key} in lexicographical ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRangeByLex(ByteBuffer key, Range range) { + return zRangeByLex(key, range, null); + } + + /** + * Get all the elements in {@link Range} from the sorted set at {@literal key} in lexicographical ordering. Result is + * limited via {@link Limit}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRangeByLex(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByLex(Mono.just(ZRangeByLexCommand.stringsWithin(range).from(key).limitTo(limit))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get all the elements in {@link Range} from the sorted set at {@literal key} in lexicographical ordering. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @return + */ + default Mono> zRevRangeByLex(ByteBuffer key, Range range) { + return zRevRangeByLex(key, range, null); + } + + /** + * Get all the elements in {@link Range} from the sorted set at {@literal key} in lexicographical ordering. Result is + * limited via {@link Limit}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + default Mono> zRevRangeByLex(ByteBuffer key, Range range, Limit limit) { + + Assert.notNull(key, "key must not be null"); + Assert.notNull(range, "range must not be null"); + + return zRangeByLex(Mono.just(ZRangeByLexCommand.reverseStringsWithin(range).from(key).limitTo(limit))).next() + .map(MultiValueResponse::getOutput); + } + + /** + * Get all the elements in {@link Range} from the sorted set at {@literal key} in lexicographical ordering. Result is + * limited via {@link Limit} and sorted by {@link ZRangeByLexCommand#getDirection()}. + * + * @param key must not be {@literal null}. + * @param range must not be {@literal null}. + * @param limit can be {@literal null}. + * @return + */ + Flux> zRangeByLex(Publisher commands); + +} 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 39d4ecaef..b51acf276 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisConnectionFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2011-2014 the original author or authors. + * Copyright 2011-2016 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,11 +37,23 @@ public interface RedisConnectionFactory extends PersistenceExceptionTranslator { * Provides a suitable connection for interacting with Redis Cluster. * * @return - * @throws * @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 diff --git a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java index 6d7ee8582..fa14be667 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisGeoCommands.java @@ -199,7 +199,7 @@ public interface RedisGeoCommands { * @author Christoph Strobl * @since 1.8 */ - public class GeoRadiusCommandArgs { + public class GeoRadiusCommandArgs implements Cloneable { Set flags = new LinkedHashSet(2, 1); Long limit; @@ -309,6 +309,16 @@ public interface RedisGeoCommands { public static enum Flag { WITHCOORD, WITHDIST } + + @Override + protected GeoRadiusCommandArgs clone() { + + GeoRadiusCommandArgs tmp = new GeoRadiusCommandArgs(); + tmp.flags = this.flags != null ? new LinkedHashSet<>(this.flags) : new LinkedHashSet<>(2); + tmp.limit = this.limit; + tmp.sortDirection = this.sortDirection; + return tmp; + } } /** diff --git a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java index 510d5752d..e88d61cfb 100644 --- a/src/main/java/org/springframework/data/redis/connection/convert/Converters.java +++ b/src/main/java/org/springframework/data/redis/connection/convert/Converters.java @@ -174,6 +174,10 @@ abstract public class Converters { }; } + public static Boolean stringToBoolean(String s) { + return ObjectUtils.nullSafeEquals("OK", s); + } + public static Converter stringToProps() { return STRING_TO_PROPS; } @@ -378,7 +382,7 @@ abstract public class Converters { * @author Christoph Strobl * @since 1.8 */ - static enum DistanceConverterFactory { + enum DistanceConverterFactory { INSTANCE; 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 4db269daf..8d3fdc38c 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 @@ -33,14 +33,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.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.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -362,8 +355,22 @@ public class JedisConnectionFactory implements InitializingBean, DisposableBean, /* * (non-Javadoc) - * @see org.springframework.dao.support.PersistenceExceptionTranslator#translateExceptionIfPossible(java.lang.RuntimeException) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getReactiveConnection() */ + @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 1390834a5..71d66cd4a 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 @@ -204,6 +204,24 @@ public class LettuceConnectionFactory implements InitializingBean, DisposableBea return new LettuceClusterConnection((RedisClusterClient) client, clusterCommandExecutor); } + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.RedisConnectionFactory#getReactiveConnection() + */ + @Override + public LettuceReactiveRedisConnection getReactiveConnection() { + return new LettuceReactiveRedisConnection(client); + } + + @Override + public LettuceReactiveRedisClusterConnection getReactiveClusterConnection() { + if(!isClusterAware()) { + throw new InvalidDataAccessApiUsageException("Cluster is not configured!"); + } + + return new LettuceReactiveRedisClusterConnection((RedisClusterClient)client); + } + public void initConnection() { synchronized (this.connectionMonitor) { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java index 1c62fac19..34d1f17d8 100644 --- a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceConverters.java @@ -345,6 +345,10 @@ abstract public class LettuceConverters extends Converters { return BYTES_LIST_TO_TUPLE_LIST_CONVERTER; } + public static Point geoCoordinatesToPoint(GeoCoordinates geoCoordinates) { + return GEO_COORDINATE_TO_POINT_CONVERTER.convert(geoCoordinates); + } + public static Converter> stringToRedisClientListConverter() { return new Converter>() { diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java new file mode 100644 index 000000000..43569dd01 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterGeoCommands.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import org.springframework.data.redis.connection.ReactiveClusterGeoCommands; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterGeoCommands extends LettuceReactiveGeoCommands + implements ReactiveClusterGeoCommands { + + /** + * Create new {@link LettuceReactiveGeoCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterGeoCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java new file mode 100644 index 000000000..da1cff84f --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHashCommands.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import org.springframework.data.redis.connection.ReactiveClusterHashCommands; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterHashCommands extends LettuceReactiveHashCommands + implements ReactiveClusterHashCommands { + + /** + * Create new {@link LettuceReactiveHashCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterHashCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java new file mode 100644 index 000000000..fca3a3ab4 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommands.java @@ -0,0 +1,86 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterHyperLogLogCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since @since 2.0 + */ +public class LettuceReactiveClusterHyperLogLogCommands extends LettuceReactiveHyperLogLogCommands + implements ReactiveClusterHyperLogLogCommands { + + /** + * Create new {@link LettuceReactiveHyperLogLogCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterHyperLogLogCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } + + @Override + public Flux> pfMerge(Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null for PFMERGE"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty for PFMERGE!"); + + List keys = new ArrayList<>(command.getSourceKeys()); + keys.add(command.getKey()); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys.toArray(new ByteBuffer[keys.size()]))) { + return super.pfMerge(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to same slot for PFMERGE in cluster mode.")); + })); + } + + @Override + public Flux> pfCount( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getKeys(), "Keys must be null or empty for PFCOUNT!"); + + if (ClusterSlotHashUtil + .isSameSlotForAllKeys(command.getKeys().toArray(new ByteBuffer[command.getKeys().size()]))) { + return super.pfCount(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to same slot for PFCOUNT in cluster mode.")); + })); + } +} 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 new file mode 100644 index 000000000..8f6cc5136 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommands.java @@ -0,0 +1,143 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.RedisSystemException; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.data.redis.connection.RedisClusterNode; +import org.springframework.util.Assert; + +import com.lambdaworks.redis.RedisException; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +/** + * @author Christoph Strobl. + * @since 2.0 + */ +public class LettuceReactiveClusterKeyCommands extends LettuceReactiveKeyCommands + implements ReactiveClusterKeyCommands { + + private LettuceReactiveRedisClusterConnection connection; + + /** + * Create new {@link LettuceReactiveKeyCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterKeyCommands(LettuceReactiveRedisClusterConnection connection) { + + super(connection); + this.connection = connection; + } + + @Override + public Mono> keys(RedisClusterNode node, ByteBuffer pattern) { + + return connection.execute(node, cmd -> { + + Assert.notNull(pattern, "Pattern must not be null!"); + + Observable> result = cmd.keys(pattern.array()).map(ByteBuffer::wrap).toList(); + return Flux.from(LettuceReactiveRedisConnection.> monoConverter().convert(result)); + }).next(); + } + + @Override + public Mono randomKey(RedisClusterNode node) { + + return connection.execute(node, cmd -> { + + Observable result = cmd.randomkey().map(ByteBuffer::wrap); + return Flux.from(LettuceReactiveRedisConnection. monoConverter().convert(result)); + }).next(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#rename(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> rename(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "key must not be null."); + Assert.notNull(command.getNewName(), "NewName must not be null"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKey(), command.getNewName())) { + return super.rename(Mono.just(command)); + } + + Observable result = cmd.dump(command.getKey().array()) + .switchIfEmpty(Observable.error(new RedisSystemException("Cannot rename key that does not exist", + new RedisException("ERR no such key.")))) + .concatMap(value -> cmd.restore(command.getNewName().array(), 0, value) + .concatMap(res -> cmd.del(command.getKey().array()))) + .map(LettuceConverters.longToBooleanConverter()::convert); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(val -> new ReactiveRedisConnection.BooleanResponse<>(command, val)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#renameNX(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> renameNX(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null."); + Assert.notNull(command.getNewName(), "NewName must not be null"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKey(), command.getNewName())) { + return super.renameNX(Mono.just(command)); + } + + Observable result = + + cmd.exists(command.getNewName().array()).concatMap(exists -> { + + if (exists == 1) { + return Observable.just(Boolean.FALSE); + } + + return cmd.dump(command.getKey().array()) + .switchIfEmpty(Observable.error(new RedisSystemException("Cannot rename key that does not exist", + new RedisException("ERR no such key.")))) + .concatMap(value -> cmd.restore(command.getNewName().array(), 0, value) + .concatMap(res -> cmd.del(command.getKey().array()))) + .map(LettuceConverters.longToBooleanConverter()::convert); + + }); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(val -> new ReactiveRedisConnection.BooleanResponse<>(command, val)); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java new file mode 100644 index 000000000..4e184cdda --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommands.java @@ -0,0 +1,85 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterListCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterListCommands extends LettuceReactiveListCommands + implements ReactiveClusterListCommands { + + /** + * Create new {@link LettuceReactiveListCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterListCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } + + @Override + public Flux bPop(Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getDirection(), "Direction must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { + return super.bPop(Mono.just(command)); + } + + return Mono.error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for BPOP command.")); + })); + } + + @Override + public Flux> rPopLPush( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKey(), command.getDestination())) { + return super.rPopLPush(Mono.just(command)); + } + + Observable result = cmd.rpop(command.getKey().array()) + .concatMap(value -> cmd.lpush(command.getDestination().array(), value).map(x -> value)).map(ByteBuffer::wrap); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.ByteBufferResponse<>(command, value)); + })); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java new file mode 100644 index 000000000..fce4dc24e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterNumberCommands.java @@ -0,0 +1,36 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import org.springframework.data.redis.connection.ReactiveClusterNumberCommands; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterNumberCommands extends LettuceReactiveNumberCommands + implements ReactiveClusterNumberCommands { + + /** + * Create new {@link LettuceReactiveStringCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterNumberCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java new file mode 100644 index 000000000..2b62d34ef --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterSetCommands.java @@ -0,0 +1,250 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterSetCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterSetCommands extends LettuceReactiveSetCommands + implements ReactiveClusterSetCommands { + + /** + * Create new {@link LettuceReactiveSetCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterSetCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } + + @Override + public Flux> sUnion( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { + return super.sUnion(Mono.just(command)); + } + + Observable> result = Observable + .merge(command.getKeys().stream().map(key -> cmd.smembers(key.array())).collect(Collectors.toList())) + .map(ByteBuffer::wrap).distinct().toList(); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.MultiValueResponse<>(command, value)); + })); + } + + @Override + public Flux> sUnionStore( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Source keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List keys = new ArrayList<>(command.getKeys()); + keys.add(command.getKey()); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sUnionStore(Mono.just(command)); + } + + return sUnion(Mono.just(SUnionCommand.keys(command.getKeys()))).next().flatMap(values -> { + Observable result = cmd.sadd(command.getKey().array(), + values.getOutput().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])); + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.NumericResponse<>(command, value)); + }); + })); + } + + @Override + public Flux> sInter( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { + return super.sInter(Mono.just(command)); + } + + Observable> sourceSet = cmd.smembers(command.getKeys().get(0).array()).map(ByteBuffer::wrap) + .distinct().toList(); + + List>> intersectingSets = new ArrayList<>(); + + for (int i = 1; i < command.getKeys().size(); i++) { + intersectingSets.add(cmd.smembers(command.getKeys().get(i).array()).map(ByteBuffer::wrap).distinct().toList()); + } + + Observable> result = Observable.zip(sourceSet, Observable.merge(intersectingSets), + (source, intersecting) -> { + + source.retainAll(intersecting); + return source; + }); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.MultiValueResponse<>(command, value)); + })); + } + + @Override + public Flux> sInterStore( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Source keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List keys = new ArrayList<>(command.getKeys()); + keys.add(command.getKey()); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sInterStore(Mono.just(command)); + } + + return sInter(Mono.just(SInterCommand.keys(command.getKeys()))).next().flatMap(values -> { + Observable result = cmd.sadd(command.getKey().array(), + values.getOutput().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])); + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.NumericResponse<>(command, value)); + }); + })); + } + + @Override + public Flux> sDiff( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeys())) { + return super.sDiff(Mono.just(command)); + } + + Observable> sourceSet = cmd.smembers(command.getKeys().get(0).array()).map(ByteBuffer::wrap) + .distinct().toList(); + + List>> intersectingSets = new ArrayList<>(); + + for (int i = 1; i < command.getKeys().size(); i++) { + intersectingSets.add(cmd.smembers(command.getKeys().get(i).array()).map(ByteBuffer::wrap).distinct().toList()); + } + + Observable> result = Observable.zip(sourceSet, Observable.merge(intersectingSets), + (source, intersecting) -> { + + source.removeAll(intersecting); + return source; + }); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.MultiValueResponse<>(command, value)); + + })); + } + + @Override + public Flux> sDiffStore( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Source keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + List keys = new ArrayList<>(command.getKeys()); + keys.add(command.getKey()); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.sDiffStore(Mono.just(command)); + } + + return sDiff(Mono.just(SDiffCommand.keys(command.getKeys()))).next().flatMap(values -> { + Observable result = cmd.sadd(command.getKey().array(), + values.getOutput().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])); + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new ReactiveRedisConnection.NumericResponse<>(command, value)); + }); + })); + } + + @Override + public Flux> sMove(Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Source key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKey(), command.getDestination())) { + return super.sMove(Mono.just(command)); + } + + Observable result = cmd.exists(command.getKey().array()).flatMap(nrKeys -> nrKeys == 0 + ? Observable.empty() : cmd.sismember(command.getKey().array(), command.getValue().array())) + .flatMap(exists -> { + + if (!exists) { + return Observable.just(Boolean.FALSE); + } + return cmd.sismember(command.getDestination().array(), command.getValue().array()) + .flatMap(existsInTarget -> { + + Observable tmp = cmd.srem(command.getKey().array(), command.getValue().array()) + .map(nrRemoved -> nrRemoved > 0); + if (!existsInTarget) { + return tmp.flatMap(removed -> cmd.sadd(command.getDestination().array(), command.getValue().array()) + .map(LettuceConverters::toBoolean)); + } + return tmp; + }); + + }); + + return LettuceReactiveRedisConnection. monoConverter().convert(result.defaultIfEmpty(Boolean.FALSE)) + .map(value -> new ReactiveRedisConnection.BooleanResponse<>(command, value)); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java new file mode 100644 index 000000000..ba66d2990 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommands.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterStringCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterStringCommands extends LettuceReactiveStringCommands + implements ReactiveClusterStringCommands { + + /** + * Create new {@link LettuceReactiveStringCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterStringCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } + + @Override + public Flux> bitOp(Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + List keys = new ArrayList<>(command.getKeys()); + keys.add(command.getDestinationKey()); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(keys)) { + return super.bitOp(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for BITOP command.")); + })); + } + + @Override + public Flux> mSetNX(Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getKeyValuePairs().keySet())) { + return super.mSetNX(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for MSETNX command.")); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java new file mode 100644 index 000000000..2045f6e82 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommands.java @@ -0,0 +1,77 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ClusterSlotHashUtil; +import org.springframework.data.redis.connection.ReactiveClusterZSetCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since @since 2.0 + */ +public class LettuceReactiveClusterZSetCommands extends LettuceReactiveZSetCommands + implements ReactiveClusterZSetCommands { + + /** + * Create new {@link LettuceReactiveSetCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveClusterZSetCommands(LettuceReactiveRedisConnection connection) { + super(connection); + } + + @Override + public Flux> zUnionStore( + Publisher commands) { + + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty."); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getSourceKeys())) { + return super.zUnionStore(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for ZUNIONSTORE command.")); + })); + } + + @Override + public Flux> zInterStore( + Publisher commands) { + return getConnection().execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty."); + + if (ClusterSlotHashUtil.isSameSlotForAllKeys(command.getSourceKeys())) { + return super.zInterStore(Mono.just(command)); + } + + return Mono + .error(new InvalidDataAccessApiUsageException("All keys must map to the same slot for ZINTERSTORE command.")); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java new file mode 100644 index 000000000..3bc6b220e --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommands.java @@ -0,0 +1,224 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +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.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.RedisGeoCommands; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; +import org.springframework.util.Assert; + +import com.lambdaworks.redis.GeoArgs; +import com.lambdaworks.redis.GeoWithin; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveGeoCommands implements ReactiveGeoCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveGeoCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveGeoCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoAdd(org.reactivestreams.Publisher) + */ + @Override + public Flux> geoAdd(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getGeoLocations(), "Locations must not be null!"); + + List values = new ArrayList<>(); + for (GeoLocation location : command.getGeoLocations()) { + + Assert.notNull(location.getName(), "Location.Name must not be null!"); + Assert.notNull(location.getPoint(), "Location.Point must not be null!"); + + values.add(location.getPoint().getX()); + values.add(location.getPoint().getY()); + values.add(location.getName().array()); + } + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.geoadd(command.getKey().array(), values.toArray())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoDist(org.reactivestreams.Publisher) + */ + @Override + public Flux> geoDist(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFrom(), "From member must not be null!"); + Assert.notNull(command.getTo(), "To member must not be null!"); + + Metric metric = command.getMetric().isPresent() ? command.getMetric().get() : RedisGeoCommands.DistanceUnit.METERS; + + GeoArgs.Unit geoUnit = LettuceConverters.toGeoArgsUnit(metric); + Converter distanceConverter = LettuceConverters.distanceConverterForMetric(metric); + + Observable result = cmd + .geodist(command.getKey().array(), command.getFrom().array(), command.getTo().array(), geoUnit) + .map(distanceConverter::convert); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new CommandResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoHash(org.reactivestreams.Publisher) + */ + @Override + public Flux> geoHash(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMembers(), "Members must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.geohash(command.getKey().array(), + command.getMembers().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoPos(org.reactivestreams.Publisher) + */ + @Override + public Flux> geoPos(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMembers(), "Members must not be null!"); + + Observable> result = cmd + .geopos(command.getKey().array(), + command.getMembers().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(LettuceConverters::geoCoordinatesToPoint).toList(); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoRadius(org.reactivestreams.Publisher) + */ + @Override + public Flux>>> geoRadius( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getPoint(), "Point must not be null!"); + Assert.notNull(command.getDistance(), "Distance must not be null!"); + + GeoArgs geoArgs = command.getArgs().isPresent() ? LettuceConverters.toGeoArgs(command.getArgs().get()) : new GeoArgs(); + + Observable>> result = cmd + .georadius(command.getKey().array(), command.getPoint().getX(), command.getPoint().getY(), + command.getDistance().getValue(), LettuceConverters.toGeoArgsUnit(command.getDistance().getMetric()), + geoArgs) + .map(converter(command.getDistance().getMetric())::convert).toList().map(vals -> new GeoResults<>(vals)); + + return LettuceReactiveRedisConnection.>> monoConverter().convert(result) + .map(value -> new CommandResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveGeoCommands#geoRadiusByMember(org.reactivestreams.Publisher) + */ + @Override + public Flux>>> geoRadiusByMember( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getMember(), "Member must not be null!"); + Assert.notNull(command.getDistance(), "Distance must not be null!"); + + GeoArgs geoArgs = command.getArgs().isPresent() ? LettuceConverters.toGeoArgs(command.getArgs().get()) : new GeoArgs(); + + Observable>> result = cmd + .georadiusbymember(command.getKey().array(), command.getMember().array(), command.getDistance().getValue(), + LettuceConverters.toGeoArgsUnit(command.getDistance().getMetric()), geoArgs) + .map(converter(command.getDistance().getMetric())::convert).toList().map(vals -> new GeoResults<>(vals)); + + return LettuceReactiveRedisConnection.>> monoConverter().convert(result) + .map(value -> new CommandResponse<>(command, value)); + })); + } + + private Converter, GeoResult>> converter(Metric metric) { + + return (source) -> { + + Point point = LettuceConverters.geoCoordinatesToPoint(source.coordinates); + + return new GeoResult<>(new GeoLocation<>(ByteBuffer.wrap(source.member), point), + new Distance(source.distance != null ? source.distance : 0D, metric)); + }; + + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java new file mode 100644 index 000000000..17ff7585b --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommands.java @@ -0,0 +1,227 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveHashCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveHashCommands implements ReactiveHashCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveHashCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveHashCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hSet(org.reactivestreams.Publisher) + */ + @Override + public Flux> hSet(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFieldValueMap(), "FieldValueMap must not be null!"); + + Observable result = Observable.empty(); + + if (command.getFieldValueMap().size() == 1) { + + Entry entry = command.getFieldValueMap().entrySet().iterator().next(); + + result = ObjectUtils.nullSafeEquals(command.isUpsert(), Boolean.TRUE) + ? cmd.hset(command.getKey().array(), entry.getKey().array(), entry.getValue().array()) + : cmd.hsetnx(command.getKey().array(), entry.getKey().array(), entry.getValue().array()); + } else { + + Map entries = command.getFieldValueMap().entrySet().stream() + .collect(Collectors.toMap(e -> e.getKey().array(), e -> e.getValue().array())); + + result = cmd.hmset(command.getKey().array(), entries).map(LettuceConverters::stringToBoolean); + } + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hMGet(org.reactivestreams.Publisher) + */ + @Override + public Flux> hMGet(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFields(), "Fields must not be null!"); + + Observable> result = null; + + if (command.getFields().size() == 1) { + result = cmd.hget(command.getKey().array(), command.getFields().iterator().next().array()).map(ByteBuffer::wrap) + .map(val -> val != null ? Collections.singletonList(val) : Collections.emptyList()); + } else { + result = cmd + .hmget(command.getKey().array(), + command.getFields().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(val -> val != null ? ByteBuffer.wrap(val) : null).toList(); + } + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hExists(org.reactivestreams.Publisher) + */ + @Override + public Flux> hExists(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getName(), "Name must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.hexists(command.getKey().array(), command.getField().array())) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hDel(org.reactivestreams.Publisher) + */ + @Override + public Flux> hDel(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getFields(), "Fields must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.hdel(command.getKey().array(), + command.getFields().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hLen(org.reactivestreams.Publisher) + */ + @Override + public Flux> hLen(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Command.getKey() must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.hlen(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hKeys(org.reactivestreams.Publisher) + */ + @Override + public Flux> hKeys(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + Observable> result = cmd.hkeys(command.getKey().array()).map(ByteBuffer::wrap).toList(); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hKeys(org.reactivestreams.Publisher) + */ + @Override + public Flux> hVals(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + Observable> result = cmd.hvals(command.getKey().array()).map(ByteBuffer::wrap).toList(); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHashCommands#hGetAll(org.reactivestreams.Publisher) + */ + @Override + public Flux>> hGetAll(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + Observable> result = cmd.hgetall(command.getKey().array()).map(val -> val.entrySet() + .stream().collect(Collectors.toMap(e -> ByteBuffer.wrap(e.getKey()), e -> ByteBuffer.wrap(e.getValue())))); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new CommandResponse<>(command, value)); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java new file mode 100644 index 000000000..ca2c4c92a --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommands.java @@ -0,0 +1,107 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveHyperLogLogCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveHyperLogLogCommands implements ReactiveHyperLogLogCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveHyperLogLogCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveHyperLogLogCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHyperLogLogCommands#pfAdd(org.reactivestreams.Publisher) + */ + @Override + public Flux> pfAdd(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.pfadd(command.getKey().array(), + command.getValues().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHyperLogLogCommands#pfCount(org.reactivestreams.Publisher) + */ + @Override + public Flux> pfCount(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getKeys(), "Keys must not be empty for PFCOUNT."); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.pfcount(command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveHyperLogLogCommands#pfMerge(org.reactivestreams.Publisher) + */ + @Override + public Flux> pfMerge(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null for PFMERGE."); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null for PFMERGE."); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd + .pfmerge(command.getKey().array(), + command.getSourceKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(LettuceConverters::stringToBoolean)) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + protected LettuceReactiveRedisConnection getConnection() { + return connection; + } +} 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 new file mode 100644 index 000000000..b776c4f8d --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommands.java @@ -0,0 +1,183 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.List; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.DataType; +import org.springframework.data.redis.connection.ReactiveKeyCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.CommandResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveKeyCommands implements ReactiveKeyCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveKeyCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveKeyCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#exists(org.reactivestreams.Publisher) + */ + @Override + public Flux> exists(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.exists(command.getKey().array()).map(LettuceConverters.longToBooleanConverter()::convert) + .map((value) -> new BooleanResponse<>(command, value))); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#type(org.reactivestreams.Publisher) + */ + @Override + public Flux> type(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.type(command.getKey().array()).map(LettuceConverters::toDataType)) + .map(respValue -> new CommandResponse<>(command, respValue)); + })); + } + + /* + * (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 LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.del(command.getKey().array()).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 LettuceReactiveRedisConnection., Long>> monoConverter() + .convert(cmd + .del(keys.stream().map(ByteBuffer::array).collect(Collectors.toList()).toArray(new byte[keys.size()][])) + .map((value) -> new NumericResponse<>(keys, value))); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#keys(org.reactivestreams.Publisher) + */ + @Override + public Flux> keys(Publisher patterns) { + return connection.execute(cmd -> Flux.from(patterns).flatMap(pattern -> { + + Assert.notNull(pattern, "Pattern must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.keys(pattern.array()).map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(pattern, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#randomKey() + */ + @Override + public Mono randomKey() { + return connection.execute(cmd -> LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.randomkey().map(ByteBuffer::wrap))).next(); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#rename(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> rename(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewName(), "New name must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert( + cmd.rename(command.getKey().array(), command.getNewName().array()).map(LettuceConverters::stringToBoolean)) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveKeyCommands#rename(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> renameNX(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getNewName(), "New name must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.renamenx(command.getKey().array(), command.getNewName().array())) + .map(value -> new BooleanResponse<>(command, value)); + })); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java new file mode 100644 index 000000000..3123ad396 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommands.java @@ -0,0 +1,315 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ReactiveListCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.data.redis.connection.RedisListCommands.Position; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveListCommands implements ReactiveListCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveListCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveListCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lPush(org.reactivestreams.Publisher) + */ + @Override + public Flux> push(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getValues(), "Values must not be null or empty!"); + + if (!command.getUpsert() && command.getValues().size() > 1) { + throw new InvalidDataAccessApiUsageException( + String.format("%s PUSHX only allows one value!", command.getDirection())); + } + + byte[][] values = command.getValues().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]); + + Observable pushResult = null; + + if (ObjectUtils.nullSafeEquals(Direction.RIGHT, command.getDirection())) { + pushResult = command.getUpsert() ? cmd.rpush(command.getKey().array(), values) + : cmd.rpushx(command.getKey().array(), values[0]); + } else { + pushResult = command.getUpsert() ? cmd.lpush(command.getKey().array(), values) + : cmd.lpushx(command.getKey().array(), values[0]); + } + + return LettuceReactiveRedisConnection. monoConverter().convert(pushResult) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lLen(org.reactivestreams.Publisher) + */ + @Override + public Flux> lLen(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.llen(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lRange(org.reactivestreams.Publisher) + */ + @Override + public Flux> lRange(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd + .lrange(command.getKey().array(), command.getRange().getLowerBound(), command.getRange().getUpperBound()) + .map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lTrim(org.reactivestreams.Publisher) + */ + @Override + public Flux> lTrim(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd + .ltrim(command.getKey().array(), command.getRange().getLowerBound(), command.getRange().getUpperBound()) + .map(LettuceConverters::stringToBoolean)) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lIndex(org.reactivestreams.Publisher) + */ + @Override + public Flux> lIndex(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getIndex(), "Index value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.lindex(command.getKey().array(), command.getIndex()).map(ByteBuffer::wrap)) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lInsert(org.reactivestreams.Publisher) + */ + @Override + public Flux> lInsert(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getPivot(), "Pivot must not be null!"); + Assert.notNull(command.getPosition(), "Position must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.linsert(command.getKey().array(), Position.BEFORE.equals(command.getPosition()), + command.getPivot().array(), command.getValue().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lSet(org.reactivestreams.Publisher) + */ + @Override + public Flux> lSet(Publisher commands) { + + return connection.execute(cmd -> { + + return Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "value must not be null!"); + Assert.notNull(command.getIndex(), "Index must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.lset(command.getKey().array(), command.getIndex(), command.getValue().array()) + .map(LettuceConverters::stringToBoolean)) + .map(value -> new BooleanResponse<>(command, value)); + }); + }); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#lRem(org.reactivestreams.Publisher) + */ + @Override + public Flux> lRem(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getCount(), "Count must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.lrem(command.getKey().array(), command.getCount(), command.getValue().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#rPop(org.reactivestreams.Publisher) + */ + @Override + public Flux> pop(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDirection(), "Direction must not be null!"); + + Observable popResult = ObjectUtils.nullSafeEquals(Direction.RIGHT, command.getDirection()) + ? cmd.rpop(command.getKey().array()) : cmd.lpop(command.getKey().array()); + + return LettuceReactiveRedisConnection. monoConverter().convert(popResult.map(ByteBuffer::wrap)) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#bPop(org.reactivestreams.Publisher) + */ + @Override + public Flux bPop(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getDirection(), "Direction must not be null!"); + + byte[][] keys = command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]); + long timeout = command.getTimeout().get(ChronoUnit.SECONDS); + + Observable mappedObservable = (ObjectUtils.nullSafeEquals(Direction.RIGHT, command.getDirection()) + ? cmd.brpop(timeout, keys) : cmd.blpop(timeout, keys)) + .map(kv -> Arrays.asList(ByteBuffer.wrap(kv.key), ByteBuffer.wrap(kv.value))) + .map(val -> new PopResult(val)); + + return LettuceReactiveRedisConnection. monoConverter().convert(mappedObservable) + .map(value -> new PopResponse(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#rPopLPush(org.reactivestreams.Publisher) + */ + @Override + public Flux> rPopLPush(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.rpoplpush(command.getKey().array(), command.getDestination().array()).map(ByteBuffer::wrap)) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveListCommands#bRPopLPush(org.reactivestreams.Publisher) + */ + @Override + public Flux> bRPopLPush(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + Assert.notNull(command.getTimeout(), "Timeout must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.brpoplpush(command.getTimeout().get(ChronoUnit.SECONDS), command.getKey().array(), + command.getDestination().array()).map(ByteBuffer::wrap)) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + protected LettuceReactiveRedisConnection getConnection() { + return connection; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java new file mode 100644 index 000000000..9a37a31fc --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommands.java @@ -0,0 +1,161 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveNumberCommands; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.util.Assert; +import org.springframework.util.NumberUtils; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveNumberCommands implements ReactiveNumberCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveStringCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveNumberCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveNumberCommands#incr(org.reactivestreams.Publisher) + */ + @Override + public Flux> incr(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.incr(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveNumberCommands#incrBy(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux, T>> incrBy(Publisher> commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value for INCRBY must not be null."); + + T incrBy = command.getValue(); + + Observable result = null; + if (incrBy instanceof Double || incrBy instanceof Float) { + result = cmd.incrbyfloat(command.getKey().array(), incrBy.doubleValue()); + } else { + result = cmd.incrby(command.getKey().array(), incrBy.longValue()); + } + + return LettuceReactiveRedisConnection. monoConverter() + .convert(result.map(val -> NumberUtils.convertNumberToTargetClass(val, incrBy.getClass()))) + .map(res -> new NumericResponse<>(command, res)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveNumberCommands#decr(org.reactivestreams.Publisher) + */ + @Override + public Flux> decr(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.decr(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveNumberCommands#decrBy(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux, T>> decrBy(Publisher> commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value for DECRBY must not be null."); + + T decrBy = command.getValue(); + + Observable result = null; + if (decrBy instanceof Double || decrBy instanceof Float) { + result = cmd.incrbyfloat(command.getKey().array(), decrBy.doubleValue() * (-1.0D)); + } else { + result = cmd.decrby(command.getKey().array(), decrBy.longValue()); + } + + return LettuceReactiveRedisConnection. monoConverter() + .convert(result.map(val -> NumberUtils.convertNumberToTargetClass(val, decrBy.getClass()))) + .map(res -> new NumericResponse<>(command, res)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveNumberCommands#hIncrBy(org.reactivestreams.Publisher) + */ + @Override + public Flux, T>> hIncrBy(Publisher> commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + T incrBy = command.getValue(); + + Observable result = null; + + if (incrBy instanceof Double || incrBy instanceof Float) { + result = cmd.hincrbyfloat(command.getKey().array(), command.getField().array(), incrBy.doubleValue()); + } else { + result = cmd.hincrby(command.getKey().array(), command.getField().array(), incrBy.longValue()); + } + + return LettuceReactiveRedisConnection. monoConverter() + .convert(result.map(val -> NumberUtils.convertNumberToTargetClass(val, incrBy.getClass()))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java new file mode 100644 index 000000000..fe54eec8c --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisClusterConnection.java @@ -0,0 +1,128 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import org.springframework.data.redis.connection.ReactiveRedisClusterConnection; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import com.lambdaworks.redis.api.rx.RedisReactiveCommands; +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; +import com.lambdaworks.redis.cluster.api.rx.RedisClusterReactiveCommands; + +import reactor.core.publisher.Flux; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveRedisClusterConnection extends LettuceReactiveRedisConnection + implements ReactiveRedisClusterConnection { + + public LettuceReactiveRedisClusterConnection(RedisClusterClient client) { + super(client); + } + + @Override + public LettuceReactiveClusterKeyCommands keyCommands() { + return new LettuceReactiveClusterKeyCommands(this); + } + + @Override + public LettuceReactiveClusterListCommands listCommands() { + return new LettuceReactiveClusterListCommands(this); + } + + @Override + public LettuceReactiveClusterSetCommands setCommands() { + return new LettuceReactiveClusterSetCommands(this); + } + + @Override + public LettuceReactiveClusterZSetCommands zSetCommands() { + return new LettuceReactiveClusterZSetCommands(this); + } + + @Override + public LettuceReactiveClusterHyperLogLogCommands hyperLogLogCommands() { + return new LettuceReactiveClusterHyperLogLogCommands(this); + } + + @Override + public LettuceReactiveClusterStringCommands stringCommands() { + return new LettuceReactiveClusterStringCommands(this); + } + + @Override + public LettuceReactiveClusterGeoCommands geoCommands() { + return new LettuceReactiveClusterGeoCommands(this); + } + + @Override + public LettuceReactiveClusterHashCommands hashCommands() { + return new LettuceReactiveClusterHashCommands(this); + } + + @Override + public LettuceReactiveClusterNumberCommands numberCommands() { + return new LettuceReactiveClusterNumberCommands(this); + } + + /** + * @param callback + * @return + */ + public Flux execute(RedisNode node, LettuceReactiveCallback callback) { + + try { + Assert.notNull(callback, "ReactiveCallback must not be null!"); + Assert.notNull(node, "Node must not be null!"); + } catch (IllegalArgumentException e) { + return Flux.error(e); + } + + return Flux.defer(() -> callback.doWithCommands(getCommands(node))).onErrorResumeWith(translateExeception()); + } + + @Override + protected StatefulRedisClusterConnection getConnection() { + + Assert.isInstanceOf(StatefulRedisClusterConnection.class, super.getConnection(), + "Connection needs to be instance of StatefulRedisClusterConnection"); + + return (StatefulRedisClusterConnection) super.getConnection(); + } + + protected RedisClusterReactiveCommands getCommands() { + return getConnection().reactive(); + } + + protected RedisReactiveCommands getCommands(RedisNode node) { + + if (!(getConnection() instanceof StatefulRedisClusterConnection)) { + throw new IllegalArgumentException("o.O connection needs to be cluster compatible " + getConnection()); + } + + if (StringUtils.hasText(node.getId())) { + return ((StatefulRedisClusterConnection) getConnection()).getConnection(node.getId()).reactive(); + } + + return ((StatefulRedisClusterConnection) getConnection()).getConnection(node.getHost(), node.getPort()).reactive(); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java new file mode 100644 index 000000000..ed02f6502 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveRedisConnection.java @@ -0,0 +1,162 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.util.function.Function; + +import org.reactivestreams.Publisher; +import org.springframework.core.convert.converter.Converter; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.data.redis.connection.*; +import org.springframework.util.Assert; + +import com.lambdaworks.redis.AbstractRedisClient; +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.api.StatefulConnection; +import com.lambdaworks.redis.api.StatefulRedisConnection; +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; +import com.lambdaworks.redis.cluster.api.rx.RedisClusterReactiveCommands; +import com.lambdaworks.redis.codec.ByteArrayCodec; +import com.lambdaworks.redis.codec.RedisCodec; + +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveRedisConnection implements ReactiveRedisConnection { + + private StatefulConnection connection; + + private static final RedisCodec CODEC = new ByteArrayCodec(); + + public LettuceReactiveRedisConnection(AbstractRedisClient client) { + + Assert.notNull(client, "RedisClient must not be null!"); + + if (client instanceof RedisClient) { + connection = ((RedisClient) client).connect(CODEC); + } else if (client instanceof RedisClusterClient) { + connection = ((RedisClusterClient) client).connect(CODEC); + } else { + throw new InvalidDataAccessResourceUsageException( + String.format("Cannot use client of type %s", client.getClass())); + } + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection#keyCommands() + */ + @Override + public ReactiveKeyCommands keyCommands() { + return new LettuceReactiveKeyCommands(this); + } + + @Override + public ReactiveStringCommands stringCommands() { + return new LettuceReactiveStringCommands(this); + } + + @Override + public ReactiveNumberCommands numberCommands() { + return new LettuceReactiveNumberCommands(this); + } + + @Override + public ReactiveListCommands listCommands() { + return new LettuceReactiveListCommands(this); + } + + @Override + public ReactiveSetCommands setCommands() { + return new LettuceReactiveSetCommands(this); + } + + @Override + public ReactiveZSetCommands zSetCommands() { + return new LettuceReactiveZSetCommands(this); + } + + @Override + public ReactiveHashCommands hashCommands() { + return new LettuceReactiveHashCommands(this); + } + + @Override + public ReactiveGeoCommands geoCommands() { + return new LettuceReactiveGeoCommands(this); + } + + @Override + public ReactiveHyperLogLogCommands hyperLogLogCommands() { + return new LettuceReactiveHyperLogLogCommands(this); + } + + /** + * @param callback + * @return + */ + public Flux execute(LettuceReactiveCallback callback) { + return Flux.defer(() -> callback.doWithCommands(getCommands())).onErrorResumeWith(translateExeception()); + } + + @Override + public void close() { + connection.close(); + } + + protected StatefulConnection getConnection() { + return connection; + } + + protected RedisClusterReactiveCommands getCommands() { + + if (connection instanceof StatefulRedisConnection) { + return ((StatefulRedisConnection) connection).reactive(); + } else if (connection instanceof StatefulRedisClusterConnection) { + return ((StatefulRedisClusterConnection) connection).reactive(); + } + + throw new RuntimeException("o.O unknown connection type " + connection); + } + + Function> translateExeception() { + + return throwable -> { + + if (throwable instanceof RuntimeException) { + + DataAccessException convertedException = null; + if (throwable instanceof RuntimeException) { + convertedException = LettuceConverters.exceptionConverter().convert((RuntimeException) throwable); + } + return Flux.error(convertedException != null ? convertedException : throwable); + } + + return Flux.error(throwable); + }; + } + + interface LettuceReactiveCallback { + Publisher doWithCommands(RedisClusterReactiveCommands cmd); + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java new file mode 100644 index 000000000..3b78ae0a3 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommands.java @@ -0,0 +1,316 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveSetCommands; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveSetCommands implements ReactiveSetCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveSetCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveSetCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sAdd(org.reactivestreams.Publisher) + */ + @Override + public Flux> sAdd(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.sadd(command.getKey().array(), + command.getValues().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sRem(org.reactivestreams.Publisher) + */ + @Override + public Flux> sRem(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValues(), "Values must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.srem(command.getKey().array(), + command.getValues().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sPop(org.reactivestreams.Publisher) + */ + @Override + public Flux> sPop(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.spop(command.getKey().array()).map(ByteBuffer::wrap)) + .map(value -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sMove(org.reactivestreams.Publisher) + */ + @Override + public Flux> sMove(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getDestination(), "Destination key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.smove(command.getKey().array(), command.getDestination().array(), command.getValue().array())) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sCard(org.reactivestreams.Publisher) + */ + @Override + public Flux> sCard(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.scard(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sIsMember(org.reactivestreams.Publisher) + */ + @Override + public Flux> sIsMember(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.sismember(command.getKey().array(), command.getValue().array())) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInter(org.reactivestreams.Publisher) + */ + @Override + public Flux> sInter(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.sinter(command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInterStore(org.reactivestreams.Publisher) + */ + @Override + public Flux> sInterStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.sinterstore(command.getKey().array(), + command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInter(org.reactivestreams.Publisher) + */ + @Override + public Flux> sUnion(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.sunion(command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInterStore(org.reactivestreams.Publisher) + */ + @Override + public Flux> sUnionStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.sunionstore(command.getKey().array(), + command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInter(org.reactivestreams.Publisher) + */ + @Override + public Flux> sDiff(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.sdiff(command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][])) + .map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sInterStore(org.reactivestreams.Publisher) + */ + @Override + public Flux> sDiffStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKeys(), "Keys must not be null!"); + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.sdiffstore(command.getKey().array(), + command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sMembers(org.reactivestreams.Publisher) + */ + @Override + public Flux> sMembers(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.smembers(command.getKey().array()).map(ByteBuffer::wrap).toList()) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveSetCommands#sRandMembers(org.reactivestreams.Publisher) + */ + @Override + public Flux> sRandMember( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + boolean singleElement = !command.getCount().isPresent() || command.getCount().get().equals(1L); + + Observable> result = singleElement + ? cmd.srandmember(command.getKey().array()).map(ByteBuffer::wrap).map(Collections::singletonList) + : cmd.srandmember(command.getKey().array(), command.getCount().get()).map(ByteBuffer::wrap).toList(); + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + protected LettuceReactiveRedisConnection getConnection() { + return connection; + } +} diff --git a/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java new file mode 100644 index 000000000..efa59cb98 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommands.java @@ -0,0 +1,415 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.RangeCommand; +import org.springframework.data.redis.connection.ReactiveStringCommands; +import org.springframework.util.Assert; + +import com.lambdaworks.redis.SetArgs; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveStringCommands implements ReactiveStringCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveStringCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveStringCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#mGet(org.reactivestreams.Publisher) + */ + @Override + public Flux, ByteBuffer>> mGet(Publisher> keyCollections) { + + return connection.execute(cmd -> Flux.from(keyCollections).flatMap((keys) -> { + + Assert.notNull(keys, "Keys must not be null!"); + + return LettuceReactiveRedisConnection., ByteBuffer>> monoConverter() + .convert(cmd + .mget(keys.stream().map(ByteBuffer::array).collect(Collectors.toList()).toArray(new byte[keys.size()][])) + .map((value) -> value != null ? ByteBuffer.wrap(value) : ByteBuffer.allocate(0)).toList() + .map((values) -> new MultiValueResponse<>(keys, values))); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#set(org.reactivestreams.Publisher) + */ + @Override + public Flux> set(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + SetArgs args = null; + + if (command.getExpiration().isPresent() || command.getOption().isPresent()) { + args = LettuceConverters.toSetArgs(command.getExpiration().isPresent() ? command.getExpiration().get() : null, + command.getOption().isPresent() ? command.getOption().get() : null); + } + + return LettuceReactiveRedisConnection. monoConverter().convert( + + args != null ? cmd.set(command.getKey().array(), command.getValue().array(), args) + : cmd.set(command.getKey().array(), command.getValue().array()).map(LettuceConverters::stringToBoolean)) + .map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#getSet(org.reactivestreams.Publisher) + */ + @Override + public Flux> getSet(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + if (command.getExpiration().isPresent() || command.getOption().isPresent()) { + throw new IllegalArgumentException("Command must not define exipiration nor option for GETSET."); + } + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(cmd.getset(command.getKey().array(), command.getValue().array()) + .map((value) -> new ByteBufferResponse<>(command, ByteBuffer.wrap(value))) + .defaultIfEmpty(new ByteBufferResponse<>(command, ByteBuffer.allocate(0)))); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#get(org.reactivestreams.Publisher) + */ + @Override + public Flux> get(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap((command) -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.get(command.getKey().array()).map(ByteBuffer::wrap)) + .map((value) -> new ByteBufferResponse<>(command, value)) + .defaultIfEmpty(new ByteBufferResponse<>(command, ByteBuffer.allocate(0))); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#setNX(org.reactivestreams.Publisher) + */ + @Override + public Flux> setNX(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.setnx(command.getKey().array(), command.getValue().array())) + .map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#setEX(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> setEX(Publisher commands) { + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.isTrue(command.getExpiration().isPresent(), "Expiration time must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.setex(command.getKey().array(), command.getExpiration().get().getExpirationTimeInSeconds(), + command.getValue().array())) + .map(LettuceConverters::stringToBoolean).map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#pSetEX(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> pSetEX(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.isTrue(command.getExpiration().isPresent(), "Expiration time must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.psetex(command.getKey().array(), command.getExpiration().get().getExpirationTimeInMilliseconds(), + command.getValue().array())) + .map(LettuceConverters::stringToBoolean).map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#mSet(org.reactivestreams.Publisher) + */ + @Override + public Flux> mSet(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getKeyValuePairs(), "Pairs must not be null or empty!"); + + Map map = new LinkedHashMap<>(); + command.getKeyValuePairs().entrySet().forEach(entry -> map.put(entry.getKey().array(), entry.getValue().array())); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.mset(map)) + .map(LettuceConverters::stringToBoolean).map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#mSet(org.reactivestreams.Publisher) + */ + @Override + public Flux> mSetNX(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notEmpty(command.getKeyValuePairs(), "Pairs must not be null or empty!"); + + Map map = new LinkedHashMap<>(); + command.getKeyValuePairs().entrySet().forEach(entry -> map.put(entry.getKey().array(), entry.getValue().array())); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.msetnx(map)) + .map((value) -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#append(org.reactivestreams.Publisher) + */ + @Override + public Flux> append(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.append(command.getKey().array(), command.getValue().array())) + .map((value) -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#getRange(org.reactivestreams.Publisher, java.util.function.Supplier, java.util.function.Supplier) + */ + @Override + public Flux> getRange(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + Range range = command.getRange(); + + return LettuceReactiveRedisConnection + . monoConverter().convert(cmd + .getrange(command.getKey().array(), range.getLowerBound(), range.getUpperBound()).map(ByteBuffer::wrap)) + .map((value) -> new ByteBufferResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#setRange(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> setRange(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.setrange(command.getKey().array(), command.getOffset(), command.getValue().array())) + .map((value) -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#getBit(org.reactivestreams.Publisher, java.util.function.Supplier) + */ + @Override + public Flux> getBit(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.getbit(command.getKey().array(), command.getOffset()).map(LettuceConverters::toBoolean)) + .map(value -> new BooleanResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#setBit(org.reactivestreams.Publisher, java.util.function.Supplier, java.util.function.Supplier) + */ + @Override + public Flux> setBit(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + Assert.notNull(command.getOffset(), "Offset must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.setbit(command.getKey().array(), command.getOffset(), command.getValue() ? 1 : 0) + .map(LettuceConverters::toBoolean)) + .map(respValue -> new BooleanResponse<>(command, respValue)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#bitCount(org.reactivestreams.Publisher, java.util.function.Supplier, java.util.function.Supplier) + */ + @Override + public Flux> bitCount(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + Range range = command.getRange(); + return LettuceReactiveRedisConnection. monoConverter() + .convert(range != null ? cmd.bitcount(command.getKey().array(), range.getLowerBound(), range.getUpperBound()) + : cmd.bitcount(command.getKey().array())) + .map(responseValue -> new NumericResponse<>(command, responseValue)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#bitOp(org.reactivestreams.Publisher, java.util.function.Supplier, java.util.function.Supplier) + */ + @Override + public Flux> bitOp(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getDestinationKey(), "DestinationKey must not be null!"); + Assert.notEmpty(command.getKeys(), "Keys must not be null or empty"); + + Observable result = null; + byte[] destinationKey = command.getDestinationKey().array(); + byte[][] sourceKeys = command.getKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]); + + switch (command.getBitOp()) { + case AND: + result = cmd.bitopAnd(destinationKey, sourceKeys); + break; + case OR: + result = cmd.bitopOr(destinationKey, sourceKeys); + break; + case XOR: + result = cmd.bitopXor(destinationKey, sourceKeys); + break; + case NOT: + + Assert.isTrue(sourceKeys.length == 1, "BITOP NOT does not allow more than 1 source key."); + + result = cmd.bitopNot(destinationKey, sourceKeys[0]); + break; + default: + throw new IllegalArgumentException(String.format("Unknown BITOP '%s'.", command.getBitOp())); + } + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveRedisConnection.ReactiveStringCommands#strLen(org.reactivestreams.Publisher) + */ + @Override + public Flux> strLen(Publisher commands) { + return connection.execute(cmd -> { + + return Flux.from(commands).flatMap(command -> { + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.strlen(command.getKey().array())) + .map(respValue -> new NumericResponse<>(command, respValue)); + }); + }); + } + + protected LettuceReactiveRedisConnection getConnection() { + return connection; + } +} 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 new file mode 100644 index 000000000..1484b1ec8 --- /dev/null +++ b/src/main/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommands.java @@ -0,0 +1,542 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.reactivestreams.Publisher; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.NumericResponse; +import org.springframework.data.redis.connection.ReactiveZSetCommands; +import org.springframework.data.redis.connection.RedisZSetCommands.Aggregate; +import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +import com.lambdaworks.redis.ScoredValue; +import com.lambdaworks.redis.ZAddArgs; +import com.lambdaworks.redis.ZStoreArgs; + +import reactor.core.publisher.Flux; +import rx.Observable; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveZSetCommands implements ReactiveZSetCommands { + + private final LettuceReactiveRedisConnection connection; + + /** + * Create new {@link LettuceReactiveSetCommands}. + * + * @param connection must not be {@literal null}. + */ + public LettuceReactiveZSetCommands(LettuceReactiveRedisConnection connection) { + + Assert.notNull(connection, "Connection must not be null!"); + this.connection = connection; + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zAdd(org.reactivestreams.Publisher) + */ + @Override + @SuppressWarnings("unchecked") + public Flux> zAdd(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getTuples(), "Tuples must not be empty or null."); + + ZAddArgs args = null; + + if (command.getIncr().isPresent() || command.getUpsert().isPresent() || command.getReturnTotalChanged().isPresent()) { + + if (command.getIncr().isPresent() && ObjectUtils.nullSafeEquals(command.getIncr().get(), Boolean.TRUE)) { + + if (command.getTuples().size() > 1) { + throw new IllegalArgumentException("ZADD INCR must not contain more than one tuple."); + } + + Tuple tuple = command.getTuples().iterator().next(); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.zaddincr(command.getKey().array(), tuple.getScore(), tuple.getValue())) + .map(value -> new NumericResponse<>(command, value)); + } + + if (command.getReturnTotalChanged().isPresent() && ObjectUtils.nullSafeEquals(command.getReturnTotalChanged().get(), Boolean.TRUE)) { + args = ZAddArgs.Builder.ch(); + } + + if (command.getUpsert().isPresent()) { + + if (command.getUpsert().get().equals(Boolean.TRUE)) { + args = ZAddArgs.Builder.nx(); + } else { + args = ZAddArgs.Builder.xx(); + } + } + } + + ScoredValue[] values = (ScoredValue[]) command.getTuples().stream() + .map(tuple -> new ScoredValue(tuple.getScore(), tuple.getValue())) + .toArray(size -> new ScoredValue[size]); + + Observable result = args == null ? cmd.zadd(command.getKey().array(), values) + : cmd.zadd(command.getKey().array(), args, values); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRem(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRem(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notEmpty(command.getValues(), "Values must not be null or empty!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.zrem(command.getKey().array(), + command.getValues().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]))) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zIncrBy(org.reactivestreams.Publisher) + */ + @Override + public Flux> zIncrBy(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Member must not be null!"); + Assert.notNull(command.getIncrement(), "Increment value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert( + cmd.zincrby(command.getKey().array(), command.getIncrement().doubleValue(), command.getValue().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRank(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRank(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + Observable result = ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC) + ? cmd.zrank(command.getKey().array(), command.getValue().array()) + : cmd.zrevrank(command.getKey().array(), command.getValue().array()); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRange(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRange(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + Observable> result = Observable.empty(); + + if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) { + if (command.getWithScores().isPresent() && ObjectUtils.nullSafeEquals(command.getWithScores().get(), Boolean.TRUE)) { + + result = cmd.zrangeWithScores(command.getKey().array(), command.getRange().getLowerBound(), + command.getRange().getUpperBound()).map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } else { + + result = cmd + .zrange(command.getKey().array(), command.getRange().getLowerBound(), command.getRange().getUpperBound()) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } + } else { + if (command.getWithScores().isPresent() && ObjectUtils.nullSafeEquals(command.getWithScores().get(), Boolean.TRUE)) { + result = cmd.zrevrangeWithScores(command.getKey().array(), command.getRange().getLowerBound(), + command.getRange().getUpperBound()).map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } else { + + result = cmd + .zrevrange(command.getKey().array(), command.getRange().getLowerBound(), + command.getRange().getUpperBound()) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } + } + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRange(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRangeByScore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + Object lowerBound = AgrumentConverters.lowerBoundArgOf(command.getRange()); + Object upperBound = AgrumentConverters.upperBoundArgOf(command.getRange()); + + boolean requiresStringConversion = lowerBound instanceof String || upperBound instanceof String; + + boolean isLimited = command.getLimit().isPresent(); + + Observable> result = Observable.empty(); + + if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) { + if (command.getWithScores().isPresent() && ObjectUtils.nullSafeEquals(command.getWithScores().get(), Boolean.TRUE)) { + + if (!isLimited) { + result = (requiresStringConversion + ? cmd.zrangebyscoreWithScores(command.getKey().array(), lowerBound.toString(), upperBound.toString()) + : cmd.zrangebyscoreWithScores(command.getKey().array(), (Double) lowerBound, (Double) upperBound)) + .map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } else { + result = (requiresStringConversion + ? cmd.zrangebyscoreWithScores(command.getKey().array(), lowerBound.toString(), upperBound.toString(), + command.getLimit().get().getOffset(), command.getLimit().get().getCount()) + : cmd.zrangebyscoreWithScores(command.getKey().array(), (Double) lowerBound, (Double) upperBound, + command.getLimit().get().getOffset(), command.getLimit().get().getCount())) + .map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } + } else { + + if (!isLimited) { + result = (requiresStringConversion + ? cmd.zrangebyscore(command.getKey().array(), lowerBound.toString(), upperBound.toString()) + : cmd.zrangebyscore(command.getKey().array(), (Double) lowerBound, (Double) upperBound)) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } else { + + result = (requiresStringConversion + ? cmd.zrangebyscore(command.getKey().array(), lowerBound.toString(), upperBound.toString(), + command.getLimit().get().getOffset(), command.getLimit().get().getCount()) + : cmd.zrangebyscore(command.getKey().array(), (Double) lowerBound, (Double) upperBound, + command.getLimit().get().getOffset(), command.getLimit().get().getCount())) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } + } + } else { + if (command.getWithScores().isPresent() && ObjectUtils.nullSafeEquals(command.getWithScores().get(), Boolean.TRUE)) { + + if (!isLimited) { + result = (requiresStringConversion + ? cmd.zrevrangebyscoreWithScores(command.getKey().array(), lowerBound.toString(), upperBound.toString()) + : cmd.zrevrangebyscoreWithScores(command.getKey().array(), (Double) lowerBound, (Double) upperBound)) + .map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } else { + + result = (requiresStringConversion + ? cmd.zrevrangebyscoreWithScores(command.getKey().array(), lowerBound.toString(), upperBound.toString(), + command.getLimit().get().getOffset(), command.getLimit().get().getCount()) + : cmd.zrevrangebyscoreWithScores(command.getKey().array(), (Double) lowerBound, (Double) upperBound, + command.getLimit().get().getOffset(), command.getLimit().get().getCount())) + .map(sc -> (Tuple) new DefaultTuple(sc.value, sc.score)).toList(); + } + } else { + + if (!isLimited) { + result = (requiresStringConversion + ? cmd.zrevrangebyscore(command.getKey().array(), lowerBound.toString(), upperBound.toString()) + : cmd.zrevrangebyscore(command.getKey().array(), (Double) lowerBound, (Double) upperBound)) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } else { + + result = (requiresStringConversion + ? cmd.zrevrangebyscore(command.getKey().array(), lowerBound.toString(), upperBound.toString(), + command.getLimit().get().getOffset(), command.getLimit().get().getCount()) + : cmd.zrevrangebyscore(command.getKey().array(), (Double) lowerBound, (Double) upperBound, + command.getLimit().get().getOffset(), command.getLimit().get().getCount())) + .map(value -> (Tuple) new DefaultTuple(value, Double.NaN)).toList(); + } + } + } + + return LettuceReactiveRedisConnection.> monoConverter().convert(result) + .map(value -> new MultiValueResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zCount(org.reactivestreams.Publisher) + */ + @Override + public Flux> zCount(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + Object lowerBound = AgrumentConverters.lowerBoundArgOf(command.getRange()); + Object upperBound = AgrumentConverters.upperBoundArgOf(command.getRange()); + + Observable result = Observable.empty(); + + if (lowerBound instanceof String || upperBound instanceof String) { + result = cmd.zcount(command.getKey().array(), lowerBound.toString(), upperBound.toString()); + } else { + result = cmd.zcount(command.getKey().array(), (Double) lowerBound, (Double) upperBound); + } + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zCard(org.reactivestreams.Publisher) + */ + @Override + public Flux> zCard(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter().convert(cmd.zcard(command.getKey().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zScore(org.reactivestreams.Publisher) + */ + @Override + public Flux> zScore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getValue(), "Value must not be null!"); + + return LettuceReactiveRedisConnection. monoConverter() + .convert(cmd.zscore(command.getKey().array(), command.getValue().array())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRemRangeByRank(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRemRangeByRank( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + return LettuceReactiveRedisConnection + . monoConverter().convert(cmd.zremrangebyrank(command.getKey().array(), + command.getRange().getLowerBound(), command.getRange().getUpperBound())) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRemRangeByRank(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRemRangeByScore( + Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Key must not be null!"); + Assert.notNull(command.getRange(), "Range must not be null!"); + + Object lowerBound = AgrumentConverters.lowerBoundArgOf(command.getRange()); + Object upperBound = AgrumentConverters.upperBoundArgOf(command.getRange()); + + Observable result = (lowerBound instanceof String || upperBound instanceof String) + ? cmd.zremrangebyscore(command.getKey().array(), lowerBound.toString(), upperBound.toString()) + : cmd.zremrangebyscore(command.getKey().array(), (Double) lowerBound, (Double) upperBound); + + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zUnionStore(org.reactivestreams.Publisher) + */ + @Override + public Flux> zUnionStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + ZStoreArgs args = null; + if (command.getAggregateFunction().isPresent() || !command.getWeights().isEmpty()) { + args = zStoreArgs(command.getAggregateFunction().isPresent() ? command.getAggregateFunction().get() : null, command.getWeights()); + } + + byte[][] sourceKeys = command.getSourceKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]); + Observable result = args != null ? cmd.zunionstore(command.getKey().array(), args, sourceKeys) + : cmd.zunionstore(command.getKey().array(), sourceKeys); + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zInterStore(org.reactivestreams.Publisher) + */ + @Override + public Flux> zInterStore(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + Assert.notEmpty(command.getSourceKeys(), "Source keys must not be null or empty!"); + + ZStoreArgs args = null; + if (command.getAggregateFunction().isPresent() || !command.getWeights().isEmpty()) { + args = zStoreArgs(command.getAggregateFunction().isPresent() ? command.getAggregateFunction().get() : null, command.getWeights()); + } + + byte[][] sourceKeys = command.getSourceKeys().stream().map(ByteBuffer::array).toArray(size -> new byte[size][]); + Observable result = args != null ? cmd.zinterstore(command.getKey().array(), args, sourceKeys) + : cmd.zinterstore(command.getKey().array(), sourceKeys); + return LettuceReactiveRedisConnection. monoConverter().convert(result) + .map(value -> new NumericResponse<>(command, value)); + })); + } + + /* + * (non-Javadoc) + * @see org.springframework.data.redis.connection.ReactiveZSetCommands#zRangeByLex(org.reactivestreams.Publisher) + */ + @Override + public Flux> zRangeByLex(Publisher commands) { + + return connection.execute(cmd -> Flux.from(commands).flatMap(command -> { + + Assert.notNull(command.getKey(), "Destination key must not be null!"); + + Observable result = Observable.empty(); + + String lowerBound = AgrumentConverters.lowerBoundArgOf(command.getRange()).toString(); + String upperBound = AgrumentConverters.upperBoundArgOf(command.getRange()).toString(); + + if (command.getLimit() != null) { + + if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) { + result = cmd.zrangebylex(command.getKey().array(), lowerBound, upperBound, command.getLimit().getOffset(), + command.getLimit().getCount()); + } else { + + // TODO: fix when https://github.com/mp911de/lettuce/issues/369 resolved + throw new UnsupportedOperationException("Lettuce does not support ZREVRANGEBYLEX."); + } + } else { + if (ObjectUtils.nullSafeEquals(command.getDirection(), Direction.ASC)) { + result = cmd.zrangebylex(command.getKey().array(), lowerBound, upperBound); + } else { + + // TODO: fix when https://github.com/mp911de/lettuce/issues/369 resolved + throw new UnsupportedOperationException("Lettuce does not support ZREVRANGEBYLEX."); + } + } + + return LettuceReactiveRedisConnection.> monoConverter() + .convert(result.map(ByteBuffer::wrap).toList()).map(value -> new MultiValueResponse<>(command, value)); + })); + } + + private ZStoreArgs zStoreArgs(Aggregate aggregate, List weights) { + + ZStoreArgs args = new ZStoreArgs(); + if (aggregate != null) { + switch (aggregate) { + case MIN: + args.min(); + break; + case MAX: + args.max(); + break; + default: + args.sum(); + break; + } + } + + // TODO: fix when https://github.com/mp911de/lettuce/issues/368 resolved + if (weights != null) { + long[] lg = new long[weights.size()]; + for (int i = 0; i < lg.length; i++) { + lg[i] = weights.get(i).longValue(); + } + args.weights(lg); + } + return args; + } + + protected LettuceReactiveRedisConnection getConnection() { + return connection; + } +} diff --git a/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java b/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java index 09278043c..dbf6a9c0b 100644 --- a/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java +++ b/src/test/java/org/springframework/data/redis/cache/RedisCacheTest.java @@ -35,6 +35,7 @@ import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; +import org.hamcrest.core.IsInstanceOf; import org.junit.AfterClass; import org.junit.AssumptionViolatedException; import org.junit.Before; @@ -254,7 +255,7 @@ public class RedisCacheTest extends AbstractNativeCacheTest { cache.put(key, value); RedisCache redisCache = (RedisCache) cache; - assertThat(redisCache.get(key, value.getClass()), instanceOf(value.getClass())); + assertThat(redisCache.get(key, value.getClass()), IsInstanceOf.instanceOf(value.getClass())); } /** diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java index 8e515e9a2..cb4fae112 100644 --- a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithCommitUnitTests.java @@ -55,7 +55,7 @@ import org.springframework.transaction.annotation.Transactional; @Transactional(transactionManager = "transactionManager") public class TransactionalRedisCacheManagerWithCommitUnitTests { - @SuppressWarnings("rawtypes")// + @SuppressWarnings("rawtypes") // protected @Autowired RedisTemplate redisTemplate; protected @Autowired FooService transactionalService; diff --git a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java index 1615a5b5d..3f690c13d 100644 --- a/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java +++ b/src/test/java/org/springframework/data/redis/cache/TransactionalRedisCacheManagerWithRollbackUnitTests.java @@ -51,7 +51,7 @@ import org.springframework.transaction.annotation.Transactional; @Transactional(transactionManager = "transactionManager") public class TransactionalRedisCacheManagerWithRollbackUnitTests { - @SuppressWarnings("rawtypes")// + @SuppressWarnings("rawtypes") // protected @Autowired RedisTemplate redisTemplate; protected @Autowired FooService transactionalService; diff --git a/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java b/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java index 332f3489e..e98099f54 100644 --- a/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java +++ b/src/test/java/org/springframework/data/redis/connection/AbstractTransactionalTestBase.java @@ -105,8 +105,9 @@ public abstract class AbstractTransactionalTestBase { RedisConnection connection = factory.getConnection(); for (String key : KEYS) { - Assert.assertThat("Values for " + key + " should " + (valuesShouldHaveBeenPersisted ? "" : "NOT ") - + "have been found.", connection.exists(key.getBytes()), Is.is(valuesShouldHaveBeenPersisted)); + Assert.assertThat( + "Values for " + key + " should " + (valuesShouldHaveBeenPersisted ? "" : "NOT ") + "have been found.", + connection.exists(key.getBytes()), Is.is(valuesShouldHaveBeenPersisted)); } connection.close(); } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java index e2bd7924f..f47d146bf 100644 --- a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryTests.java @@ -32,6 +32,7 @@ import org.springframework.data.redis.SettingsUtils; import org.springframework.data.redis.connection.DefaultStringRedisConnection; import org.springframework.data.redis.connection.RedisConnection; import org.springframework.data.redis.connection.StringRedisConnection; +import org.springframework.data.repository.util.QueryExecutionConverters; import com.lambdaworks.redis.RedisException; import com.lambdaworks.redis.api.async.RedisAsyncCommands; @@ -325,4 +326,20 @@ public class LettuceConnectionFactoryTests { connection.close(); } } + + /** + * @see DATAREDIS-525 + */ + @Test + public void factoryShouldReturnReactiveConnectionWhenCorrectly() { + + LettuceConnectionFactory factory = new LettuceConnectionFactory(); + factory.afterPropertiesSet(); + + ConnectionFactoryTracker.add(factory); + + assertThat(factory.getReactiveConnection() + .execute(cmd -> QueryExecutionConverters.RxJava1ObservableToMonoConverter.INSTANCE.convert(cmd.ping())) + .blockFirst(), is("PONG")); + } } diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsTestsBase.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsTestsBase.java new file mode 100644 index 000000000..b1be185d6 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterCommandsTestsBase.java @@ -0,0 +1,58 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assume.assumeThat; + +import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; +import org.junit.After; +import org.junit.Before; +import org.junit.ClassRule; +import org.springframework.data.redis.test.util.LettuceRedisClusterClientProvider; + +/** + * @author Christoph Strobl + */ +public abstract class LettuceReactiveClusterCommandsTestsBase { + + public static @ClassRule LettuceRedisClusterClientProvider clientProvider = LettuceRedisClusterClientProvider.local(); + + RedisClusterCommands nativeCommands; + LettuceReactiveRedisClusterConnection connection; + + @Before + public void before() { + assumeThat(clientProvider.test(), is(true)); + nativeCommands = clientProvider.getClient().connect().sync(); + connection = new LettuceReactiveRedisClusterConnection(clientProvider.getClient()); + } + + @After + public void tearDown() { + + if(nativeCommands != null) { + nativeCommands.flushall(); + nativeCommands.close(); + } + + if(connection != null) { + connection.close(); + } + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsTests.java new file mode 100644 index 000000000..be5228c89 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterHyperLogLogCommandsTests.java @@ -0,0 +1,78 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.lettuce.LettuceReactiveCommandsTestsBase.*; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveClusterHyperLogLogCommandsTests extends LettuceReactiveClusterCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfCountWithMultipleKeysShouldReturnCorrectlyWhenKeysMapToSameSlot() { + + nativeCommands.pfadd(SAME_SLOT_KEY_1, new String[] { VALUE_1, VALUE_2 }); + nativeCommands.pfadd(SAME_SLOT_KEY_2, new String[] { VALUE_2, VALUE_3 }); + + assertThat(connection.hyperLogLogCommands().pfCount(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER)) + .block(), is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfMergeShouldWorkCorrectlyWhenKeysMapToSameSlot() { + + nativeCommands.pfadd(SAME_SLOT_KEY_1, new String[] { VALUE_1, VALUE_2 }); + nativeCommands.pfadd(SAME_SLOT_KEY_2, new String[] { VALUE_2, VALUE_3 }); + + assertThat( + connection.hyperLogLogCommands() + .pfMerge(SAME_SLOT_KEY_3_BBUFFER, Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER)).block(), + is(true)); + + assertThat(nativeCommands.pfcount(new String[] { SAME_SLOT_KEY_3 }), is(3L)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsTests.java new file mode 100644 index 000000000..022ed81b5 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterKeyCommandsTests.java @@ -0,0 +1,72 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.RedisClusterNode.*; +import static org.springframework.data.redis.connection.lettuce.LettuceReactiveCommandsTestsBase.*; + +import java.nio.ByteBuffer; +import java.util.List; + +import org.junit.Test; +import org.springframework.data.redis.connection.RedisClusterNode; + +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveClusterKeyCommandsTests extends LettuceReactiveClusterCommandsTestsBase { + + static final RedisClusterNode NODE_1 = newRedisClusterNode().listeningAt("127.0.0.1", 7379).build(); + + /** + * @see DATAREDIS-525 + */ + @Test + public void keysShouldReturnOnlyKeysFromSelectedNode() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + List result = connection.keyCommands().keys(NODE_1, ByteBuffer.wrap("*".getBytes())).block(); + assertThat(result, hasSize(1)); + assertThat(result, contains(KEY_1_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void randomkeyShouldReturnOnlyKeysFromSelectedNode() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Mono randomkey = connection.keyCommands().randomKey(NODE_1); + + for (int i = 0; i < 10; i++) { + assertThat(randomkey.block(), is(equalTo(KEY_1_BBUFFER))); + } + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsTests.java new file mode 100644 index 000000000..67dfae8ac --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterListCommandsTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.lettuce.LettuceReactiveCommandsTestsBase.*; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.data.redis.connection.ReactiveListCommands; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveClusterListCommandsTests extends LettuceReactiveClusterCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void bRPopLPushShouldWorkCorrectlyWhenAllKeysMapToSameSlot() { + + nativeCommands.rpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.rpush(SAME_SLOT_KEY_2, VALUE_1); + + ByteBuffer result = connection.listCommands() + .bRPopLPush(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER, Duration.ofSeconds(1)).block(); + + assertThat(result, is(equalTo(VALUE_3_BBUFFER))); + assertThat(nativeCommands.llen(SAME_SLOT_KEY_2), is(2L)); + assertThat(nativeCommands.lindex(SAME_SLOT_KEY_2, 0), is(equalTo(VALUE_3))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void blPopShouldReturnFirstAvailableWhenAllKeysMapToTheSameSlot() { + + nativeCommands.rpush(SAME_SLOT_KEY_1, VALUE_1, VALUE_2, VALUE_3); + + ReactiveListCommands.PopResult result = connection.listCommands() + .blPop(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), Duration.ofSeconds(1L)).block(); + assertThat(result.getKey(), is(equalTo(SAME_SLOT_KEY_1_BBUFFER))); + assertThat(result.getValue(), is(equalTo(VALUE_1_BBUFFER))); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsTests.java new file mode 100644 index 000000000..0abb53587 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterStringCommandsTests.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.lettuce.LettuceReactiveCommandsTestsBase.*; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; +import org.springframework.data.redis.connection.RedisStringCommands; + +/** + * @author Christoph Strobl + * @since 2.0 + */ +public class LettuceReactiveClusterStringCommandsTests extends LettuceReactiveClusterCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void mSetNXShouldAddMultipleKeyValueParisWhenMappedToSameSlot() { + + Map map = new LinkedHashMap<>(); + map.put(SAME_SLOT_KEY_1_BBUFFER, VALUE_1_BBUFFER); + map.put(SAME_SLOT_KEY_2_BBUFFER, VALUE_2_BBUFFER); + + connection.stringCommands().mSetNX(map).block(); + + assertThat(nativeCommands.get(SAME_SLOT_KEY_1), is(equalTo(VALUE_1))); + assertThat(nativeCommands.get(SAME_SLOT_KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mSetNXShouldNotAddMultipleKeyValueParisWhenAlreadyExitAndMapToSameSlot() { + + nativeCommands.set(SAME_SLOT_KEY_2, VALUE_2); + + Map map = new LinkedHashMap<>(); + map.put(SAME_SLOT_KEY_1_BBUFFER, VALUE_1_BBUFFER); + map.put(SAME_SLOT_KEY_2_BBUFFER, VALUE_2_BBUFFER); + + assertThat(connection.stringCommands().mSetNX(map).block(), is(false)); + + assertThat(nativeCommands.exists(SAME_SLOT_KEY_1), is(false)); + assertThat(nativeCommands.get(SAME_SLOT_KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitOpAndShouldWorkAsExpectedWhenKeysMapToSameSlot() { + + nativeCommands.set(SAME_SLOT_KEY_1, VALUE_1); + nativeCommands.set(SAME_SLOT_KEY_2, VALUE_2); + + assertThat(connection.stringCommands().bitOp(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), + RedisStringCommands.BitOperation.AND, SAME_SLOT_KEY_3_BBUFFER).block(), is(7L)); + assertThat(nativeCommands.get(SAME_SLOT_KEY_3), is(equalTo("value-0"))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitOpOrShouldWorkAsExpectedWhenKeysMapToSameSlot() { + + nativeCommands.set(SAME_SLOT_KEY_1, VALUE_1); + nativeCommands.set(SAME_SLOT_KEY_2, VALUE_2); + + assertThat(connection.stringCommands().bitOp(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), + RedisStringCommands.BitOperation.OR, SAME_SLOT_KEY_3_BBUFFER).block(), is(7L)); + assertThat(nativeCommands.get(SAME_SLOT_KEY_3), is(equalTo(VALUE_3))); + } + + /** + * @see DATAREDIS-525 + */ + @Test(expected = IllegalArgumentException.class) + public void bitNotShouldThrowExceptionWhenMoreThanOnSourceKeyAndKeysMapToSameSlot() { + + connection.stringCommands().bitOp(Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), + RedisStringCommands.BitOperation.NOT, SAME_SLOT_KEY_3_BBUFFER).block(); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsTests.java new file mode 100644 index 000000000..1f22d5aae --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveClusterZSetCommandsTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.lettuce.LettuceReactiveCommandsTestsBase.*; + +import java.util.Arrays; + +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveClusterZSetCommandsTests extends LettuceReactiveClusterCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void zUnionStoreShouldWorkWhenAllKeysMapToSameSlot() { + + nativeCommands.zadd(SAME_SLOT_KEY_1, 1D, VALUE_1); + nativeCommands.zadd(SAME_SLOT_KEY_1, 2D, VALUE_2); + nativeCommands.zadd(SAME_SLOT_KEY_2, 1D, VALUE_1); + nativeCommands.zadd(SAME_SLOT_KEY_2, 2D, VALUE_2); + nativeCommands.zadd(SAME_SLOT_KEY_2, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zUnionStore(SAME_SLOT_KEY_3_BBUFFER, + Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), Arrays.asList(2D, 3D)).block(), is(3L)); + + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zInterStoreShouldWorkCorrectlyWhenKeysMapToSameSlot() { + + nativeCommands.zadd(SAME_SLOT_KEY_1, 1D, VALUE_1); + nativeCommands.zadd(SAME_SLOT_KEY_1, 2D, VALUE_2); + nativeCommands.zadd(SAME_SLOT_KEY_2, 1D, VALUE_1); + nativeCommands.zadd(SAME_SLOT_KEY_2, 2D, VALUE_2); + nativeCommands.zadd(SAME_SLOT_KEY_2, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zInterStore(SAME_SLOT_KEY_3_BBUFFER, + Arrays.asList(SAME_SLOT_KEY_1_BBUFFER, SAME_SLOT_KEY_2_BBUFFER), Arrays.asList(2D, 3D)).block(), is(2L)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java new file mode 100644 index 000000000..8e1623a2b --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveCommandsTestsBase.java @@ -0,0 +1,129 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.is; +import static org.junit.Assume.assumeThat; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; + +import com.lambdaworks.redis.AbstractRedisClient; +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; +import org.hamcrest.core.Is; +import org.junit.After; +import org.junit.Assume; +import org.junit.Before; +import org.junit.ClassRule; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.springframework.data.redis.test.util.LettuceRedisClientProvider; + +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.api.sync.RedisCommands; +import org.springframework.data.redis.test.util.LettuceRedisClusterClientProvider; + +/** + * @author Christoph Strobl + */ +@RunWith(Parameterized.class) +public class LettuceReactiveCommandsTestsBase { + + static final String KEY_1 = "key-1"; + static final String KEY_2 = "key-2"; + static final String KEY_3 = "key-3"; + static final String SAME_SLOT_KEY_1 = "{key}-1"; + static final String SAME_SLOT_KEY_2 = "{key}-2"; + static final String SAME_SLOT_KEY_3 = "{key}-3"; + static final String VALUE_1 = "value-1"; + static final String VALUE_2 = "value-2"; + static final String VALUE_3 = "value-3"; + + static final byte[] SAME_SLOT_KEY_1_BYTES = SAME_SLOT_KEY_1.getBytes(Charset.forName("UTF-8")); + static final byte[] SAME_SLOT_KEY_2_BYTES = SAME_SLOT_KEY_2.getBytes(Charset.forName("UTF-8")); + static final byte[] SAME_SLOT_KEY_3_BYTES = SAME_SLOT_KEY_3.getBytes(Charset.forName("UTF-8")); + static final byte[] KEY_1_BYTES = KEY_1.getBytes(Charset.forName("UTF-8")); + static final byte[] KEY_2_BYTES = KEY_2.getBytes(Charset.forName("UTF-8")); + static final byte[] KEY_3_BYTES = KEY_3.getBytes(Charset.forName("UTF-8")); + static final byte[] VALUE_1_BYTES = VALUE_1.getBytes(Charset.forName("UTF-8")); + static final byte[] VALUE_2_BYTES = VALUE_2.getBytes(Charset.forName("UTF-8")); + static final byte[] VALUE_3_BYTES = VALUE_3.getBytes(Charset.forName("UTF-8")); + + static final ByteBuffer KEY_1_BBUFFER = ByteBuffer.wrap(KEY_1_BYTES); + static final ByteBuffer SAME_SLOT_KEY_1_BBUFFER = ByteBuffer.wrap(SAME_SLOT_KEY_1_BYTES); + static final ByteBuffer VALUE_1_BBUFFER = ByteBuffer.wrap(VALUE_1_BYTES); + + static final ByteBuffer KEY_2_BBUFFER = ByteBuffer.wrap(KEY_2_BYTES); + static final ByteBuffer SAME_SLOT_KEY_2_BBUFFER = ByteBuffer.wrap(SAME_SLOT_KEY_2_BYTES); + static final ByteBuffer VALUE_2_BBUFFER = ByteBuffer.wrap(VALUE_2_BYTES); + + static final ByteBuffer KEY_3_BBUFFER = ByteBuffer.wrap(KEY_3_BYTES); + static final ByteBuffer SAME_SLOT_KEY_3_BBUFFER = ByteBuffer.wrap(SAME_SLOT_KEY_3_BYTES); + static final ByteBuffer VALUE_3_BBUFFER = ByteBuffer.wrap(VALUE_3_BYTES); + + @Parameterized.Parameter(value = 0) public Object clientProvider; + + LettuceReactiveRedisConnection connection; + RedisClusterCommands nativeCommands; + + @Parameterized.Parameters + public static List parameters() { + return Arrays.asList(LettuceRedisClientProvider.local(), LettuceRedisClusterClientProvider.local()); + } + + @Before + public void setUp() { + + AbstractRedisClient abstractRedisClient = null; + if (clientProvider instanceof LettuceRedisClientProvider) { + abstractRedisClient = ((LettuceRedisClientProvider) clientProvider).getClient(); + } else if (clientProvider instanceof LettuceRedisClusterClientProvider) { + abstractRedisClient = ((LettuceRedisClusterClientProvider) clientProvider).getClient(); + assumeThat(((LettuceRedisClusterClientProvider) clientProvider).test(), is(true)); + } + + if (abstractRedisClient instanceof RedisClient) { + nativeCommands = ((RedisClient) abstractRedisClient).connect().sync(); + connection = new LettuceReactiveRedisConnection(abstractRedisClient); + + } else if (abstractRedisClient instanceof RedisClusterClient) { + nativeCommands = ((RedisClusterClient) abstractRedisClient).connect().sync(); + connection = new LettuceReactiveRedisClusterConnection((RedisClusterClient) abstractRedisClient); + } + + } + + @After + public void tearDown() { + + if (nativeCommands != null) { + flushAll(); + nativeCommands.close(); + } + + if (connection != null) { + connection.close(); + } + } + + private void flushAll() { + nativeCommands.flushall(); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsTests.java new file mode 100644 index 000000000..baa97eeaf --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveGeoCommandsTests.java @@ -0,0 +1,275 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsNull.*; +import static org.hamcrest.number.IsCloseTo.*; +import static org.junit.Assert.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.DistanceUnit.*; +import static org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs.*; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; +import org.springframework.data.geo.Circle; +import org.springframework.data.geo.Distance; +import org.springframework.data.geo.GeoResults; +import org.springframework.data.geo.Metrics; +import org.springframework.data.geo.Point; +import org.springframework.data.redis.connection.RedisGeoCommands.GeoLocation; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveGeoCommandsTests extends LettuceReactiveCommandsTestsBase { + + private static final String ARIGENTO_MEMBER_NAME = "arigento"; + private static final String CATANIA_MEMBER_NAME = "catania"; + private static final String PALERMO_MEMBER_NAME = "palermo"; + + 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 GeoLocation ARIGENTO = new GeoLocation<>( + ByteBuffer.wrap(ARIGENTO_MEMBER_NAME.getBytes(Charset.forName("UTF-8"))), POINT_ARIGENTO); + private static final GeoLocation CATANIA = new GeoLocation<>( + ByteBuffer.wrap(CATANIA_MEMBER_NAME.getBytes(Charset.forName("UTF-8"))), POINT_CATANIA); + private static final GeoLocation PALERMO = new GeoLocation<>( + ByteBuffer.wrap(PALERMO_MEMBER_NAME.getBytes(Charset.forName("UTF-8"))), POINT_PALERMO); + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoAddShouldAddSingleGeoLocationCorrectly() { + assertThat(connection.geoCommands().geoAdd(KEY_1_BBUFFER, ARIGENTO).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoAddShouldAddMultipleGeoLocationsCorrectly() { + assertThat(connection.geoCommands().geoAdd(KEY_1_BBUFFER, Arrays.asList(ARIGENTO, CATANIA, PALERMO)).block(), + is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoDistShouldReturnDistanceInMetersByDefault() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + assertThat(connection.geoCommands().geoDist(KEY_1_BBUFFER, PALERMO.getName(), CATANIA.getName()).block().getValue(), + is(closeTo(166274.15156960033D, 0.005))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoDistShouldReturnDistanceInDesiredMetric() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + assertThat(connection.geoCommands().geoDist(KEY_1_BBUFFER, PALERMO.getName(), CATANIA.getName(), Metrics.KILOMETERS) + .block().getValue(), is(closeTo(166.27415156960033D, 0.005))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoHash() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + assertThat( + connection.geoCommands().geoHash(KEY_1_BBUFFER, Arrays.asList(PALERMO.getName(), CATANIA.getName())).block(), + contains("sqc8b49rny0", "sqdtr74hyu0")); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoHashNotExisting() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + assertThat( + connection.geoCommands() + .geoHash(KEY_1_BBUFFER, Arrays.asList(PALERMO.getName(), ARIGENTO.getName(), CATANIA.getName())).block(), + contains("sqc8b49rny0", null, "sqdtr74hyu0")); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoPos() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + List result = connection.geoCommands() + .geoPos(KEY_1_BBUFFER, Arrays.asList(PALERMO.getName(), CATANIA.getName())).block(); + assertThat(result.get(0).getX(), is(closeTo(POINT_PALERMO.getX(), 0.005))); + assertThat(result.get(0).getY(), is(closeTo(POINT_PALERMO.getY(), 0.005))); + + assertThat(result.get(1).getX(), is(closeTo(POINT_CATANIA.getX(), 0.005))); + assertThat(result.get(1).getY(), is(closeTo(POINT_CATANIA.getY(), 0.005))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoPosNonExisting() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + + List result = connection.geoCommands() + .geoPos(KEY_1_BBUFFER, Arrays.asList(PALERMO.getName(), ARIGENTO.getName(), CATANIA.getName())).block(); + assertThat(result.get(0).getX(), is(closeTo(POINT_PALERMO.getX(), 0.005))); + assertThat(result.get(0).getY(), is(closeTo(POINT_PALERMO.getY(), 0.005))); + + assertThat(result.get(1), is(nullValue())); + + assertThat(result.get(2).getX(), is(closeTo(POINT_CATANIA.getX(), 0.005))); + assertThat(result.get(2).getY(), is(closeTo(POINT_CATANIA.getY(), 0.005))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusShouldReturnMembersCorrectly() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + assertThat( + connection.geoCommands() + .geoRadius(KEY_1_BBUFFER, new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS))).block(), + hasSize(3)); + assertThat( + connection.geoCommands() + .geoRadius(KEY_1_BBUFFER, new Circle(new Point(15D, 37D), new Distance(150D, KILOMETERS))).block(), + hasSize(2)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusShouldReturnDistanceCorrectly() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + GeoResults> result = connection.geoCommands().geoRadius(KEY_1_BBUFFER, + new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS)), newGeoRadiusArgs().includeDistance()).block(); + + assertThat(result.getContent().get(0).getDistance().getValue(), is(closeTo(130.423D, 0.005))); + assertThat(result.getContent().get(0).getDistance().getUnit(), is("km")); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusShouldApplyLimit() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + GeoResults> result = connection.geoCommands().geoRadius(KEY_1_BBUFFER, + new Circle(new Point(15D, 37D), new Distance(200D, KILOMETERS)), newGeoRadiusArgs().limit(2)).block(); + + assertThat(result.getContent(), hasSize(2)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusByMemberShouldReturnMembersCorrectly() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + List> result = connection.geoCommands() + .geoRadiusByMember(KEY_1_BBUFFER, ARIGENTO.getName(), new Distance(100, KILOMETERS)).block(); + + assertThat(result.get(0).getName(), is(ARIGENTO.getName())); + assertThat(result.get(1).getName(), is(PALERMO.getName())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusByMemberShouldReturnDistanceCorrectly() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + GeoResults> result = connection.geoCommands().geoRadiusByMember(KEY_1_BBUFFER, + PALERMO.getName(), new Distance(100, KILOMETERS), newGeoRadiusArgs().includeDistance()).block(); + + assertThat(result.getContent(), hasSize(2)); + assertThat(result.getContent().get(0).getDistance().getValue(), is(closeTo(90.978D, 0.005))); + assertThat(result.getContent().get(0).getDistance().getUnit(), is("km")); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void geoRadiusByMemberShouldApplyLimit() { + + nativeCommands.geoadd(KEY_1, PALERMO.getPoint().getX(), PALERMO.getPoint().getY(), PALERMO_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, CATANIA.getPoint().getX(), CATANIA.getPoint().getY(), CATANIA_MEMBER_NAME); + nativeCommands.geoadd(KEY_1, ARIGENTO.getPoint().getX(), ARIGENTO.getPoint().getY(), ARIGENTO_MEMBER_NAME); + + GeoResults> result = connection.geoCommands() + .geoRadiusByMember(KEY_1_BBUFFER, PALERMO.getName(), new Distance(200, KILOMETERS), newGeoRadiusArgs().limit(2)) + .block(); + + assertThat(result.getContent(), hasSize(2)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java new file mode 100644 index 000000000..c683f99ad --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHashCommandsTests.java @@ -0,0 +1,244 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveHashCommandsTests extends LettuceReactiveCommandsTestsBase { + + static final String FIELD_1 = "field-1"; + static final String FIELD_2 = "field-2"; + static final String FIELD_3 = "field-3"; + + static final byte[] FIELD_1_BYTES = FIELD_1.getBytes(Charset.forName("UTF-8")); + static final byte[] FIELD_2_BYTES = FIELD_2.getBytes(Charset.forName("UTF-8")); + static final byte[] FIELD_3_BYTES = FIELD_3.getBytes(Charset.forName("UTF-8")); + + static final ByteBuffer FIELD_1_BBUFFER = ByteBuffer.wrap(FIELD_1_BYTES); + static final ByteBuffer FIELD_2_BBUFFER = ByteBuffer.wrap(FIELD_2_BYTES); + static final ByteBuffer FIELD_3_BBUFFER = ByteBuffer.wrap(FIELD_3_BYTES); + + /** + * @see DATAREDIS-525 + */ + @Test + public void hSetShouldOperateCorrectly() { + assertThat(connection.hashCommands().hSet(KEY_1_BBUFFER, FIELD_1_BBUFFER, VALUE_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hSetNxShouldOperateCorrectly() { + assertThat(connection.hashCommands().hSetNX(KEY_1_BBUFFER, FIELD_1_BBUFFER, VALUE_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hSetNxShouldReturnFalseIfFieldAlreadyExists() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + + assertThat(connection.hashCommands().hSetNX(KEY_1_BBUFFER, FIELD_1_BBUFFER, VALUE_1_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hGetShouldReturnValueForExistingField() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hGet(KEY_1_BBUFFER, FIELD_1_BBUFFER).block(), is(equalTo(VALUE_1_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hGetShouldReturnNullForNotExistingField() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + + assertThat(connection.hashCommands().hGet(KEY_1_BBUFFER, FIELD_2_BBUFFER).block(), is(nullValue())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hMGetShouldReturnValueForFields() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hMGet(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_3_BBUFFER)).block(), + contains(VALUE_1_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hMGetShouldReturnNullValueForFieldsThatHaveNoValue() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands() + .hMGet(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER)).block(), + contains(VALUE_1_BBUFFER, null, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hMSetSouldSetValuesCorrectly() { + + Map fieldValues = new LinkedHashMap<>(); + fieldValues.put(FIELD_1_BBUFFER, VALUE_1_BBUFFER); + fieldValues.put(FIELD_2_BBUFFER, VALUE_2_BBUFFER); + + assertThat(connection.hashCommands().hMSet(KEY_1_BBUFFER, fieldValues).block(), is(true)); + assertThat(nativeCommands.hget(KEY_1, FIELD_1), is(equalTo(VALUE_1))); + assertThat(nativeCommands.hget(KEY_1, FIELD_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hExistsShouldReturnTrueForExistingField() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + + assertThat(connection.hashCommands().hExists(KEY_1_BBUFFER, FIELD_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hExistsShouldReturnFalseForNonExistingField() { + assertThat(connection.hashCommands().hExists(KEY_1_BBUFFER, FIELD_1_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hDelShouldRemoveSingleFieldsCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hDel(KEY_1_BBUFFER, FIELD_2_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hDelShouldRemoveMultipleFieldsCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hDel(KEY_1_BBUFFER, Arrays.asList(FIELD_1_BBUFFER, FIELD_3_BBUFFER)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hLenShouldReturnSizeCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hLen(KEY_1_BBUFFER).block(), is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hKeysShouldReturnFieldsCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hKeys(KEY_1_BBUFFER).block(), + containsInAnyOrder(FIELD_1_BBUFFER, FIELD_2_BBUFFER, FIELD_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hValsShouldReturnValuesCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + assertThat(connection.hashCommands().hVals(KEY_1_BBUFFER).block(), + containsInAnyOrder(VALUE_1_BBUFFER, VALUE_2_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hGetAlllShouldReturnEntriesCorrectly() { + + nativeCommands.hset(KEY_1, FIELD_1, VALUE_1); + nativeCommands.hset(KEY_1, FIELD_2, VALUE_2); + nativeCommands.hset(KEY_1, FIELD_3, VALUE_3); + + System.out.println(connection.hashCommands().hGetAll(KEY_1_BBUFFER).block()); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java new file mode 100644 index 000000000..53abc8662 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveHyperLogLogCommandsTests.java @@ -0,0 +1,97 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; + +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.data.redis.test.util.LettuceRedisClientProvider; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveHyperLogLogCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfAddShouldAddToNonExistingKeyCorrectly() { + + assertThat(connection.hyperLogLogCommands() + .pfAdd(KEY_1_BBUFFER, Arrays.asList(VALUE_1_BBUFFER, VALUE_2_BBUFFER, VALUE_3_BBUFFER)).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfAddShouldReturnZeroWhenValueAlreadyExists() { + + nativeCommands.pfadd(KEY_1, new String[] { VALUE_1, VALUE_2 }); + nativeCommands.pfadd(KEY_1, new String[] { VALUE_3 }); + + assertThat(connection.hyperLogLogCommands().pfAdd(KEY_1_BBUFFER, Arrays.asList(VALUE_1_BBUFFER)).block(), is(0L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfCountShouldReturnCorrectly() { + + nativeCommands.pfadd(KEY_1, new String[] { VALUE_1, VALUE_2 }); + + assertThat(connection.hyperLogLogCommands().pfCount(KEY_1_BBUFFER).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfCountWithMultipleKeysShouldReturnCorrectly() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.pfadd(KEY_1, new String[] { VALUE_1, VALUE_2 }); + nativeCommands.pfadd(KEY_2, new String[] { VALUE_2, VALUE_3 }); + + assertThat(connection.hyperLogLogCommands().pfCount(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(), is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pfMergeShouldWorkCorrectly() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.pfadd(KEY_1, new String[] { VALUE_1, VALUE_2 }); + nativeCommands.pfadd(KEY_2, new String[] { VALUE_2, VALUE_3 }); + + assertThat( + connection.hyperLogLogCommands().pfMerge(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(), + is(true)); + + assertThat(nativeCommands.pfcount(new String[] { KEY_3 }), is(3L)); + } + +} 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 new file mode 100644 index 000000000..c22ef1e81 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveKeyCommandsTests.java @@ -0,0 +1,226 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.collection.IsCollectionWithSize.*; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.junit.Test; +import org.springframework.data.redis.RedisSystemException; +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; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveKeyCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void existsShouldReturnTrueForExistingKeys() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.keyCommands().exists(KEY_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void existsShouldReturnFalseForNonExistingKeys() { + assertThat(connection.keyCommands().exists(KEY_1_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void typeShouldReturnTypeCorrectly() { + + nativeCommands.set(KEY_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2); + nativeCommands.hset(KEY_3, KEY_1, VALUE_1); + + assertThat(connection.keyCommands().type(KEY_1_BBUFFER).block(), is(DataType.STRING)); + assertThat(connection.keyCommands().type(KEY_2_BBUFFER).block(), is(DataType.SET)); + assertThat(connection.keyCommands().type(KEY_3_BBUFFER).block(), is(DataType.HASH)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void keysShouldReturnCorrectly() { + + nativeCommands.set(KEY_1, VALUE_2); + nativeCommands.set(KEY_2, VALUE_2); + nativeCommands.set(KEY_3, VALUE_3); + + nativeCommands.set(VALUE_1, KEY_1); + nativeCommands.set(VALUE_2, KEY_2); + nativeCommands.set(VALUE_3, KEY_3); + + assertThat(connection.keyCommands().keys(ByteBuffer.wrap("*".getBytes())).block(), hasSize(6)); + assertThat(connection.keyCommands().keys(ByteBuffer.wrap("key*".getBytes())).block(), hasSize(3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void randomKeyShouldReturnAnyKey() { + + nativeCommands.set(KEY_1, VALUE_2); + nativeCommands.set(KEY_2, VALUE_2); + nativeCommands.set(KEY_3, VALUE_3); + + assertThat(connection.keyCommands().randomKey().block(), is(notNullValue())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void randomKeyShouldReturnNullWhenNoKeyExists() { + assertThat(connection.keyCommands().randomKey().block(), is(nullValue())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void renameShouldAlterKeyNameCorrectly() { + + nativeCommands.set(KEY_1, VALUE_2); + + assertThat(connection.keyCommands().rename(KEY_1_BBUFFER, KEY_2_BBUFFER).block(), is(true)); + assertThat(nativeCommands.exists(KEY_2), is(true)); + assertThat(nativeCommands.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test(expected = RedisSystemException.class) + public void renameShouldThrowErrorWhenKeyDoesNotExit() { + assertThat(connection.keyCommands().rename(KEY_1_BBUFFER, KEY_2_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void renameNXShouldAlterKeyNameCorrectly() { + + nativeCommands.set(KEY_1, VALUE_2); + + assertThat(connection.keyCommands().rename(KEY_1_BBUFFER, KEY_2_BBUFFER).block(), is(true)); + + assertThat(nativeCommands.exists(KEY_2), is(true)); + assertThat(nativeCommands.exists(KEY_1), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void renameNXShouldNotAlterExistingKeyName() { + + nativeCommands.set(KEY_1, VALUE_2); + nativeCommands.set(KEY_2, VALUE_2); + + assertThat(connection.keyCommands().renameNX(KEY_1_BBUFFER, KEY_2_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void shouldDeleteKeyCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + Mono result = connection.keyCommands().del(KEY_1_BBUFFER); + assertThat(result.block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void shouldDeleteKeysCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Flux> result = connection.keyCommands() + .del(Flux.fromIterable(Arrays.asList(new KeyCommand(KEY_1_BBUFFER), new KeyCommand(KEY_2_BBUFFER)))); + + TestSubscriber> subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(2); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void shouldDeleteKeysInBatchCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Mono result = connection.keyCommands().mDel(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)); + + assertThat(result.block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void shouldDeleteKeysInMultipleBatchesCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Flux result = connection.keyCommands() + .mDel( + Flux.fromIterable(Arrays.asList(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Arrays.asList(KEY_1_BBUFFER)))) + .map(NumericResponse::getOutput); + + TestSubscriber subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(2); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java new file mode 100644 index 000000000..cb2ca4ec8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveListCommandTests.java @@ -0,0 +1,320 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNot.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; + +import java.nio.ByteBuffer; +import java.time.Duration; +import java.util.Arrays; + +import org.junit.Assume; +import org.junit.Test; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.data.redis.connection.ReactiveListCommands.PopResult; +import org.springframework.data.redis.connection.ReactiveListCommands.PushCommand; +import org.springframework.data.redis.connection.RedisListCommands.Position; + +import org.springframework.data.redis.test.util.LettuceRedisClientProvider; +import reactor.core.publisher.Mono; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveListCommandTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void rPushShouldAppendValuesCorrectly() { + + nativeCommands.lpush(KEY_1, VALUE_1); + + assertThat(connection.listCommands().rPush(KEY_1_BBUFFER, Arrays.asList(VALUE_2_BBUFFER, VALUE_3_BBUFFER)).block(), + is(3L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lPushShouldPrependValuesCorrectly() { + + nativeCommands.lpush(KEY_1, VALUE_1); + + assertThat(connection.listCommands().lPush(KEY_1_BBUFFER, Arrays.asList(VALUE_2_BBUFFER, VALUE_3_BBUFFER)).block(), + is(3L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_3, VALUE_2, VALUE_1)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void rPushXShouldAppendValuesCorrectly() { + + nativeCommands.lpush(KEY_1, VALUE_1); + + assertThat(connection.listCommands().rPushX(KEY_1_BBUFFER, VALUE_2_BBUFFER).block(), is(2L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lPushXShouldPrependValuesCorrectly() { + + nativeCommands.lpush(KEY_1, VALUE_1); + + assertThat(connection.listCommands().lPushX(KEY_1_BBUFFER, VALUE_2_BBUFFER).block(), is(2L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_2, VALUE_1)); + } + + /** + * @see DATAREDIS-525 + */ + @Test(expected = InvalidDataAccessApiUsageException.class) + public void pushShouldThrowErrorForMoreThanOneValueWhenUsingExistsOption() { + + connection.listCommands() + .push(Mono.just( + PushCommand.right().values(Arrays.asList(VALUE_1_BBUFFER, VALUE_2_BBUFFER)).to(KEY_1_BBUFFER).ifExists())) + .blockFirst(); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lLenShouldReturnSizeCorrectly() { + + nativeCommands.lpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(connection.listCommands().lLen(KEY_1_BBUFFER).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lRangeShouldReturnValuesCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.listCommands().lRange(KEY_1_BBUFFER, 1, 2).block(), + contains(VALUE_2_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lTrimShouldReturnValuesCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.listCommands().lTrim(KEY_1_BBUFFER, 1, 2).block(), is(true)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), not(contains(VALUE_1_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lIndexShouldReturnValueCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.listCommands().lIndex(KEY_1_BBUFFER, 1).block(), is(equalTo(VALUE_2_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lInsertShouldAddValueCorrectlyBeforeExisting() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat( + connection.listCommands().lInsert(KEY_1_BBUFFER, Position.BEFORE, VALUE_2_BBUFFER, VALUE_3_BBUFFER).block(), + is(3L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_3, VALUE_2)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lInsertShouldAddValueCorrectlyAfterExisting() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat( + connection.listCommands().lInsert(KEY_1_BBUFFER, Position.AFTER, VALUE_2_BBUFFER, VALUE_3_BBUFFER).block(), + is(3L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lSetSouldSetValueCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2); + + assertThat(connection.listCommands().lSet(KEY_1_BBUFFER, 1L, VALUE_3_BBUFFER).block(), is(true)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_3)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), not(contains(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lRemSouldRemoveAllValuesCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_1, VALUE_3); + + assertThat(connection.listCommands().lRem(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(2L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_2, VALUE_3)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), not(contains(VALUE_1))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lRemSouldRemoveFirstValuesCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_1, VALUE_3); + + assertThat(connection.listCommands().lRem(KEY_1_BBUFFER, 1L, VALUE_1_BBUFFER).block(), is(1L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_2, VALUE_1, VALUE_3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lRemSouldRemoveLastValuesCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_1, VALUE_3); + + assertThat(connection.listCommands().lRem(KEY_1_BBUFFER, -1L, VALUE_1_BBUFFER).block(), is(1L)); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void lPopSouldRemoveFirstValueCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.listCommands().lPop(KEY_1_BBUFFER).block(), is(equalTo(VALUE_1_BBUFFER))); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_2, VALUE_3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void rPopSouldRemoveFirstValueCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.listCommands().rPop(KEY_1_BBUFFER).block(), is(equalTo(VALUE_3_BBUFFER))); + assertThat(nativeCommands.lrange(KEY_1, 0, -1), contains(VALUE_1, VALUE_2)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void blPopShouldReturnFirstAvailable() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + PopResult result = connection.listCommands() + .blPop(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Duration.ofSeconds(1L)).block(); + assertThat(result.getKey(), is(equalTo(KEY_1_BBUFFER))); + assertThat(result.getValue(), is(equalTo(VALUE_1_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void brPopShouldReturnLastAvailable() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + PopResult result = connection.listCommands() + .brPop(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Duration.ofSeconds(1L)).block(); + assertThat(result.getKey(), is(equalTo(KEY_1_BBUFFER))); + assertThat(result.getValue(), is(equalTo(VALUE_3_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void rPopLPushShouldWorkCorrectly() { + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.rpush(KEY_2, VALUE_1); + + ByteBuffer result = connection.listCommands().rPopLPush(KEY_1_BBUFFER, KEY_2_BBUFFER).block(); + + assertThat(result, is(equalTo(VALUE_3_BBUFFER))); + assertThat(nativeCommands.llen(KEY_2), is(2L)); + assertThat(nativeCommands.lindex(KEY_2, 0), is(equalTo(VALUE_3))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void brPopLPushShouldWorkCorrectly() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.rpush(KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.rpush(KEY_2, VALUE_1); + + ByteBuffer result = connection.listCommands().bRPopLPush(KEY_1_BBUFFER, KEY_2_BBUFFER, Duration.ofSeconds(1)) + .block(); + + assertThat(result, is(equalTo(VALUE_3_BBUFFER))); + assertThat(nativeCommands.llen(KEY_2), is(2L)); + assertThat(nativeCommands.lindex(KEY_2, 0), is(equalTo(VALUE_3))); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsTests.java new file mode 100644 index 000000000..b1dd8604a --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveNumberCommandsTests.java @@ -0,0 +1,82 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.hamcrest.number.IsCloseTo.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveNumberCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void incrByDoubleShouldIncreaseValueCorrectly() { + assertThat(connection.numberCommands().incrBy(KEY_1_BBUFFER, 1.5D).block(), is(closeTo(1.5D, 0D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void incrByIntegerShouldIncreaseValueCorrectly() { + assertThat(connection.numberCommands().incrBy(KEY_1_BBUFFER, 3).block(), is(3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void decrByDoubleShouldDecreaseValueCorrectly() { + assertThat(connection.numberCommands().decrBy(KEY_1_BBUFFER, 1.5D).block(), is(closeTo(-1.5D, 0D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void decrByIntegerShouldDecreaseValueCorrectly() { + assertThat(connection.numberCommands().decrBy(KEY_1_BBUFFER, 3).block(), is(-3)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hIncrByDoubleShouldIncreaseValueCorrectly() { + + nativeCommands.hset(KEY_1, KEY_1, "2"); + + assertThat(connection.numberCommands().hIncrBy(KEY_1_BBUFFER, KEY_1_BBUFFER, 1.5D).block(), is(closeTo(3.5D, 0D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void hIncrByIntegerShouldIncreaseValueCorrectly() { + + nativeCommands.hset(KEY_1, KEY_1, "2"); + + assertThat(connection.numberCommands().hIncrBy(KEY_1_BBUFFER, KEY_1_BBUFFER, 3).block(), is(5)); + } +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsTests.java new file mode 100644 index 000000000..3a1fb367f --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveSetCommandsTests.java @@ -0,0 +1,286 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsIterableContainingInAnyOrder.*; +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.AnyOf.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNot.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.List; + +import org.junit.Test; + +public class LettuceReactiveSetCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void sAddShouldAddSingleValue() { + assertThat(connection.setCommands().sAdd(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sAddShouldAddValues() { + assertThat(connection.setCommands().sAdd(KEY_1_BBUFFER, Arrays.asList(VALUE_1_BBUFFER, VALUE_2_BBUFFER)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sRemShouldRemoveSingleValue() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sRem(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(1L)); + assertThat(nativeCommands.sismember(KEY_1, VALUE_1), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sRemShouldRemoveValues() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sRem(KEY_1_BBUFFER, Arrays.asList(VALUE_1_BBUFFER, VALUE_2_BBUFFER)).block(), + is(2L)); + assertThat(nativeCommands.sismember(KEY_1, VALUE_1), is(false)); + assertThat(nativeCommands.sismember(KEY_1, VALUE_2), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sPopShouldRetrieveRandomValue() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sPop(KEY_1_BBUFFER).block(), is(notNullValue())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sPopShouldReturnNullWhenNotPresent() { + assertThat(connection.setCommands().sPop(KEY_1_BBUFFER).block(), is(nullValue())); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sMoveShouldMoveValueCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.sadd(KEY_2, VALUE_1); + + assertThat(connection.setCommands().sMove(KEY_1_BBUFFER, KEY_2_BBUFFER, VALUE_3_BBUFFER).block(), is(true)); + assertThat(nativeCommands.sismember(KEY_2, VALUE_3), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sMoveShouldReturnFalseIfValueIsNotAMember() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_1); + + assertThat(connection.setCommands().sMove(KEY_1_BBUFFER, KEY_2_BBUFFER, VALUE_3_BBUFFER).block(), is(false)); + assertThat(nativeCommands.sismember(KEY_2, VALUE_3), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sMoveShouldReturnOperateCorrectlyWhenValueAlreadyPresentInTarget() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + nativeCommands.sadd(KEY_2, VALUE_1, VALUE_3); + + assertThat(connection.setCommands().sMove(KEY_1_BBUFFER, KEY_2_BBUFFER, VALUE_3_BBUFFER).block(), is(true)); + assertThat(nativeCommands.sismember(KEY_1, VALUE_3), is(false)); + assertThat(nativeCommands.sismember(KEY_2, VALUE_3), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sCardShouldCountValuesCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sCard(KEY_1_BBUFFER).block(), is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sIsMemberShouldReturnTrueWhenValueContainedInKey() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(connection.setCommands().sIsMember(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sIsMemberShouldReturnFalseWhenValueNotContainedInKey() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + + assertThat(connection.setCommands().sIsMember(KEY_1_BBUFFER, VALUE_3_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sInterShouldIntersectSetsCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + List result = connection.setCommands().sInter(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(); + assertThat(result, contains(VALUE_2_BBUFFER)); + assertThat(result, not(containsInAnyOrder(VALUE_1_BBUFFER, VALUE_3_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sInterStoreShouldReturnSizeCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sInterStore(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(), + is(1L)); + assertThat(nativeCommands.sismember(KEY_3, VALUE_2), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sUnionShouldCombineSetsCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + List result = connection.setCommands().sUnion(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(); + assertThat(result, containsInAnyOrder(VALUE_1_BBUFFER, VALUE_3_BBUFFER, VALUE_2_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sUnionStoreShouldReturnSizeCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sUnionStore(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(), + is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sDiffShouldBeExcecutedCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + List result = connection.setCommands().sDiff(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(); + assertThat(result, containsInAnyOrder(VALUE_1_BBUFFER)); + assertThat(result, not(containsInAnyOrder(VALUE_2_BBUFFER, VALUE_3_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sDiffStoreShouldBeExcecutedCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2); + nativeCommands.sadd(KEY_2, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sDiffStore(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)).block(), + is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sMembersReadsValuesFromSetCorrectly() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sMembers(KEY_1_BBUFFER).block(), + containsInAnyOrder(VALUE_1_BBUFFER, VALUE_2_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sRandMemberReturnsRandomMember() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sRandMember(KEY_1_BBUFFER).block(), + anyOf(equalTo(VALUE_1_BBUFFER), equalTo(VALUE_2_BBUFFER), equalTo(VALUE_3_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void sRandMemberReturnsRandomMembers() { + + nativeCommands.sadd(KEY_1, VALUE_1, VALUE_2, VALUE_3); + + assertThat(connection.setCommands().sRandMember(KEY_1_BBUFFER, 2L).block().size(), is(2)); + } + +} diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java new file mode 100644 index 000000000..10abbc7fd --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveStringCommandsTests.java @@ -0,0 +1,448 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.collection.IsIterableContainingInOrder.*; +import static org.hamcrest.core.Is.*; +import static org.hamcrest.core.IsEqual.*; +import static org.hamcrest.core.IsNull.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; +import org.springframework.data.redis.connection.ReactiveRedisConnection.BooleanResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.ByteBufferResponse; +import org.springframework.data.redis.connection.ReactiveRedisConnection.KeyCommand; +import org.springframework.data.redis.connection.ReactiveRedisConnection.MultiValueResponse; +import org.springframework.data.redis.connection.ReactiveStringCommands.SetCommand; +import org.springframework.data.redis.connection.RedisStringCommands.BitOperation; +import org.springframework.data.redis.core.types.Expiration; + +import org.springframework.data.redis.test.util.LettuceRedisClientProvider; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; +import reactor.test.TestSubscriber; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveStringCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void getSetShouldReturnPreviousValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + Mono result = connection.stringCommands().getSet(KEY_1_BBUFFER, VALUE_2_BBUFFER); + + assertThat(result.block(), is(equalTo(VALUE_1_BBUFFER))); + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getSetShouldReturnPreviousValueCorrectlyWhenNoExists() { + + Mono result = connection.stringCommands().getSet(KEY_1_BBUFFER, VALUE_2_BBUFFER); + + ByteBuffer value = result.block(); + assertThat(value, is(notNullValue())); + assertThat(value, is(equalTo(ByteBuffer.allocate(0)))); + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setShouldAddValueCorrectly() { + + Mono result = connection.stringCommands().set(KEY_1_BBUFFER, VALUE_1_BBUFFER); + + assertThat(result.block(), is(true)); + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_1))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setShouldAddValuesCorrectly() { + + Flux> result = connection.stringCommands() + .set(Flux.fromIterable(Arrays.asList(SetCommand.set(KEY_1_BBUFFER).value(VALUE_1_BBUFFER), + SetCommand.set(KEY_2_BBUFFER).value(VALUE_2_BBUFFER)))); + + TestSubscriber> subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(2); + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_1))); + assertThat(nativeCommands.get(KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getShouldRetriveValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + Mono result = connection.stringCommands().get(KEY_1_BBUFFER); + assertThat(result.block(), is(equalTo(VALUE_1_BBUFFER))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getShouldRetriveNullValueCorrectly() { + + Mono result = connection.stringCommands().get(KEY_1_BBUFFER); + assertThat(result.block(), is(equalTo(ByteBuffer.allocate(0)))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getShouldRetriveValuesCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Flux> result = connection.stringCommands() + .get(Flux.fromStream(Arrays.asList(new KeyCommand(KEY_1_BBUFFER), new KeyCommand(KEY_2_BBUFFER)).stream())); + + TestSubscriber> subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(2); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getShouldRetriveValuesWithNullCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_3, VALUE_3); + + Flux> result = connection.stringCommands().get(Flux.fromStream(Arrays + .asList(new KeyCommand(KEY_1_BBUFFER), new KeyCommand(KEY_2_BBUFFER), new KeyCommand(KEY_3_BBUFFER)).stream())); + + TestSubscriber> subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(3); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mGetShouldRetriveValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Mono> result = connection.stringCommands().mGet(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER)); + assertThat(result.block(), contains(VALUE_1_BBUFFER, VALUE_2_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mGetShouldRetriveNullValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_3, VALUE_3); + + Mono> result = connection.stringCommands() + .mGet(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER, KEY_3_BBUFFER)); + + assertThat(result.block(), contains(VALUE_1_BBUFFER, ByteBuffer.allocate(0), VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mGetShouldRetriveValuesCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + Flux> result = connection.stringCommands() + .mGet( + Flux.fromIterable(Arrays.asList(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Arrays.asList(KEY_2_BBUFFER)))) + .map(MultiValueResponse::getOutput); + + TestSubscriber> subscriber = TestSubscriber.create(); + result.subscribe(subscriber); + subscriber.await(); + + subscriber.assertValueCount(2); + subscriber.assertContainValues( + new HashSet<>(Arrays.asList(Arrays.asList(VALUE_1_BBUFFER, VALUE_2_BBUFFER), Arrays.asList(VALUE_2_BBUFFER)))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setNXshouldOnlySetValueWhenNotPresent() { + assertThat(connection.stringCommands().setNX(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setNXshouldNotSetValueWhenAlreadyPresent() { + + nativeCommands.setnx(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().setNX(KEY_1_BBUFFER, VALUE_2_BBUFFER).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setEXshouldSetKeyAndExpirationTime() { + + connection.stringCommands().setEX(KEY_1_BBUFFER, VALUE_1_BBUFFER, Expiration.seconds(3)).block(); + + assertThat(nativeCommands.ttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void pSetEXshouldSetKeyAndExpirationTime() { + + connection.stringCommands().pSetEX(KEY_1_BBUFFER, VALUE_1_BBUFFER, Expiration.milliseconds(600)).block(); + + assertThat(nativeCommands.pttl(KEY_1) > 1, is(true)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mSetShouldAddMultipleKeyValueParis() { + + Map map = new LinkedHashMap<>(); + map.put(KEY_1_BBUFFER, VALUE_1_BBUFFER); + map.put(KEY_2_BBUFFER, VALUE_2_BBUFFER); + + connection.stringCommands().mSet(map).block(); + + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_1))); + assertThat(nativeCommands.get(KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mSetNXShouldAddMultipleKeyValueParis() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + Map map = new LinkedHashMap<>(); + map.put(KEY_1_BBUFFER, VALUE_1_BBUFFER); + map.put(KEY_2_BBUFFER, VALUE_2_BBUFFER); + + connection.stringCommands().mSetNX(map).block(); + + assertThat(nativeCommands.get(KEY_1), is(equalTo(VALUE_1))); + assertThat(nativeCommands.get(KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void mSetNXShouldNotAddMultipleKeyValueParisWhenAlreadyExit() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.set(KEY_2, VALUE_2); + + Map map = new LinkedHashMap<>(); + map.put(KEY_1_BBUFFER, VALUE_1_BBUFFER); + map.put(KEY_2_BBUFFER, VALUE_2_BBUFFER); + + assertThat(connection.stringCommands().mSetNX(map).block(), is(false)); + + assertThat(nativeCommands.exists(KEY_1), is(false)); + assertThat(nativeCommands.get(KEY_2), is(equalTo(VALUE_2))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void appendShouldDoItsThing() { + + assertThat(connection.stringCommands().append(KEY_1_BBUFFER, VALUE_1_BBUFFER).block(), is(7L)); + assertThat(connection.stringCommands().append(KEY_1_BBUFFER, VALUE_2_BBUFFER).block(), is(14L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getRangeShouldReturnSubstringCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().getRange(KEY_1_BBUFFER, 2, 3).block(), + is(equalTo(ByteBuffer.wrap("lu".getBytes())))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setRangeShouldReturnNewStringLengthCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().setRange(KEY_1_BBUFFER, VALUE_2_BBUFFER, 3).block(), is(10L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void getBitShouldReturnValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().getBit(KEY_1_BBUFFER, 1).block(), is(true)); + assertThat(connection.stringCommands().getBit(KEY_1_BBUFFER, 7).block(), is(false)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void setBitShouldReturnValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().setBit(KEY_1_BBUFFER, 1, false).block(), is(true)); + assertThat(nativeCommands.getbit(KEY_1, 1), is(0L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitCountShouldReturnValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().bitCount(KEY_1_BBUFFER).block(), is(28L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitCountShouldCountInRangeCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + + assertThat(connection.stringCommands().bitCount(KEY_1_BBUFFER, 2, 4).block(), is(13L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitOpAndShouldWorkAsExpected() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + assertThat(connection.stringCommands() + .bitOp(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), BitOperation.AND, KEY_3_BBUFFER).block(), is(7L)); + assertThat(nativeCommands.get(KEY_3), is(equalTo("value-0"))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void bitOpOrShouldWorkAsExpected() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.set(KEY_1, VALUE_1); + nativeCommands.set(KEY_2, VALUE_2); + + assertThat(connection.stringCommands() + .bitOp(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), BitOperation.OR, KEY_3_BBUFFER).block(), is(7L)); + assertThat(nativeCommands.get(KEY_3), is(equalTo(VALUE_3))); + } + + /** + * @see DATAREDIS-525 + */ + @Test(expected = IllegalArgumentException.class) + public void bitNotShouldThrowExceptionWhenMoreThanOnSourceKey() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + connection.stringCommands().bitOp(Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), BitOperation.NOT, KEY_3_BBUFFER) + .block(); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void strLenShouldReturnValueCorrectly() { + + nativeCommands.set(KEY_1, VALUE_1); + assertThat(connection.stringCommands().strLen(KEY_1_BBUFFER).block(), is(7L)); + } +} 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 new file mode 100644 index 000000000..a7fd542a0 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceReactiveZSetCommandsTests.java @@ -0,0 +1,594 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.connection.lettuce; + +import static org.hamcrest.core.Is.*; +import static org.junit.Assert.*; +import static org.junit.Assume.assumeThat; + +import java.nio.ByteBuffer; +import java.util.Arrays; + +import org.hamcrest.collection.IsIterableContainingInOrder; +import org.junit.Test; +import org.springframework.data.domain.Range; +import org.springframework.data.redis.connection.DefaultTuple; +import org.springframework.data.redis.test.util.LettuceRedisClientProvider; + +/** + * @author Christoph Strobl + */ +public class LettuceReactiveZSetCommandsTests extends LettuceReactiveCommandsTestsBase { + + /** + * @see DATAREDIS-525 + */ + @Test + public void zAddShouldAddValuesWithScores() { + assertThat(connection.zSetCommands().zAdd(KEY_1_BBUFFER, 3.5D, VALUE_1_BBUFFER).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemShouldRemoveValuesFromSet() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRem(KEY_1_BBUFFER, Arrays.asList(VALUE_1_BBUFFER, VALUE_3_BBUFFER)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zIncrByShouldInreaseAndReturnScore() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + + assertThat(connection.zSetCommands().zIncrBy(KEY_1_BBUFFER, 3.5D, VALUE_1_BBUFFER).block(), is(4.5D)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRankShouldReturnIndexCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRank(KEY_1_BBUFFER, VALUE_3_BBUFFER).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRankShouldReturnIndexCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRevRank(KEY_1_BBUFFER, VALUE_3_BBUFFER).block(), is(0L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeShouldReturnValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRange(KEY_1_BBUFFER, new Range(1L, 2L)).block(), + IsIterableContainingInOrder.contains(VALUE_2_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeWithScoreShouldReturnTuplesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRangeWithScores(KEY_1_BBUFFER, new Range(1L, 2L)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D), + new DefaultTuple(VALUE_3_BBUFFER.array(), 3D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeShouldReturnValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRevRange(KEY_1_BBUFFER, new Range(1L, 2L)).block(), + IsIterableContainingInOrder.contains(VALUE_2_BBUFFER, VALUE_1_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeWithScoreShouldReturnTuplesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRevRangeWithScores(KEY_1_BBUFFER, new Range(1L, 2L)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D), + new DefaultTuple(VALUE_1_BBUFFER.array(), 1D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D)).block(), + IsIterableContainingInOrder.contains(VALUE_2_BBUFFER, VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectlyWithMinExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), + IsIterableContainingInOrder.contains(VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreShouldReturnValuesCorrectlyWithMaxExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), + IsIterableContainingInOrder.contains(VALUE_2_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreWithScoreShouldReturnTuplesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D), + new DefaultTuple(VALUE_3_BBUFFER.array(), 3D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreWithScoreShouldReturnTuplesCorrectlyWithMinExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat( + connection.zSetCommands().zRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_3_BBUFFER.array(), 3D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByScoreWithScoreShouldReturnTuplesCorrectlyWithMaxExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat( + connection.zSetCommands().zRangeByScoreWithScores(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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(), + IsIterableContainingInOrder.contains(VALUE_3_BBUFFER, VALUE_2_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectlyWithMinExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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(), + IsIterableContainingInOrder.contains(VALUE_3_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreShouldReturnValuesCorrectlyWithMaxExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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(), + IsIterableContainingInOrder.contains(VALUE_2_BBUFFER)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreWithScoreShouldReturnTuplesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_3_BBUFFER.array(), 3D), + new DefaultTuple(VALUE_2_BBUFFER.array(), 2D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreWithScoreShouldReturnTuplesCorrectlyWithMinExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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, true, false)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_3_BBUFFER.array(), 3D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRevRangeByScoreWithScoreShouldReturnTuplesCorrectlyWithMaxExclusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + 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, false, true)).block(), + IsIterableContainingInOrder.contains(new DefaultTuple(VALUE_2_BBUFFER.array(), 2D))); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCountShouldCountValuesInRange() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCount(KEY_1_BBUFFER, new Range<>(2D, 3D)).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCountShouldCountValuesInRangeWithMinExlusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCount(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCountShouldCountValuesInRangeWithMaxExlusion() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCount(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCountShouldCountValuesInRangeWithNegativeInfinity() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCount(KEY_1_BBUFFER, new Range<>(Double.NEGATIVE_INFINITY, 2D)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCountShouldCountValuesInRangeWithPositiveInfinity() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCount(KEY_1_BBUFFER, new Range<>(2D, Double.POSITIVE_INFINITY)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zCardShouldReturnSizeCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zCard(KEY_1_BBUFFER).block(), is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zScoreShouldReturnScoreCorrectly() { + + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + + assertThat(connection.zSetCommands().zScore(KEY_1_BBUFFER, VALUE_2_BBUFFER).block(), is(2D)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByRankShouldRemoveValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRemRangeByRank(KEY_1_BBUFFER, new Range<>(1L, 2L)).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByScoreShouldRemoveValuesCorrectly() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRemRangeByScore(KEY_1_BBUFFER, new Range<>(1D, 2D)).block(), is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByScoreShouldRemoveValuesCorrectlyWithNegativeInfinity() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat( + connection.zSetCommands().zRemRangeByScore(KEY_1_BBUFFER, new Range<>(Double.NEGATIVE_INFINITY, 2D)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByScoreShouldRemoveValuesCorrectlyWithPositiveInfinity() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat( + connection.zSetCommands().zRemRangeByScore(KEY_1_BBUFFER, new Range<>(2D, Double.POSITIVE_INFINITY)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByScoreShouldRemoveValuesCorrectlyWithExcludingMinRange() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRemRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, false, true)).block(), + is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRemRangeByScoreShouldRemoveValuesCorrectlyWithExcludingMaxRange() { + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_1, 3D, VALUE_3); + + assertThat(connection.zSetCommands().zRemRangeByScore(KEY_1_BBUFFER, new Range<>(2D, 3D, true, false)).block(), + is(1L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zUnionStoreShouldWorkCorrectly() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_2, 1D, VALUE_1); + nativeCommands.zadd(KEY_2, 2D, VALUE_2); + nativeCommands.zadd(KEY_2, 3D, VALUE_3); + + assertThat( + connection.zSetCommands() + .zUnionStore(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Arrays.asList(2D, 3D)).block(), + is(3L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zInterStoreShouldWorkCorrectly() { + + assumeThat(clientProvider instanceof LettuceRedisClientProvider, is(true)); + + nativeCommands.zadd(KEY_1, 1D, VALUE_1); + nativeCommands.zadd(KEY_1, 2D, VALUE_2); + nativeCommands.zadd(KEY_2, 1D, VALUE_1); + nativeCommands.zadd(KEY_2, 2D, VALUE_2); + nativeCommands.zadd(KEY_2, 3D, VALUE_3); + + assertThat( + connection.zSetCommands() + .zInterStore(KEY_3_BBUFFER, Arrays.asList(KEY_1_BBUFFER, KEY_2_BBUFFER), Arrays.asList(2D, 3D)).block(), + is(2L)); + } + + /** + * @see DATAREDIS-525 + */ + @Test + public void zRangeByLex() { + + nativeCommands.zadd(KEY_1, 0D, "a"); + nativeCommands.zadd(KEY_1, 0D, "b"); + nativeCommands.zadd(KEY_1, 0D, "c"); + nativeCommands.zadd(KEY_1, 0D, "d"); + nativeCommands.zadd(KEY_1, 0D, "e"); + nativeCommands.zadd(KEY_1, 0D, "f"); + nativeCommands.zadd(KEY_1, 0D, "g"); + + assertThat(connection.zSetCommands().zRangeByLex(KEY_1_BBUFFER, new Range<>("", "c")).block(), + IsIterableContainingInOrder.contains(ByteBuffer.wrap("a".getBytes()), ByteBuffer.wrap("b".getBytes()), + ByteBuffer.wrap("c".getBytes()))); + + assertThat(connection.zSetCommands().zRangeByLex(KEY_1_BBUFFER, new Range<>("", "c", true, false)).block(), + IsIterableContainingInOrder.contains(ByteBuffer.wrap("a".getBytes()), ByteBuffer.wrap("b".getBytes()))); + + assertThat(connection.zSetCommands().zRangeByLex(KEY_1_BBUFFER, new Range<>("aaa", "g", true, false)).block(), + IsIterableContainingInOrder.contains(ByteBuffer.wrap("b".getBytes()), ByteBuffer.wrap("c".getBytes()), + ByteBuffer.wrap("d".getBytes()), ByteBuffer.wrap("e".getBytes()), ByteBuffer.wrap("f".getBytes()))); + } + + /** + * @see DATAREDIS-525 + */ + @Test(expected = UnsupportedOperationException.class) + public void zRevRangeByLex() { + + nativeCommands.zadd(KEY_1, 0D, "a"); + nativeCommands.zadd(KEY_1, 0D, "b"); + nativeCommands.zadd(KEY_1, 0D, "c"); + nativeCommands.zadd(KEY_1, 0D, "d"); + nativeCommands.zadd(KEY_1, 0D, "e"); + nativeCommands.zadd(KEY_1, 0D, "f"); + nativeCommands.zadd(KEY_1, 0D, "g"); + + 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(), + IsIterableContainingInOrder.contains(ByteBuffer.wrap("a".getBytes()), ByteBuffer.wrap("b".getBytes()))); + + assertThat(connection.zSetCommands().zRevRangeByLex(KEY_1_BBUFFER, new Range<>("g", "aaa", false, true)).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/test/util/LettuceRedisClientProvider.java b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java new file mode 100644 index 000000000..15cfec9f8 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClientProvider.java @@ -0,0 +1,68 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.test.util; + +import org.junit.rules.ExternalResource; + +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.RedisURI; + +/** + * @author Christoph Strobl + */ +public class LettuceRedisClientProvider extends ExternalResource { + + String host = "127.0.0.1"; + int port = 6379; + + RedisClient client; + + @Override + protected void before() { + + try { + super.before(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + client = RedisClient.create(RedisURI.builder().withHost(host).withPort(port).build()); + } + + @Override + protected void after() { + super.after(); + client.shutdown(); + } + + public RedisClient getClient() { + if(client == null) { + before(); + } + return client; + } + + public void destroy() { + + if(client != null) { + after(); + } + } + + public static LettuceRedisClientProvider local() { + return new LettuceRedisClientProvider(); + } +} diff --git a/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java new file mode 100644 index 000000000..41cd76b10 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/test/util/LettuceRedisClusterClientProvider.java @@ -0,0 +1,101 @@ +/* + * Copyright 2016. the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.redis.test.util; + +import com.lambdaworks.redis.cluster.RedisClusterClient; +import com.lambdaworks.redis.cluster.api.StatefulRedisClusterConnection; +import com.lambdaworks.redis.cluster.api.sync.RedisClusterCommands; +import org.junit.rules.ExternalResource; + +import com.lambdaworks.redis.RedisClient; +import com.lambdaworks.redis.RedisURI; + +/** + * @author Christoph Strobl + */ +public class LettuceRedisClusterClientProvider extends ExternalResource { + + String host = "127.0.0.1"; + int port = 7379; + + RedisClusterClient client; + + @Override + protected void before() { + + try { + super.before(); + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + + client = RedisClusterClient.create(RedisURI.builder().withHost(host).withPort(port).build()); + } + + @Override + protected void after() { + + super.after(); + client.shutdown(); + } + + public RedisClusterClient getClient() { + + if(client == null) { + before(); + } + return client; + } + + public void destroy() { + + if(client != null) { + after(); + } + } + + public boolean test() { + + try { + StatefulRedisClusterConnection conn = getClient().connect(); + conn.close(); + } catch (Exception e) { + return false; + } + + return true; + } + + public static LettuceRedisClusterClientProvider local() { + return new LettuceRedisClusterClientProvider(); + } +} diff --git a/src/test/java/reactor/test/TestSubscriber.java b/src/test/java/reactor/test/TestSubscriber.java new file mode 100644 index 000000000..b23b55115 --- /dev/null +++ b/src/test/java/reactor/test/TestSubscriber.java @@ -0,0 +1,1129 @@ +/* + * Copyright 2016 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package reactor.test; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.function.BooleanSupplier; +import java.util.function.Consumer; +import java.util.function.Supplier; + +import org.reactivestreams.Publisher; +import org.reactivestreams.Subscriber; +import org.reactivestreams.Subscription; + +import reactor.core.Fuseable; +import reactor.core.Receiver; +import reactor.core.Trackable; +import reactor.core.publisher.Operators; + +/** + *
+ *  ###############################################################
+ *  ###############################################################
+ *  ###############################################################
+ *
+ *  	THIS CODE IS IMPORTED FROM REACTOR-CORE BECAUSE OF
+ *  	https://github.com/reactor/reactor-core/issues/135
+ *
+ *  ###############################################################
+ *  ###############################################################
+ *  ###############################################################
+ * 
+ * + * A Subscriber implementation that hosts assertion tests for its state and allows asynchronous cancellation and + * requesting. + *

+ * To create a new instance of {@link TestSubscriber}, you have the choice between these static methods: + *

    + *
  • {@link TestSubscriber#subscribe(Publisher)}: create a new {@link TestSubscriber}, subscribe to it with the + * specified {@link Publisher} and requests an unbounded number of elements.
  • + *
  • {@link TestSubscriber#subscribe(Publisher, long)}: create a new {@link TestSubscriber}, subscribe to it with the + * specified {@link Publisher} and requests {@code n} elements (can be 0 if you want no initial demand). + *
  • {@link TestSubscriber#create()}: create a new {@link TestSubscriber} and requests an unbounded number of + * elements.
  • + *
  • {@link TestSubscriber#create(long)}: create a new {@link TestSubscriber} and requests {@code n} elements (can be + * 0 if you want no initial demand). + *
+ *

+ * If you are testing asynchronous publishers, don't forget to use one of the {@code await*()} methods to wait for the + * data to assert. + *

+ * You can extend this class but only the onNext, onError and onComplete can be overridden. You can call + * {@link #request(long)} and {@link #cancel()} from any thread or from within the overridable methods but you should + * avoid calling the assertXXX methods asynchronously. + *

+ * Usage: + * + *

+ * {@code
+ * TestSubscriber
+ *   .subscribe(publisher)
+ *   .await()
+ *   .assertValues("ABC", "DEF");
+ * }
+ * 
+ * + * @param the value type. + * @author Sebastien Deleuze + * @author David Karnok + * @author Anatoly Kadyshev + * @author Stephane Maldini + * @author Brian Clozel + */ +public class TestSubscriber implements Subscriber, Subscription, Trackable, Receiver { + + /** + * Default timeout for waiting next values to be received + */ + public static final Duration DEFAULT_VALUES_TIMEOUT = Duration.ofSeconds(3); + + @SuppressWarnings("rawtypes") private static final AtomicLongFieldUpdater REQUESTED = AtomicLongFieldUpdater + .newUpdater(TestSubscriber.class, "requested"); + + @SuppressWarnings("rawtypes") private static final AtomicReferenceFieldUpdater NEXT_VALUES = AtomicReferenceFieldUpdater + .newUpdater(TestSubscriber.class, List.class, "values"); + + @SuppressWarnings("rawtypes") private static final AtomicReferenceFieldUpdater S = AtomicReferenceFieldUpdater + .newUpdater(TestSubscriber.class, Subscription.class, "s"); + + private final List errors = new LinkedList<>(); + + private final CountDownLatch cdl = new CountDownLatch(1); + + volatile Subscription s; + + volatile long requested; + + volatile List values = new LinkedList<>(); + + /** + * The fusion mode to request. + */ + private int requestedFusionMode = -1; + + /** + * The established fusion mode. + */ + private volatile int establishedFusionMode = -1; + + /** + * The fuseable QueueSubscription in case a fusion mode was specified. + */ + private Fuseable.QueueSubscription qs; + + private int subscriptionCount = 0; + + private int completionCount = 0; + + private volatile long valueCount = 0L; + + private volatile long nextValueAssertedCount = 0L; + + private Duration valuesTimeout = DEFAULT_VALUES_TIMEOUT; + + private boolean valuesStorage = true; + + // ============================================================================================================== + // Static methods + // ============================================================================================================== + + /** + * Blocking method that waits until {@code conditionSupplier} returns true, or if it does not before the specified + * timeout, throws an {@link AssertionError} with the specified error message supplier. + * + * @param timeout the timeout duration + * @param errorMessageSupplier the error message supplier + * @param conditionSupplier condition to break out of the wait loop + * @throws AssertionError + */ + public static void await(Duration timeout, Supplier errorMessageSupplier, BooleanSupplier conditionSupplier) { + + Objects.requireNonNull(errorMessageSupplier); + Objects.requireNonNull(conditionSupplier); + Objects.requireNonNull(timeout); + + long timeoutNs = timeout.toNanos(); + long startTime = System.nanoTime(); + do { + if (conditionSupplier.getAsBoolean()) { + return; + } + try { + Thread.sleep(100); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } + } while (System.nanoTime() - startTime < timeoutNs); + throw new AssertionError(errorMessageSupplier.get()); + } + + /** + * Blocking method that waits until {@code conditionSupplier} returns true, or if it does not before the specified + * timeout, throw an {@link AssertionError} with the specified error message. + * + * @param timeout the timeout duration + * @param errorMessage the error message + * @param conditionSupplier condition to break out of the wait loop + * @throws AssertionError + */ + public static void await(Duration timeout, final String errorMessage, BooleanSupplier conditionSupplier) { + await(timeout, new Supplier() { + @Override + public String get() { + return errorMessage; + } + }, conditionSupplier); + } + + /** + * Create a new {@link TestSubscriber} that requests an unbounded number of elements. + *

+ * Be sure at least a publisher has subscribed to it via {@link Publisher#subscribe(Subscriber)} before use assert + * methods. + * + * @see #subscribe(Publisher) + * @param the observed value type + * @return a fresh TestSubscriber instance + */ + public static TestSubscriber create() { + return new TestSubscriber<>(); + } + + /** + * Create a new {@link TestSubscriber} that requests initially {@code n} elements. You can then manage the demand with + * {@link Subscription#request(long)}. + *

+ * Be sure at least a publisher has subscribed to it via {@link Publisher#subscribe(Subscriber)} before use assert + * methods. + * + * @param n Number of elements to request (can be 0 if you want no initial demand). + * @see #subscribe(Publisher, long) + * @param the observed value type + * @return a fresh TestSubscriber instance + */ + public static TestSubscriber create(long n) { + return new TestSubscriber<>(n); + } + + /** + * Create a new {@link TestSubscriber} that requests an unbounded number of elements, and make the specified + * {@code publisher} subscribe to it. + * + * @param publisher The publisher to subscribe with + * @param the observed value type + * @return a fresh TestSubscriber instance + */ + public static TestSubscriber subscribe(Publisher publisher) { + TestSubscriber subscriber = new TestSubscriber<>(); + publisher.subscribe(subscriber); + return subscriber; + } + + /** + * Create a new {@link TestSubscriber} that requests initially {@code n} elements, and make the specified + * {@code publisher} subscribe to it. You can then manage the demand with {@link Subscription#request(long)}. + * + * @param publisher The publisher to subscribe with + * @param n Number of elements to request (can be 0 if you want no initial demand). + * @param the observed value type + * @return a fresh TestSubscriber instance + */ + public static TestSubscriber subscribe(Publisher publisher, long n) { + TestSubscriber subscriber = new TestSubscriber<>(n); + publisher.subscribe(subscriber); + return subscriber; + } + + // ============================================================================================================== + // Private constructors + // ============================================================================================================== + + private TestSubscriber() { + this(Long.MAX_VALUE); + } + + private TestSubscriber(long n) { + if (n < 0) { + throw new IllegalArgumentException("initialRequest >= required but it was " + n); + } + REQUESTED.lazySet(this, n); + } + + // ============================================================================================================== + // Configuration + // ============================================================================================================== + + /** + * Enable or disabled the values storage. It is enabled by default, and can be disable in order to be able to perform + * performance benchmarks or tests with a huge amount values. + * + * @param enabled enable value storage? + * @return this + */ + public final TestSubscriber configureValuesStorage(boolean enabled) { + this.valuesStorage = enabled; + return this; + } + + /** + * Configure the timeout in seconds for waiting next values to be received (3 seconds by default). + * + * @param timeout the new default value timeout duration + * @return this + */ + public final TestSubscriber configureValuesTimeout(Duration timeout) { + this.valuesTimeout = timeout; + return this; + } + + /** + * Returns the established fusion mode or -1 if it was not enabled + * + * @return the fusion mode, see Fuseable constants + */ + public final int establishedFusionMode() { + return establishedFusionMode; + } + + // ============================================================================================================== + // Assertions + // ============================================================================================================== + + /** + * Assert a complete successfully signal has been received. + * + * @return this + */ + public final TestSubscriber assertComplete() { + assertNoError(); + int c = completionCount; + if (c == 0) { + throw new AssertionError("Not completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert the specified values have been received. Values storage should be enabled to use this method. + * + * @param expectedValues the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final TestSubscriber assertContainValues(Set expectedValues) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + if (expectedValues.size() > values.size()) { + throw new AssertionError("Actual contains fewer elements" + values, null); + } + + Iterator expected = expectedValues.iterator(); + + for (;;) { + boolean n2 = expected.hasNext(); + if (n2) { + T t2 = expected.next(); + if (!values.contains(t2)) { + throw new AssertionError( + "The element is not contained in the " + "received resuls" + " = " + valueAndClass(t2), null); + } + } else { + break; + } + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @return this + */ + public final TestSubscriber assertError() { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert an error signal has been received. + * + * @param clazz The class of the exception contained in the error signal + * @return this + */ + public final TestSubscriber assertError(Class clazz) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + Throwable e = errors.get(0); + if (!clazz.isInstance(e)) { + throw new AssertionError("Error class incompatible: expected = " + clazz + ", actual = " + e, null); + } + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + public final TestSubscriber assertErrorMessage(String message) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + assertionError("No error", null); + } + if (s == 1) { + if (!Objects.equals(message, errors.get(0).getMessage())) { + assertionError( + "Error class incompatible: expected = \"" + message + "\", actual = \"" + errors.get(0).getMessage() + "\"", + null); + } + } + if (s > 1) { + assertionError("Multiple errors: " + s, null); + } + + return this; + } + + /** + * Assert an error signal has been received. + * + * @param expectation A method that can verify the exception contained in the error signal and throw an exception + * (like an {@link AssertionError}) if the exception is not valid. + * @return this + */ + public final TestSubscriber assertErrorWith(Consumer expectation) { + assertNotComplete(); + int s = errors.size(); + if (s == 0) { + throw new AssertionError("No error", null); + } + if (s == 1) { + expectation.accept(errors.get(0)); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert that the upstream was a Fuseable source. + * + * @return this + */ + public final TestSubscriber assertFuseableSource() { + if (qs == null) { + throw new AssertionError("Upstream was not Fuseable"); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final TestSubscriber assertFusionEnabled() { + if (establishedFusionMode != Fuseable.SYNC && establishedFusionMode != Fuseable.ASYNC) { + throw new AssertionError("Fusion was not enabled"); + } + return this; + } + + public final TestSubscriber assertFusionMode(int expectedMode) { + if (establishedFusionMode != expectedMode) { + throw new AssertionError("Wrong fusion mode: expected: " + fusionModeName(expectedMode) + ", actual: " + + fusionModeName(establishedFusionMode)); + } + return this; + } + + /** + * Assert that the fusion mode was granted. + * + * @return this + */ + public final TestSubscriber assertFusionRejected() { + if (establishedFusionMode != Fuseable.NONE) { + throw new AssertionError("Fusion was granted"); + } + return this; + } + + /** + * Assert no error signal has been received. + * + * @return this + */ + public final TestSubscriber assertNoError() { + int s = errors.size(); + if (s == 1) { + Throwable e = errors.get(0); + String valueAndClass = e == null ? null : e + " (" + e.getClass().getSimpleName() + ")"; + throw new AssertionError("Error present: " + valueAndClass, null); + } + if (s > 1) { + throw new AssertionError("Multiple errors: " + s, null); + } + return this; + } + + /** + * Assert no values have been received. + * + * @return this + */ + public final TestSubscriber assertNoValues() { + if (valueCount != 0) { + throw new AssertionError("No values expected but received: [length = " + values.size() + "] " + values, null); + } + return this; + } + + /** + * Assert that the upstream was not a Fuseable source. + * + * @return this + */ + public final TestSubscriber assertNonFuseableSource() { + if (qs != null) { + throw new AssertionError("Upstream was Fuseable"); + } + return this; + } + + /** + * Assert no complete successfully signal has been received. + * + * @return this + */ + public final TestSubscriber assertNotComplete() { + int c = completionCount; + if (c == 1) { + throw new AssertionError("Completed", null); + } + if (c > 1) { + throw new AssertionError("Multiple completions: " + c, null); + } + return this; + } + + /** + * Assert no subscription occurred. + * + * @return this + */ + public final TestSubscriber assertNotSubscribed() { + int s = subscriptionCount; + + if (s == 1) { + throw new AssertionError("OnSubscribe called once", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert no complete successfully or error signal has been received. + * + * @return this + */ + public final TestSubscriber assertNotTerminated() { + if (cdl.getCount() == 0) { + throw new AssertionError("Terminated", null); + } + return this; + } + + /** + * Assert subscription occurred (once). + * + * @return this + */ + public final TestSubscriber assertSubscribed() { + int s = subscriptionCount; + + if (s == 0) { + throw new AssertionError("OnSubscribe not called", null); + } + if (s > 1) { + throw new AssertionError("OnSubscribe called multiple times: " + s, null); + } + + return this; + } + + /** + * Assert either complete successfully or error signal has been received. + * + * @return this + */ + public final TestSubscriber assertTerminated() { + if (cdl.getCount() != 0) { + throw new AssertionError("Not terminated", null); + } + return this; + } + + /** + * Assert {@code n} values has been received. + * + * @param n the expected value count + * @return this + */ + public final TestSubscriber assertValueCount(long n) { + if (valueCount != n) { + throw new AssertionError("Different value count: expected = " + n + ", actual = " + valueCount, null); + } + return this; + } + + /** + * Assert the specified values have been received in the same order read by the passed {@link Iterable}. Values + * storage should be enabled to use this method. + * + * @param expectedSequence the values to assert + * @see #configureValuesStorage(boolean) + * @return this + */ + public final TestSubscriber assertValueSequence(Iterable expectedSequence) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + Iterator actual = values.iterator(); + Iterator expected = expectedSequence.iterator(); + int i = 0; + for (;;) { + boolean n1 = actual.hasNext(); + boolean n2 = expected.hasNext(); + if (n1 && n2) { + T t1 = actual.next(); + T t2 = expected.next(); + if (!Objects.equals(t1, t2)) { + throw new AssertionError("The element with index " + i + " does not match: expected = " + valueAndClass(t2) + + ", actual = " + valueAndClass(t1), null); + } + i++; + } else if (n1 && !n2) { + throw new AssertionError("Actual contains more elements" + values, null); + } else if (!n1 && n2) { + throw new AssertionError("Actual contains fewer elements: " + values, null); + } else { + break; + } + } + return this; + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be enabled to use this + * method. + * + * @param expectedValues the values to assert + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final TestSubscriber assertValues(T... expectedValues) { + return assertValueSequence(Arrays.asList(expectedValues)); + } + + /** + * Assert the specified values have been received in the declared order. Values storage should be enabled to use this + * method. + * + * @param expectations One or more methods that can verify the values and throw a exception (like an + * {@link AssertionError}) if the value is not valid. + * @return this + * @see #configureValuesStorage(boolean) + */ + @SafeVarargs + public final TestSubscriber assertValuesWith(Consumer... expectations) { + if (!valuesStorage) { + throw new IllegalStateException("Using assertNoValues() requires enabling values storage"); + } + final int expectedValueCount = expectations.length; + if (expectedValueCount != values.size()) { + throw new AssertionError("Different value count: expected = " + expectedValueCount + ", actual = " + valueCount, + null); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = values.get(i); + consumer.accept(actualValue); + } + return this; + } + + // ============================================================================================================== + // Await methods + // ============================================================================================================== + + /** + * Blocking method that waits until a complete successfully or error signal is received. + * + * @return this + */ + public final TestSubscriber await() { + if (cdl.getCount() == 0) { + return this; + } + try { + cdl.await(); + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + return this; + } + + /** + * Blocking method that waits until a complete successfully or error signal is received or until a timeout occurs. + * + * @param timeout The timeout value + * @return this + */ + public final TestSubscriber await(Duration timeout) { + if (cdl.getCount() == 0) { + return this; + } + try { + if (!cdl.await(timeout.toMillis(), TimeUnit.MILLISECONDS)) { + throw new AssertionError("No complete or error signal before timeout"); + } + return this; + } catch (InterruptedException ex) { + throw new AssertionError("Wait interrupted", ex); + } + } + + /** + * Blocking method that waits until {@code n} next values have been received. + * + * @param n the value count to assert + * @return this + */ + public final TestSubscriber awaitAndAssertNextValueCount(final long n) { + await(valuesTimeout, () -> { + if (valuesStorage) { + return String.format("%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, n, valuesTimeout.toMillis(), values.toString()); + } + return String.format("%d out of %d next values received within %d", valueCount - nextValueAssertedCount, n, + valuesTimeout.toMillis()); + }, () -> valueCount >= (nextValueAssertedCount + n)); + nextValueAssertedCount += n; + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of values provided) to + * assert them. + * + * @param values the values to assert + * @return this + */ + @SafeVarargs + @SuppressWarnings("unchecked") + public final TestSubscriber awaitAndAssertNextValues(T... values) { + final int expectedNum = values.length; + final List> expectations = new ArrayList<>(); + for (int i = 0; i < expectedNum; i++) { + final T expectedValue = values[i]; + expectations.add(actualValue -> { + if (!actualValue.equals(expectedValue)) { + throw new AssertionError(String.format("Expected Next signal: %s, but got: %s", expectedValue, actualValue)); + } + }); + } + awaitAndAssertNextValuesWith(expectations.toArray((Consumer[]) new Consumer[0])); + return this; + } + + /** + * Blocking method that waits until {@code n} next values have been received (n is the number of expectations + * provided) to assert them. + * + * @param expectations One or more methods that can verify the values and throw a exception (like an + * {@link AssertionError}) if the value is not valid. + * @return this + */ + @SafeVarargs + public final TestSubscriber awaitAndAssertNextValuesWith(Consumer... expectations) { + valuesStorage = true; + final int expectedValueCount = expectations.length; + await(valuesTimeout, () -> { + if (valuesStorage) { + return String.format("%d out of %d next values received within %d, " + "values : %s", + valueCount - nextValueAssertedCount, expectedValueCount, valuesTimeout.toMillis(), values.toString()); + } + return String.format("%d out of %d next values received within %d ms", valueCount - nextValueAssertedCount, + expectedValueCount, valuesTimeout.toMillis()); + }, () -> valueCount >= (nextValueAssertedCount + expectedValueCount)); + List nextValuesSnapshot; + List empty = new ArrayList<>(); + for (;;) { + nextValuesSnapshot = values; + if (NEXT_VALUES.compareAndSet(this, values, empty)) { + break; + } + } + if (nextValuesSnapshot.size() < expectedValueCount) { + throw new AssertionError(String.format("Expected %d number of signals but received %d", expectedValueCount, + nextValuesSnapshot.size())); + } + for (int i = 0; i < expectedValueCount; i++) { + Consumer consumer = expectations[i]; + T actualValue = nextValuesSnapshot.get(i); + consumer.accept(actualValue); + } + nextValueAssertedCount += expectedValueCount; + return this; + } + + // ============================================================================================================== + // Overrides + // ============================================================================================================== + + @Override + public void cancel() { + Subscription a = s; + if (a != Operators.cancelledSubscription()) { + a = S.getAndSet(this, Operators.cancelledSubscription()); + if (a != null && a != Operators.cancelledSubscription()) { + a.cancel(); + } + } + } + + @Override + public final boolean isCancelled() { + return s == Operators.cancelledSubscription(); + } + + @Override + public final boolean isStarted() { + return s != null; + } + + @Override + public final boolean isTerminated() { + return isCancelled(); + } + + @Override + public void onComplete() { + completionCount++; + cdl.countDown(); + } + + @Override + public void onError(Throwable t) { + errors.add(t); + cdl.countDown(); + } + + @Override + public void onNext(T t) { + if (establishedFusionMode == Fuseable.ASYNC) { + for (;;) { + t = qs.poll(); + if (t == null) { + break; + } + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (;;) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } else { + valueCount++; + if (valuesStorage) { + List nextValuesSnapshot; + for (;;) { + nextValuesSnapshot = values; + nextValuesSnapshot.add(t); + if (NEXT_VALUES.compareAndSet(this, nextValuesSnapshot, nextValuesSnapshot)) { + break; + } + } + } + } + } + + @Override + @SuppressWarnings("unchecked") + public void onSubscribe(Subscription s) { + subscriptionCount++; + int requestMode = requestedFusionMode; + if (requestMode >= 0) { + if (!setWithoutRequesting(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + } else { + if (s instanceof Fuseable.QueueSubscription) { + this.qs = (Fuseable.QueueSubscription) s; + + int m = qs.requestFusion(requestMode); + establishedFusionMode = m; + + if (m == Fuseable.SYNC) { + for (;;) { + T v = qs.poll(); + if (v == null) { + onComplete(); + break; + } + + onNext(v); + } + } else { + requestDeferred(); + } + } else { + requestDeferred(); + } + } + } else { + if (!set(s)) { + if (!isCancelled()) { + errors.add(new IllegalStateException("Subscription already set: " + subscriptionCount)); + } + } + } + } + + @Override + public void request(long n) { + if (Operators.validate(n)) { + if (establishedFusionMode != Fuseable.SYNC) { + normalRequest(n); + } + } + } + + @Override + public final long requestedFromDownstream() { + return requested; + } + + /** + * Setup what fusion mode should be requested from the incomining Subscription if it happens to be QueueSubscription + * + * @param requestMode the mode to request, see Fuseable constants + * @return this + */ + public final TestSubscriber requestedFusionMode(int requestMode) { + this.requestedFusionMode = requestMode; + return this; + } + + @Override + public Subscription upstream() { + return s; + } + + // ============================================================================================================== + // Non public methods + // ============================================================================================================== + + protected final void normalRequest(long n) { + Subscription a = s; + if (a != null) { + a.request(n); + } else { + Operators.addAndGet(REQUESTED, this, n); + + a = s; + + if (a != null) { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + a.request(r); + } + } + } + } + + /** + * Requests the deferred amount if not zero. + */ + protected final void requestDeferred() { + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + } + + /** + * Atomically sets the single subscription and requests the missed amount from it. + * + * @param s + * @return false if this arbiter is cancelled or there was a subscription already set + */ + protected final boolean set(Subscription s) { + Objects.requireNonNull(s, "s"); + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + + long r = REQUESTED.getAndSet(this, 0L); + + if (r != 0L) { + s.request(r); + } + + return true; + } + + a = this.s; + + if (a != Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + + Operators.reportSubscriptionSet(); + return false; + } + + /** + * Sets the Subscription once but does not request anything. + * + * @param s the Subscription to set + * @return true if successful, false if the current subscription is not null + */ + protected final boolean setWithoutRequesting(Subscription s) { + Objects.requireNonNull(s, "s"); + for (;;) { + Subscription a = this.s; + if (a == Operators.cancelledSubscription()) { + s.cancel(); + return false; + } + if (a != null) { + s.cancel(); + Operators.reportSubscriptionSet(); + return false; + } + + if (S.compareAndSet(this, null, s)) { + return true; + } + } + } + + /** + * Prepares and throws an AssertionError exception based on the message, cause, the active state and the potential + * errors so far. + * + * @param message the message + * @param cause the optional Throwable cause + * @throws AssertionError as expected + */ + protected final void assertionError(String message, Throwable cause) { + StringBuilder b = new StringBuilder(); + + if (cdl.getCount() != 0) { + b.append("(active) "); + } + b.append(message); + + List err = errors; + if (!err.isEmpty()) { + b.append(" (+ ").append(err.size()).append(" errors)"); + } + AssertionError e = new AssertionError(b.toString(), cause); + + for (Throwable t : err) { + e.addSuppressed(t); + } + + throw e; + } + + protected final String fusionModeName(int mode) { + switch (mode) { + case -1: + return "Disabled"; + case Fuseable.NONE: + return "None"; + case Fuseable.SYNC: + return "Sync"; + case Fuseable.ASYNC: + return "Async"; + default: + return "Unknown(" + mode + ")"; + } + } + + protected final String valueAndClass(Object o) { + if (o == null) { + return null; + } + return o + " (" + o.getClass().getSimpleName() + ")"; + } + +}