2020import java .time .Duration ;
2121import java .util .*;
2222import java .util .concurrent .TimeUnit ;
23- import java .util .regex .Matcher ;
24- import java .util .regex .Pattern ;
2523
2624import org .apache .commons .logging .Log ;
2725import org .apache .commons .logging .LogFactory ;
26+
2827import org .springframework .core .convert .converter .Converter ;
2928import org .springframework .data .geo .Distance ;
3029import org .springframework .data .geo .GeoResult ;
4544import org .springframework .data .redis .connection .zset .Tuple ;
4645import org .springframework .data .redis .serializer .RedisSerializer ;
4746import org .springframework .data .redis .util .ByteUtils ;
47+ import org .springframework .lang .NonNull ;
4848import org .springframework .lang .Nullable ;
4949import org .springframework .util .Assert ;
5050import org .springframework .util .ClassUtils ;
@@ -545,10 +545,15 @@ enum ClusterNodesConverter implements Converter<String, RedisClusterNode> {
545545 * <li>{@code %s:%i} (Redis 3)</li>
546546 * <li>{@code %s:%i@%i} (Redis 4, with bus port)</li>
547547 * <li>{@code %s:%i@%i,%s} (Redis 7, with announced hostname)</li>
548+ *
549+ * The output of the {@code CLUSTER NODES } command is just a space-separated CSV string, where each
550+ * line represents a node in the cluster. The following is an example of output on Redis 7.2.0.
551+ * You can check the latest <a href="https://redis.io/docs/latest/commands/cluster-nodes/">here</a>.
552+ *
553+ * {@code <id> <ip:port@cport[,hostname]> <flags> <master> <ping-sent> <pong-recv> <config-epoch> <link-state> <slot> <slot> ... <slot>}
554+ *
548555 * </ul>
549556 */
550- static final Pattern clusterEndpointPattern = Pattern
551- .compile ("\\ [?([0-9a-zA-Z\\ -_\\ .:]*)\\ ]?:([0-9]+)(?:@[0-9]+(?:,([^,].*))?)?" );
552557private static final Map <String , Flag > flagLookupMap ;
553558
554559static {
@@ -567,18 +572,75 @@ enum ClusterNodesConverter implements Converter<String, RedisClusterNode> {
567572static final int LINK_STATE_INDEX = 7 ;
568573static final int SLOTS_INDEX = 8 ;
569574
575+ record AddressPortHostname (String addressPart , String portPart , @ Nullable String hostnamePart ) {
576+
577+ static AddressPortHostname of (String [] args ) {
578+ Assert .isTrue (args .length >= HOST_PORT_INDEX + 1 , "ClusterNode information does not define host and port" );
579+ // <ip:port@cport[,hostname]>
580+ String hostPort = args [HOST_PORT_INDEX ];
581+ int lastColon = hostPort .lastIndexOf (":" );
582+ Assert .isTrue (lastColon != -1 , "ClusterNode information does not define host and port" );
583+ String addressPart = getAddressPart (hostPort , lastColon );
584+ // Everything to the right of port
585+ int indexOfColon = hostPort .indexOf ("," );
586+ boolean hasColon = indexOfColon != -1 ;
587+ String hostnamePart = getHostnamePart (hasColon , hostPort , indexOfColon );
588+ String portPart = getPortPart (hostPort , lastColon , hasColon , indexOfColon );
589+ return new AddressPortHostname (addressPart , portPart , hostnamePart );
590+ }
591+
592+ @ NonNull private static String getAddressPart (String hostPort , int lastColon ) {
593+ // Everything to the left of port
594+ // 127.0.0.1:6380
595+ // 127.0.0.1:6380@6381
596+ // :6380
597+ // :6380@6381
598+ // 2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380
599+ // 2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380@6381
600+ // 127.0.0.1:6380,hostname1
601+ // 127.0.0.1:6380@6381,hostname1
602+ // :6380,hostname1
603+ // :6380@6381,hostname1
604+ // 2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380,hostname1
605+ // 2a02:6b8:c67:9c:0:6d8b:33da:5a2c:6380@6381,hostname1
606+ String addressPart = hostPort .substring (0 , lastColon );
607+ // [2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380
608+ // [2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380@6381
609+ // [2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380,hostname1
610+ // [2a02:6b8:c67:9c:0:6d8b:33da:5a2c]:6380@6381,hostname1
611+ if (addressPart .startsWith ("[" ) && addressPart .endsWith ("]" )) {
612+ addressPart = addressPart .substring (1 , addressPart .length () - 1 );
613+ }
614+ return addressPart ;
615+ }
616+
617+ @ Nullable
618+ private static String getHostnamePart (boolean hasColon , String hostPort , int indexOfColon ) {
619+ // Everything to the right starting from comma
620+ String hostnamePart = hasColon ? hostPort .substring (indexOfColon + 1 ) : null ;
621+ return StringUtils .hasText (hostnamePart ) ? hostnamePart : null ;
622+ }
623+
624+ @ NonNull private static String getPortPart (String hostPort , int lastColon , boolean hasColon , int indexOfColon ) {
625+ String portPart = hostPort .substring (lastColon + 1 );
626+ if (portPart .contains ("@" )) {
627+ portPart = portPart .substring (0 , portPart .indexOf ("@" ));
628+ } else if (hasColon ) {
629+ portPart = portPart .substring (0 , indexOfColon );
630+ }
631+ return portPart ;
632+ }
633+ }
634+
570635@ Override
571636public RedisClusterNode convert (String source ) {
572637
573638String [] args = source .split (" " );
574639
575- Matcher matcher = clusterEndpointPattern .matcher (args [HOST_PORT_INDEX ]);
576-
577- Assert .isTrue (matcher .matches (), "ClusterNode information does not define host and port" );
578-
579- String addressPart = matcher .group (1 );
580- String portPart = matcher .group (2 );
581- String hostnamePart = matcher .group (3 );
640+ AddressPortHostname addressPortHostname = AddressPortHostname .of (args );
641+ String addressPart = addressPortHostname .addressPart ;
642+ String portPart = addressPortHostname .portPart ;
643+ String hostnamePart = addressPortHostname .hostnamePart ;
582644
583645SlotRange range = parseSlotRange (args );
584646Set <Flag > flags = parseFlags (args );
0 commit comments