001/* 002 * Copyright (C) 2011 The Guava Authors 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016 017package com.google.common.net; 018 019import static com.google.common.base.Preconditions.checkArgument; 020import static com.google.common.base.Preconditions.checkNotNull; 021import static com.google.common.base.Preconditions.checkState; 022 023import com.google.common.annotations.Beta; 024import com.google.common.annotations.GwtCompatible; 025import com.google.common.base.Objects; 026import com.google.common.base.Strings; 027 028import java.io.Serializable; 029 030import javax.annotation.Nullable; 031import javax.annotation.concurrent.Immutable; 032 033/** 034 * An immutable representation of a host and port. 035 * 036 * <p>Example usage: 037 * <pre> 038 * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]") 039 * .withDefaultPort(80) 040 * .requireBracketsForIPv6(); 041 * hp.getHostText(); // returns "2001:db8::1" 042 * hp.getPort(); // returns 80 043 * hp.toString(); // returns "[2001:db8::1]:80" 044 * </pre> 045 * 046 * <p>Here are some examples of recognized formats: 047 * <ul> 048 * <li>example.com 049 * <li>example.com:80 050 * <li>192.0.2.1 051 * <li>192.0.2.1:80 052 * <li>[2001:db8::1] - {@link #getHostText()} omits brackets 053 * <li>[2001:db8::1]:80 - {@link #getHostText()} omits brackets 054 * <li>2001:db8::1 - Use {@link #requireBracketsForIPv6()} to prohibit this 055 * </ul> 056 * 057 * <p>Note that this is not an exhaustive list, because these methods are only 058 * concerned with brackets, colons, and port numbers. Full validation of the 059 * host field (if desired) is the caller's responsibility. 060 * 061 * @author Paul Marks 062 * @since 10.0 063 */ 064@Beta 065@Immutable 066@GwtCompatible 067public final class HostAndPort implements Serializable { 068 /** Magic value indicating the absence of a port number. */ 069 private static final int NO_PORT = -1; 070 071 /** Hostname, IPv4/IPv6 literal, or unvalidated nonsense. */ 072 private final String host; 073 074 /** Validated port number in the range [0..65535], or NO_PORT */ 075 private final int port; 076 077 /** True if the parsed host has colons, but no surrounding brackets. */ 078 private final boolean hasBracketlessColons; 079 080 private HostAndPort(String host, int port, boolean hasBracketlessColons) { 081 this.host = host; 082 this.port = port; 083 this.hasBracketlessColons = hasBracketlessColons; 084 } 085 086 /** 087 * Returns the portion of this {@code HostAndPort} instance that should 088 * represent the hostname or IPv4/IPv6 literal. 089 * 090 * <p>A successful parse does not imply any degree of sanity in this field. 091 * For additional validation, see the {@link HostSpecifier} class. 092 */ 093 public String getHostText() { 094 return host; 095 } 096 097 /** Return true if this instance has a defined port. */ 098 public boolean hasPort() { 099 return port >= 0; 100 } 101 102 /** 103 * Get the current port number, failing if no port is defined. 104 * 105 * @return a validated port number, in the range [0..65535] 106 * @throws IllegalStateException if no port is defined. You can use 107 * {@link #withDefaultPort(int)} to prevent this from occurring. 108 */ 109 public int getPort() { 110 checkState(hasPort()); 111 return port; 112 } 113 114 /** 115 * Returns the current port number, with a default if no port is defined. 116 */ 117 public int getPortOrDefault(int defaultPort) { 118 return hasPort() ? port : defaultPort; 119 } 120 121 /** 122 * Build a HostAndPort instance from separate host and port values. 123 * 124 * <p>Note: Non-bracketed IPv6 literals are allowed. 125 * Use {@link #requireBracketsForIPv6()} to prohibit these. 126 * 127 * @param host the host string to parse. Must not contain a port number. 128 * @param port a port number from [0..65535] 129 * @return if parsing was successful, a populated HostAndPort object. 130 * @throws IllegalArgumentException if {@code host} contains a port number, 131 * or {@code port} is out of range. 132 */ 133 public static HostAndPort fromParts(String host, int port) { 134 checkArgument(isValidPort(port), "Port out of range: %s", port); 135 HostAndPort parsedHost = fromString(host); 136 checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host); 137 return new HostAndPort(parsedHost.host, port, parsedHost.hasBracketlessColons); 138 } 139 140 /** 141 * Build a HostAndPort instance from a host only. 142 * 143 * <p>Note: Non-bracketed IPv6 literals are allowed. 144 * Use {@link #requireBracketsForIPv6()} to prohibit these. 145 * 146 * @param host the host-only string to parse. Must not contain a port number. 147 * @return if parsing was successful, a populated HostAndPort object. 148 * @throws IllegalArgumentException if {@code host} contains a port number. 149 * @since 17.0 150 */ 151 public static HostAndPort fromHost(String host) { 152 HostAndPort parsedHost = fromString(host); 153 checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host); 154 return parsedHost; 155 } 156 157 /** 158 * Split a freeform string into a host and port, without strict validation. 159 * 160 * Note that the host-only formats will leave the port field undefined. You 161 * can use {@link #withDefaultPort(int)} to patch in a default value. 162 * 163 * @param hostPortString the input string to parse. 164 * @return if parsing was successful, a populated HostAndPort object. 165 * @throws IllegalArgumentException if nothing meaningful could be parsed. 166 */ 167 public static HostAndPort fromString(String hostPortString) { 168 checkNotNull(hostPortString); 169 String host; 170 String portString = null; 171 boolean hasBracketlessColons = false; 172 173 if (hostPortString.startsWith("[")) { 174 String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString); 175 host = hostAndPort[0]; 176 portString = hostAndPort[1]; 177 } else { 178 int colonPos = hostPortString.indexOf(':'); 179 if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) { 180 // Exactly 1 colon. Split into host:port. 181 host = hostPortString.substring(0, colonPos); 182 portString = hostPortString.substring(colonPos + 1); 183 } else { 184 // 0 or 2+ colons. Bare hostname or IPv6 literal. 185 host = hostPortString; 186 hasBracketlessColons = (colonPos >= 0); 187 } 188 } 189 190 int port = NO_PORT; 191 if (!Strings.isNullOrEmpty(portString)) { 192 // Try to parse the whole port string as a number. 193 // JDK7 accepts leading plus signs. We don't want to. 194 checkArgument(!portString.startsWith("+"), "Unparseable port number: %s", hostPortString); 195 try { 196 port = Integer.parseInt(portString); 197 } catch (NumberFormatException e) { 198 throw new IllegalArgumentException("Unparseable port number: " + hostPortString); 199 } 200 checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString); 201 } 202 203 return new HostAndPort(host, port, hasBracketlessColons); 204 } 205 206 /** 207 * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails. 208 * 209 * @param hostPortString the full bracketed host-port specification. Post might not be specified. 210 * @return an array with 2 strings: host and port, in that order. 211 * @throws IllegalArgumentException if parsing the bracketed host-port string fails. 212 */ 213 private static String[] getHostAndPortFromBracketedHost(String hostPortString) { 214 int colonIndex = 0; 215 int closeBracketIndex = 0; 216 checkArgument(hostPortString.charAt(0) == '[', 217 "Bracketed host-port string must start with a bracket: %s", hostPortString); 218 colonIndex = hostPortString.indexOf(':'); 219 closeBracketIndex = hostPortString.lastIndexOf(']'); 220 checkArgument(colonIndex > -1 && closeBracketIndex > colonIndex, 221 "Invalid bracketed host/port: %s", hostPortString); 222 223 String host = hostPortString.substring(1, closeBracketIndex); 224 if (closeBracketIndex + 1 == hostPortString.length()) { 225 return new String[] { host, "" }; 226 } else { 227 checkArgument(hostPortString.charAt(closeBracketIndex + 1) == ':', 228 "Only a colon may follow a close bracket: %s", hostPortString); 229 for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) { 230 checkArgument(Character.isDigit(hostPortString.charAt(i)), 231 "Port must be numeric: %s", hostPortString); 232 } 233 return new String[] { host, hostPortString.substring(closeBracketIndex + 2) }; 234 } 235 } 236 237 /** 238 * Provide a default port if the parsed string contained only a host. 239 * 240 * You can chain this after {@link #fromString(String)} to include a port in 241 * case the port was omitted from the input string. If a port was already 242 * provided, then this method is a no-op. 243 * 244 * @param defaultPort a port number, from [0..65535] 245 * @return a HostAndPort instance, guaranteed to have a defined port. 246 */ 247 public HostAndPort withDefaultPort(int defaultPort) { 248 checkArgument(isValidPort(defaultPort)); 249 if (hasPort() || port == defaultPort) { 250 return this; 251 } 252 return new HostAndPort(host, defaultPort, hasBracketlessColons); 253 } 254 255 /** 256 * Generate an error if the host might be a non-bracketed IPv6 literal. 257 * 258 * <p>URI formatting requires that IPv6 literals be surrounded by brackets, 259 * like "[2001:db8::1]". Chain this call after {@link #fromString(String)} 260 * to increase the strictness of the parser, and disallow IPv6 literals 261 * that don't contain these brackets. 262 * 263 * <p>Note that this parser identifies IPv6 literals solely based on the 264 * presence of a colon. To perform actual validation of IP addresses, see 265 * the {@link InetAddresses#forString(String)} method. 266 * 267 * @return {@code this}, to enable chaining of calls. 268 * @throws IllegalArgumentException if bracketless IPv6 is detected. 269 */ 270 public HostAndPort requireBracketsForIPv6() { 271 checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host); 272 return this; 273 } 274 275 @Override 276 public boolean equals(@Nullable Object other) { 277 if (this == other) { 278 return true; 279 } 280 if (other instanceof HostAndPort) { 281 HostAndPort that = (HostAndPort) other; 282 return Objects.equal(this.host, that.host) 283 && this.port == that.port 284 && this.hasBracketlessColons == that.hasBracketlessColons; 285 } 286 return false; 287 } 288 289 @Override 290 public int hashCode() { 291 return Objects.hashCode(host, port, hasBracketlessColons); 292 } 293 294 /** Rebuild the host:port string, including brackets if necessary. */ 295 @Override 296 public String toString() { 297 // "[]:12345" requires 8 extra bytes. 298 StringBuilder builder = new StringBuilder(host.length() + 8); 299 if (host.indexOf(':') >= 0) { 300 builder.append('[').append(host).append(']'); 301 } else { 302 builder.append(host); 303 } 304 if (hasPort()) { 305 builder.append(':').append(port); 306 } 307 return builder.toString(); 308 } 309 310 /** Return true for valid port numbers. */ 311 private static boolean isValidPort(int port) { 312 return port >= 0 && port <= 65535; 313 } 314 315 private static final long serialVersionUID = 0; 316}