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 e04f45bac7..9eca99886f 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 @@ -61,6 +61,7 @@ import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** @@ -97,6 +98,7 @@ * @author Ruben Cervilla * @author Luis De Bello * @author Andrea Como + * @author Chris Bono */ public class LettuceConnectionFactory implements InitializingBean, DisposableBean, RedisConnectionFactory, ReactiveRedisConnectionFactory { @@ -279,6 +281,32 @@ public LettuceConnectionFactory(RedisClusterConfiguration clusterConfiguration, this.configuration = clusterConfiguration; } + /** + * Creates a {@link RedisConfiguration} based on a {@link RedisURI} according to the following: + * + * + * @param redisURI the connection info in the format of a RedisURI + * @return an appropriate {@link RedisConfiguration} instance representing the Redis URI. + */ + public static RedisConfiguration createRedisConfiguration(RedisURI redisURI) { + + Assert.notNull(redisURI, "RedisURI must not be null"); + + if (!ObjectUtils.isEmpty(redisURI.getSentinels())) { + return LettuceConverters.redisUriToSentinelConfiguration(redisURI); + } + + if (!ObjectUtils.isEmpty(redisURI.getSocket())) { + return LettuceConverters.redisUriToSocketConfiguration(redisURI); + } + + return LettuceConverters.redisUriToStandaloneConfiguration(redisURI); + } + /* * (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() 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 811893d7c6..e1740e68ab 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 @@ -33,14 +33,12 @@ import org.springframework.data.geo.Metric; import org.springframework.data.geo.Metrics; import org.springframework.data.geo.Point; -import org.springframework.data.redis.connection.BitFieldSubCommands; +import org.springframework.data.redis.connection.*; import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldGet; import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy; import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldIncrBy.Overflow; import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSet; import org.springframework.data.redis.connection.BitFieldSubCommands.BitFieldSubCommand; -import org.springframework.data.redis.connection.DefaultTuple; -import org.springframework.data.redis.connection.RedisClusterNode; import org.springframework.data.redis.connection.RedisClusterNode.Flag; import org.springframework.data.redis.connection.RedisClusterNode.LinkState; import org.springframework.data.redis.connection.RedisClusterNode.SlotRange; @@ -49,17 +47,10 @@ import org.springframework.data.redis.connection.RedisGeoCommands.GeoRadiusCommandArgs; import org.springframework.data.redis.connection.RedisListCommands.Direction; import org.springframework.data.redis.connection.RedisListCommands.Position; -import org.springframework.data.redis.connection.RedisNode; import org.springframework.data.redis.connection.RedisNode.NodeType; -import org.springframework.data.redis.connection.RedisPassword; -import org.springframework.data.redis.connection.RedisSentinelConfiguration; -import org.springframework.data.redis.connection.RedisServer; import org.springframework.data.redis.connection.RedisStringCommands.SetOption; -import org.springframework.data.redis.connection.RedisZSetCommands; import org.springframework.data.redis.connection.RedisZSetCommands.Range.Boundary; import org.springframework.data.redis.connection.RedisZSetCommands.Tuple; -import org.springframework.data.redis.connection.ReturnType; -import org.springframework.data.redis.connection.SortParameters; import org.springframework.data.redis.connection.SortParameters.Order; import org.springframework.data.redis.connection.convert.Converters; import org.springframework.data.redis.connection.convert.ListConverter; @@ -85,6 +76,7 @@ * @author Mark Paluch * @author Ninad Divadkar * @author dengliming + * @author Chris Bono */ public abstract class LettuceConverters extends Converters { @@ -535,6 +527,86 @@ public static RedisURI sentinelConfigurationToRedisURI(RedisSentinelConfiguratio return builder.build(); } + /** + * Converts a {@link RedisURI} to its corresponding {@link RedisSentinelConfiguration}. + * + * @param redisURI the uri containing the Redis Sentinel connection info + * @return a {@link RedisSentinelConfiguration} representing the Redis Sentinel information in the Redis URI. + * @since 2.6 + */ + static RedisSentinelConfiguration redisUriToSentinelConfiguration(RedisURI redisURI) { + + Assert.notNull(redisURI, "RedisURI is required"); + + RedisSentinelConfiguration sentinelConfiguration = new RedisSentinelConfiguration(); + if (!ObjectUtils.isEmpty(redisURI.getSentinelMasterId())) { + sentinelConfiguration.setMaster(redisURI.getSentinelMasterId()); + } + sentinelConfiguration.setDatabase(redisURI.getDatabase()); + + for (RedisURI sentinelNodeRedisUri : redisURI.getSentinels()) { + RedisNode sentinelNode = new RedisNode(sentinelNodeRedisUri.getHost(), sentinelNodeRedisUri.getPort()); + if (sentinelNodeRedisUri.getPassword() != null) { + sentinelConfiguration.setSentinelPassword(sentinelNodeRedisUri.getPassword()); + } + sentinelConfiguration.addSentinel(sentinelNode); + } + + applyAuthentication(redisURI, sentinelConfiguration); + + return sentinelConfiguration; + } + + /** + * Converts a {@link RedisURI} to its corresponding {@link RedisSocketConfiguration}. + * + * @param redisURI the uri containing the Redis connection info using a local unix domain socket + * @return a {@link RedisSocketConfiguration} representing the connection information in the Redis URI. + * @since 2.6 + */ + static RedisSocketConfiguration redisUriToSocketConfiguration(RedisURI redisURI) { + + Assert.notNull(redisURI, "RedisURI is required"); + + RedisSocketConfiguration socketConfiguration = new RedisSocketConfiguration(); + socketConfiguration.setSocket(redisURI.getSocket()); + socketConfiguration.setDatabase(redisURI.getDatabase()); + + applyAuthentication(redisURI, socketConfiguration); + + return socketConfiguration; + } + + /** + * Converts a {@link RedisURI} to its corresponding {@link RedisStandaloneConfiguration}. + * + * @param redisURI the uri containing the Redis connection info + * @return a {@link RedisStandaloneConfiguration} representing the connection information in the Redis URI. + * @since 2.6 + */ + static RedisStandaloneConfiguration redisUriToStandaloneConfiguration(RedisURI redisURI) { + + Assert.notNull(redisURI, "RedisURI is required"); + + RedisStandaloneConfiguration standaloneConfiguration = new RedisStandaloneConfiguration(); + standaloneConfiguration.setHostName(redisURI.getHost()); + standaloneConfiguration.setPort(redisURI.getPort()); + standaloneConfiguration.setDatabase(redisURI.getDatabase()); + + applyAuthentication(redisURI, standaloneConfiguration); + + return standaloneConfiguration; + } + + private static void applyAuthentication(RedisURI redisURI, RedisConfiguration.WithAuthentication redisConfiguration) { + if (StringUtils.hasText(redisURI.getUsername())) { + redisConfiguration.setUsername(redisURI.getUsername()); + } + if (redisURI.getPassword() != null) { + redisConfiguration.setPassword(redisURI.getPassword()); + } + } + public static byte[] toBytes(@Nullable String source) { if (source == null) { return null; diff --git a/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryCreateRedisConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryCreateRedisConfigurationUnitTests.java new file mode 100644 index 0000000000..e14f54df03 --- /dev/null +++ b/src/test/java/org/springframework/data/redis/connection/lettuce/LettuceConnectionFactoryCreateRedisConfigurationUnitTests.java @@ -0,0 +1,193 @@ +/* + * Copyright 2015-2021 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 + * + * https://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.assertj.core.api.Assertions.*; + +import io.lettuce.core.RedisURI; + +import org.assertj.core.api.recursive.comparison.RecursiveComparisonConfiguration; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.data.redis.connection.RedisConfiguration; +import org.springframework.data.redis.connection.RedisNode; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.RedisSocketConfiguration; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; + +/** + * Unit tests for the {@link LettuceConnectionFactory#createRedisConfiguration(RedisURI)} factory method. + * + * @author Chris Bono + */ +class LettuceConnectionFactoryCreateRedisConfigurationUnitTests { + + @Test + void requiresRedisURI() { + assertThatThrownBy(() -> LettuceConnectionFactory.createRedisConfiguration(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("RedisURI must not be null"); + } + + @Nested // GH-2117 + class CreateRedisSentinelConfiguration { + + @Test + void minimalFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.create("redis-sentinel://myserver?sentinelMasterId=5150"); + RedisSentinelConfiguration expectedSentinelConfiguration = new RedisSentinelConfiguration(); + expectedSentinelConfiguration.setMaster("5150"); + expectedSentinelConfiguration.addSentinel(new RedisNode("myserver", 26379)); + + RedisConfiguration sentinelConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(sentinelConfiguration).usingRecursiveComparison(sentinelCompareConfig()) + .isInstanceOf(RedisSentinelConfiguration.class) + .isEqualTo(expectedSentinelConfiguration); + } + + @Test + void allFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.create("redis-sentinel://fooUser:fooPass@myserver1:111,myserver2:222/7?sentinelMasterId=5150"); + // Set the passwords directly on the sentinels so that it gets picked up by converter + char[] sentinelPass = "changeme".toCharArray(); + redisURI.getSentinels().forEach(sentinelRedisUri -> sentinelRedisUri.setPassword(sentinelPass)); + RedisSentinelConfiguration expectedSentinelConfiguration = new RedisSentinelConfiguration(); + expectedSentinelConfiguration.setMaster("5150"); + expectedSentinelConfiguration.setDatabase(7); + expectedSentinelConfiguration.setUsername("fooUser"); + expectedSentinelConfiguration.setPassword("fooPass"); + expectedSentinelConfiguration.setSentinelPassword(sentinelPass); + expectedSentinelConfiguration.addSentinel(new RedisNode("myserver1", 111)); + expectedSentinelConfiguration.addSentinel(new RedisNode("myserver2", 222)); + + RedisConfiguration sentinelConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(sentinelConfiguration).usingRecursiveComparison(sentinelCompareConfig()) + .isInstanceOf(RedisSentinelConfiguration.class) + .isEqualTo(expectedSentinelConfiguration); + } + + // RedisSentinelConfiguration does not provide equals impl + private RecursiveComparisonConfiguration sentinelCompareConfig() { + return RecursiveComparisonConfiguration.builder().withComparedFields( + "master", + "username", + "password", + "sentinelPassword", + "database", + "sentinels") + .build(); + } + } + + @Nested // GH-2117 + class CreateRedisSocketConfiguration { + + @Test + void minimalFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.builder() + .socket("mysocket") + .build(); + RedisSocketConfiguration expectedSocketConfiguration = new RedisSocketConfiguration(); + expectedSocketConfiguration.setSocket("mysocket"); + + RedisConfiguration socketConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(socketConfiguration).usingRecursiveComparison(socketCompareConfig()) + .isInstanceOf(RedisSocketConfiguration.class) + .isEqualTo(expectedSocketConfiguration); + } + + @Test + void allFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.builder() + .socket("mysocket") + .withAuthentication("fooUser", "fooPass".toCharArray()) + .withDatabase(7) + .build(); + RedisSocketConfiguration expectedSocketConfiguration = new RedisSocketConfiguration(); + expectedSocketConfiguration.setSocket("mysocket"); + expectedSocketConfiguration.setUsername("fooUser"); + expectedSocketConfiguration.setPassword("fooPass"); + expectedSocketConfiguration.setDatabase(7); + + RedisConfiguration socketConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(socketConfiguration).usingRecursiveComparison(socketCompareConfig()) + .isInstanceOf(RedisSocketConfiguration.class) + .isEqualTo(expectedSocketConfiguration); + } + + // RedisSocketConfiguration does not provide equals impl + private RecursiveComparisonConfiguration socketCompareConfig() { + return RecursiveComparisonConfiguration.builder().withComparedFields( + "socket", + "username", + "password", + "database") + .build(); + } + } + + @Nested // GH-2117 + class CreateRedisStandaloneConfiguration { + + @Test + void minimalFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.create("redis://myserver"); + RedisStandaloneConfiguration expectedStandaloneConfiguration = new RedisStandaloneConfiguration(); + expectedStandaloneConfiguration.setHostName("myserver"); + + RedisConfiguration StandaloneConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(StandaloneConfiguration).usingRecursiveComparison(standaloneCompareConfig()) + .isInstanceOf(RedisStandaloneConfiguration.class) + .isEqualTo(expectedStandaloneConfiguration); + } + + @Test + void allFieldsSetOnRedisURI() { + RedisURI redisURI = RedisURI.create("redis://fooUser:fooPass@myserver1:111/7"); + RedisStandaloneConfiguration expectedStandaloneConfiguration = new RedisStandaloneConfiguration(); + expectedStandaloneConfiguration.setHostName("myserver1"); + expectedStandaloneConfiguration.setPort(111); + expectedStandaloneConfiguration.setDatabase(7); + expectedStandaloneConfiguration.setUsername("fooUser"); + expectedStandaloneConfiguration.setPassword("fooPass"); + + RedisConfiguration StandaloneConfiguration = LettuceConnectionFactory.createRedisConfiguration(redisURI); + + assertThat(StandaloneConfiguration).usingRecursiveComparison(standaloneCompareConfig()) + .isInstanceOf(RedisStandaloneConfiguration.class) + .isEqualTo(expectedStandaloneConfiguration); + + } + + // RedisStandaloneConfiguration does not provide equals impl + private RecursiveComparisonConfiguration standaloneCompareConfig() { + return RecursiveComparisonConfiguration.builder().withComparedFields( + "host", + "port", + "username", + "password", + "database") + .build(); + } + } + +}