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 * Split a freeform string into a host and port, without strict validation. 142 * 143 * Note that the host-only formats will leave the port field undefined. You 144 * can use {@link #withDefaultPort(int)} to patch in a default value. 145 * 146 * @param hostPortString the input string to parse. 147 * @return if parsing was successful, a populated HostAndPort object. 148 * @throws IllegalArgumentException if nothing meaningful could be parsed. 149 */ 150 public static HostAndPort fromString(String hostPortString) { 151 checkNotNull(hostPortString); 152 String host; 153 String portString = null; 154 boolean hasBracketlessColons = false; 155 156 if (hostPortString.startsWith("[")) { 157 String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString); 158 host = hostAndPort[0]; 159 portString = hostAndPort[1]; 160 } else { 161 int colonPos = hostPortString.indexOf(':'); 162 if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) { 163 // Exactly 1 colon. Split into host:port. 164 host = hostPortString.substring(0, colonPos); 165 portString = hostPortString.substring(colonPos + 1); 166 } else { 167 // 0 or 2+ colons. Bare hostname or IPv6 literal. 168 host = hostPortString; 169 hasBracketlessColons = (colonPos >= 0); 170 } 171 } 172 173 int port = NO_PORT; 174 if (!Strings.isNullOrEmpty(portString)) { 175 // Try to parse the whole port string as a number. 176 // JDK7 accepts leading plus signs. We don't want to. 177 checkArgument(!portString.startsWith("+"), "Unparseable port number: %s", hostPortString); 178 try { 179 port = Integer.parseInt(portString); 180 } catch (NumberFormatException e) { 181 throw new IllegalArgumentException("Unparseable port number: " + hostPortString); 182 } 183 checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString); 184 } 185 186 return new HostAndPort(host, port, hasBracketlessColons); 187 } 188 189 /** 190 * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails. 191 * 192 * @param hostPortString the full bracketed host-port specification. Post might not be specified. 193 * @return an array with 2 strings: host and port, in that order. 194 * @throws IllegalArgumentException if parsing the bracketed host-port string fails. 195 */ 196 private static String[] getHostAndPortFromBracketedHost(String hostPortString) { 197 int colonIndex = 0; 198 int closeBracketIndex = 0; 199 boolean hasPort = false; 200 checkArgument(hostPortString.charAt(0) == '[', 201 "Bracketed host-port string must start with a bracket: %s", hostPortString); 202 colonIndex = hostPortString.indexOf(':'); 203 closeBracketIndex = hostPortString.lastIndexOf(']'); 204 checkArgument(colonIndex > -1 && closeBracketIndex > colonIndex, 205 "Invalid bracketed host/port: %s", hostPortString); 206 207 String host = hostPortString.substring(1, closeBracketIndex); 208 if (closeBracketIndex + 1 == hostPortString.length()) { 209 return new String[] { host, "" }; 210 } else { 211 checkArgument(hostPortString.charAt(closeBracketIndex + 1) == ':', 212 "Only a colon may follow a close bracket: %s", hostPortString); 213 for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) { 214 checkArgument(Character.isDigit(hostPortString.charAt(i)), 215 "Port must be numeric: %s", hostPortString); 216 } 217 return new String[] { host, hostPortString.substring(closeBracketIndex + 2) }; 218 } 219 } 220 221 /** 222 * Provide a default port if the parsed string contained only a host. 223 * 224 * You can chain this after {@link #fromString(String)} to include a port in 225 * case the port was omitted from the input string. If a port was already 226 * provided, then this method is a no-op. 227 * 228 * @param defaultPort a port number, from [0..65535] 229 * @return a HostAndPort instance, guaranteed to have a defined port. 230 */ 231 public HostAndPort withDefaultPort(int defaultPort) { 232 checkArgument(isValidPort(defaultPort)); 233 if (hasPort() || port == defaultPort) { 234 return this; 235 } 236 return new HostAndPort(host, defaultPort, hasBracketlessColons); 237 } 238 239 /** 240 * Generate an error if the host might be a non-bracketed IPv6 literal. 241 * 242 * <p>URI formatting requires that IPv6 literals be surrounded by brackets, 243 * like "[2001:db8::1]". Chain this call after {@link #fromString(String)} 244 * to increase the strictness of the parser, and disallow IPv6 literals 245 * that don't contain these brackets. 246 * 247 * <p>Note that this parser identifies IPv6 literals solely based on the 248 * presence of a colon. To perform actual validation of IP addresses, see 249 * the {@link InetAddresses#forString(String)} method. 250 * 251 * @return {@code this}, to enable chaining of calls. 252 * @throws IllegalArgumentException if bracketless IPv6 is detected. 253 */ 254 public HostAndPort requireBracketsForIPv6() { 255 checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host); 256 return this; 257 } 258 259 @Override 260 public boolean equals(@Nullable Object other) { 261 if (this == other) { 262 return true; 263 } 264 if (other instanceof HostAndPort) { 265 HostAndPort that = (HostAndPort) other; 266 return Objects.equal(this.host, that.host) 267 && this.port == that.port 268 && this.hasBracketlessColons == that.hasBracketlessColons; 269 } 270 return false; 271 } 272 273 @Override 274 public int hashCode() { 275 return Objects.hashCode(host, port, hasBracketlessColons); 276 } 277 278 /** Rebuild the host:port string, including brackets if necessary. */ 279 @Override 280 public String toString() { 281 StringBuilder builder = new StringBuilder(host.length() + 7); 282 if (host.indexOf(':') >= 0) { 283 builder.append('[').append(host).append(']'); 284 } else { 285 builder.append(host); 286 } 287 if (hasPort()) { 288 builder.append(':').append(port); 289 } 290 return builder.toString(); 291 } 292 293 /** Return true for valid port numbers. */ 294 private static boolean isValidPort(int port) { 295 return port >= 0 && port <= 65535; 296 } 297 298 private static final long serialVersionUID = 0; 299}