diff --git a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java index 363e877d2..ff670a293 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisClusterConfiguration.java @@ -170,7 +170,7 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon private void appendClusterNodes(Set hostAndPorts) { for (String hostAndPort : hostAndPorts) { - addClusterNode(readHostAndPortFromString(hostAndPort)); + addClusterNode(RedisNode.fromString(hostAndPort)); } } @@ -228,15 +228,6 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon return result; } - private RedisNode readHostAndPortFromString(String hostAndPort) { - - String[] args = split(hostAndPort, ":"); - - Assert.notNull(args, "HostAndPort need to be seperated by ':'."); - Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port"); - return new RedisNode(args[0], Integer.valueOf(args[1])); - } - /** * @param clusterHostAndPorts must not be {@literal null} or empty. * @param redirects the max number of redirects to follow. @@ -256,4 +247,5 @@ public class RedisClusterConfiguration implements RedisConfiguration, ClusterCon return map; } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisNode.java b/src/main/java/org/springframework/data/redis/connection/RedisNode.java index 5dbf894f4..a7b0e5b44 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisNode.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisNode.java @@ -61,6 +61,95 @@ public class RedisNode implements NamedNode { this.masterId = redisNode.masterId; } + /** + * Parse a {@code hostAndPort} string into {@link RedisNode}. Supports IPv4, IPv6, and hostname notations including + * the port. For example: + * + *
+	 * RedisNode.fromString("127.0.0.1:6379");
+	 * RedisNode.fromString("[aaaa:bbbb::dddd:eeee]:6379");
+	 * RedisNode.fromString("my.redis.server:6379");
+	 * 
+ * + * @param hostPortString must not be {@literal null} or empty. + * @return the parsed {@link RedisNode}. + * @since 2.7.4 + */ + public static RedisNode fromString(String hostPortString) { + + Assert.notNull(hostPortString, "HostAndPort must not be null"); + + String host; + String portString = null; + + if (hostPortString.startsWith("[")) { + String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString); + host = hostAndPort[0]; + portString = hostAndPort[1]; + } else { + int colonPos = hostPortString.indexOf(':'); + if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) { + // Exactly 1 colon. Split into host:port. + host = hostPortString.substring(0, colonPos); + portString = hostPortString.substring(colonPos + 1); + } else { + // 0 or 2+ colons. Bare hostname or IPv6 literal. + host = hostPortString; + } + } + + int port = -1; + try { + port = Integer.parseInt(portString); + } catch (RuntimeException e) { + throw new IllegalArgumentException(String.format("Unparseable port number: %s", hostPortString)); + } + + if (!isValidPort(port)) { + throw new IllegalArgumentException(String.format("Port number out of range: %s", hostPortString)); + } + + return new RedisNode(host, port); + } + + /** + * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails. + * + * @param hostPortString the full bracketed host-port specification. Post might not be specified. + * @return an array with 2 strings: host and port, in that order. + * @throws IllegalArgumentException if parsing the bracketed host-port string fails. + */ + private static String[] getHostAndPortFromBracketedHost(String hostPortString) { + + if (hostPortString.charAt(0) != '[') { + throw new IllegalArgumentException( + String.format("Bracketed host-port string must start with a bracket: %s", hostPortString)); + } + + int colonIndex = hostPortString.indexOf(':'); + int closeBracketIndex = hostPortString.lastIndexOf(']'); + + if (!(colonIndex > -1 && closeBracketIndex > colonIndex)) { + throw new IllegalArgumentException(String.format("Invalid bracketed host/port: %s", hostPortString)); + } + + String host = hostPortString.substring(1, closeBracketIndex); + if (closeBracketIndex + 1 == hostPortString.length()) { + return new String[] { host, "" }; + } else { + if (!(hostPortString.charAt(closeBracketIndex + 1) == ':')) { + throw new IllegalArgumentException( + String.format("Only a colon may follow a close bracket: %s", hostPortString)); + } + for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) { + if (!Character.isDigit(hostPortString.charAt(i))) { + throw new IllegalArgumentException(String.format("Port must be numeric: %s", hostPortString)); + } + } + return new String[] { host, hostPortString.substring(closeBracketIndex + 2) }; + } + } + /** * @return can be {@literal null}. */ @@ -86,6 +175,11 @@ public class RedisNode implements NamedNode { } public String asString() { + + if (host != null && host.contains(":")) { + return "[" + host + "]:" + port; + } + return host + ":" + port; } @@ -294,4 +388,8 @@ public class RedisNode implements NamedNode { } } + private static boolean isValidPort(int port) { + return port >= 0 && port <= 65535; + } + } diff --git a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java index 6bc3d676d..68fcc918a 100644 --- a/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java +++ b/src/main/java/org/springframework/data/redis/connection/RedisSentinelConfiguration.java @@ -200,7 +200,7 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC private void appendSentinels(Set hostAndPorts) { for (String hostAndPort : hostAndPorts) { - addSentinel(readHostAndPortFromString(hostAndPort)); + addSentinel(RedisNode.fromString(hostAndPort)); } } @@ -306,15 +306,6 @@ public class RedisSentinelConfiguration implements RedisConfiguration, SentinelC return result; } - private RedisNode readHostAndPortFromString(String hostAndPort) { - - String[] args = split(hostAndPort, ":"); - - Assert.notNull(args, "HostAndPort need to be separated by ':'"); - Assert.isTrue(args.length == 2, "Host and Port String needs to specified as host:port"); - return new RedisNode(args[0], Integer.valueOf(args[1]).intValue()); - } - /** * @param master must not be {@literal null} or empty. * @param sentinelHostAndPorts must not be {@literal null}. diff --git a/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java index 9950f2bfc..4ab3fc603 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisClusterConfigurationUnitTests.java @@ -28,6 +28,8 @@ import org.springframework.mock.env.MockPropertySource; import org.springframework.util.StringUtils; /** + * Unit tests for {@link RedisClusterConfiguration}. + * * @author Christoph Strobl * @author Mark Paluch */ @@ -48,12 +50,22 @@ class RedisClusterConfigurationUnitTests { assertThat(config.getMaxRedirects()).isNull(); } + @Test // GH-2418 + void shouldCreateRedisClusterConfigurationForIPV6Correctly() { + + RedisClusterConfiguration config = new RedisClusterConfiguration(Collections.singleton("[aaa:bbb:ccc::dd1]:123")); + + assertThat(config.getClusterNodes().size()).isEqualTo(1); + assertThat(config.getClusterNodes()).contains(new RedisNode("aaa:bbb:ccc::dd1", 123)); + assertThat(config.getClusterNodes()).first().hasToString("[aaa:bbb:ccc::dd1]:123"); + assertThat(config.getMaxRedirects()).isNull(); + } + @Test // DATAREDIS-315 void shouldCreateRedisClusterConfigurationCorrectlyGivenMultipleHostAndPortStrings() { RedisClusterConfiguration config = new RedisClusterConfiguration( - new HashSet<>(Arrays.asList(HOST_AND_PORT_1, - HOST_AND_PORT_2, HOST_AND_PORT_3))); + new HashSet<>(Arrays.asList(HOST_AND_PORT_1, HOST_AND_PORT_2, HOST_AND_PORT_3))); assertThat(config.getClusterNodes().size()).isEqualTo(3); assertThat(config.getClusterNodes()).contains(new RedisNode("127.0.0.1", 123), new RedisNode("localhost", 456), diff --git a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java index fc7ae87d5..b5bce7049 100644 --- a/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java +++ b/src/test/java/org/springframework/data/redis/connection/RedisSentinelConfigurationUnitTests.java @@ -30,6 +30,7 @@ import org.springframework.util.StringUtils; * Unit tests for {@link RedisSentinelConfiguration}. * * @author Christoph Strobl + * @author Mark Paluch * @author Vikas Garg */ class RedisSentinelConfigurationUnitTests { @@ -49,6 +50,16 @@ class RedisSentinelConfigurationUnitTests { assertThat(config.getSentinels()).contains(new RedisNode("127.0.0.1", 123)); } + @Test // GH-2418 + void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndSingleIPV6HostAndPortString() { + + RedisSentinelConfiguration config = new RedisSentinelConfiguration("mymaster", + Collections.singleton("[ca:fee::1]:123")); + + assertThat(config.getSentinels()).hasSize(1); + assertThat(config.getSentinels()).contains(new RedisNode("ca:fee::1", 123)); + } + @Test // DATAREDIS-372 void shouldCreateRedisSentinelConfigurationCorrectlyGivenMasterAndMultipleHostAndPortStrings() {