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