package com.lambdaworks.redis.internal;
import com.lambdaworks.redis.LettuceStrings;
/**
* An immutable representation of a host and port.
*
* @author Mark Paluch
* @since 4.2
*/
public class HostAndPort {
private static final int NO_PORT = -1;
public final String hostText;
public final int port;
/**
*
* @param hostText must not be empty or {@literal null}.
* @param port
*/
private HostAndPort(String hostText, int port) {
LettuceAssert.notNull(hostText, "HostText must not be null");
this.hostText = hostText;
this.port = port;
}
/**
* Create a {@link HostAndPort} of {@code host} and {@code port}
*
* @param host the hostname
* @param port a valid port
* @return the {@link HostAndPort} of {@code host} and {@code port}
*/
public static HostAndPort of(String host, int port) {
LettuceAssert.isTrue(isValidPort(port), String.format("Port out of range: %s", port));
HostAndPort parsedHost = parse(host);
LettuceAssert.isTrue(!parsedHost.hasPort(), String.format("Host has a port: %s", host));
return new HostAndPort(host, port);
}
/**
* Parse a host and port string into a {@link HostAndPort}. The port is optional. Examples: {@code host:port} or
* {@code host}
*
* @param hostPortString
* @return
*/
public static HostAndPort parse(String hostPortString) {
LettuceAssert.notNull(hostPortString, "HostPortString 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 = NO_PORT;
if (!LettuceStrings.isEmpty(portString)) {
// Try to parse the whole port string as a number.
// JDK7 accepts leading plus signs. We don't want to.
LettuceAssert.isTrue(!portString.startsWith("+"), String.format("Unparseable port number: %s", hostPortString));
try {
port = Integer.parseInt(portString);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(String.format("Unparseable port number: %s" + hostPortString));
}
LettuceAssert.isTrue(isValidPort(port), String.format("Port number out of range: %s", hostPortString));
}
return new HostAndPort(host, port);
}
/**
* Temporary workaround until Redis provides IPv6 addresses in bracket notation. Allows parsing of {@code 1.2.3.4:6479} and
* {@code dead:beef:dead:beef:affe::1:6379} into host and port. We assume the last item after the colon is a port.
*
* @param hostAndPortPart the string containing the host and port
* @return the parsed {@link HostAndPort}.
*/
public static HostAndPort parseCompat(String hostAndPortPart) {
int firstColonIndex = hostAndPortPart.indexOf(':');
int lastColonIndex = hostAndPortPart.lastIndexOf(':');
int bracketIndex = hostAndPortPart.lastIndexOf(']');
if (firstColonIndex != lastColonIndex && lastColonIndex != -1 && bracketIndex == -1) {
String hostPart = hostAndPortPart.substring(0, lastColonIndex);
String portPart = hostAndPortPart.substring(lastColonIndex + 1);
return HostAndPort.of(hostPart, Integer.parseInt(portPart));
}
return HostAndPort.parse(hostAndPortPart);
}
/**
*
* @return {@literal true} if has a port.
*/
public boolean hasPort() {
return port != NO_PORT;
}
/**
*
* @return the host text.
*/
public String getHostText() {
return hostText;
}
/**
*
* @return the port.
*/
public int getPort() {
if (!hasPort()) {
throw new IllegalStateException("No port present.");
}
return port;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof HostAndPort))
return false;
HostAndPort that = (HostAndPort) o;
if (port != that.port)
return false;
return hostText != null ? hostText.equals(that.hostText) : that.hostText == null;
}
@Override
public int hashCode() {
int result = hostText != null ? hostText.hashCode() : 0;
result = 31 * result + port;
return result;
}
/**
* 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) {
LettuceAssert.isTrue(hostPortString.charAt(0) == '[',
String.format("Bracketed host-port string must start with a bracket: %s", hostPortString));
int colonIndex = hostPortString.indexOf(':');
int closeBracketIndex = hostPortString.lastIndexOf(']');
LettuceAssert.isTrue(colonIndex > -1 && closeBracketIndex > colonIndex,
String.format("Invalid bracketed host/port: ", hostPortString));
String host = hostPortString.substring(1, closeBracketIndex);
if (closeBracketIndex + 1 == hostPortString.length()) {
return new String[] { host, "" };
} else {
LettuceAssert.isTrue(hostPortString.charAt(closeBracketIndex + 1) == ':',
"Only a colon may follow a close bracket: " + hostPortString);
for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) {
LettuceAssert.isTrue(Character.isDigit(hostPortString.charAt(i)),
String.format("Port must be numeric: %s", hostPortString));
}
return new String[] { host, hostPortString.substring(closeBracketIndex + 2) };
}
}
/**
*
* @param port the port number
* @return {@literal true} for valid port numbers.
*/
private static boolean isValidPort(int port) {
return port >= 0 && port <= 65535;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(hostText);
if (hasPort()) {
sb.append(':').append(port);
}
return sb.toString();
}
}