View Javadoc
1   /*
2    * Copyright (C) 2011 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5    * in compliance with the License. You may obtain a copy of the License at
6    *
7    * http://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software distributed under the License
10   * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
11   * or implied. See the License for the specific language governing permissions and limitations under
12   * the License.
13   */
14  
15  package com.google.common.net;
16  
17  import static com.google.common.base.Preconditions.checkArgument;
18  import static com.google.common.base.Preconditions.checkNotNull;
19  import static com.google.common.base.Preconditions.checkState;
20  
21  import com.google.common.annotations.Beta;
22  import com.google.common.annotations.GwtCompatible;
23  import com.google.common.base.Objects;
24  import com.google.common.base.Strings;
25  import java.io.Serializable;
26  import javax.annotation.Nullable;
27  import javax.annotation.concurrent.Immutable;
28  
29  /**
30   * An immutable representation of a host and port.
31   *
32   * <p>Example usage:
33   *
34   * <pre>
35   * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]")
36   *     .withDefaultPort(80)
37   *     .requireBracketsForIPv6();
38   * hp.getHost();   // returns "2001:db8::1"
39   * hp.getPort();   // returns 80
40   * hp.toString();  // returns "[2001:db8::1]:80"
41   * </pre>
42   *
43   * <p>Here are some examples of recognized formats:
44   * <ul>
45   * <li>example.com
46   * <li>example.com:80
47   * <li>192.0.2.1
48   * <li>192.0.2.1:80
49   * <li>[2001:db8::1] - {@link #getHost()} omits brackets
50   * <li>[2001:db8::1]:80 - {@link #getHost()} omits brackets
51   * <li>2001:db8::1 - Use {@link #requireBracketsForIPv6()} to prohibit this
52   * </ul>
53   *
54   * <p>Note that this is not an exhaustive list, because these methods are only concerned with
55   * brackets, colons, and port numbers. Full validation of the host field (if desired) is the
56   * caller's responsibility.
57   *
58   * @author Paul Marks
59   * @since 10.0
60   */
61  @Beta
62  @Immutable
63  @GwtCompatible
64  public final class HostAndPort implements Serializable {
65    /** Magic value indicating the absence of a port number. */
66    private static final int NO_PORT = -1;
67  
68    /** Hostname, IPv4/IPv6 literal, or unvalidated nonsense. */
69    private final String host;
70  
71    /** Validated port number in the range [0..65535], or NO_PORT */
72    private final int port;
73  
74    /** True if the parsed host has colons, but no surrounding brackets. */
75    private final boolean hasBracketlessColons;
76  
77    private HostAndPort(String host, int port, boolean hasBracketlessColons) {
78      this.host = host;
79      this.port = port;
80      this.hasBracketlessColons = hasBracketlessColons;
81    }
82  
83    /**
84     * Returns the portion of this {@code HostAndPort} instance that should represent the hostname or
85     * IPv4/IPv6 literal.
86     *
87     * <p>A successful parse does not imply any degree of sanity in this field. For additional
88     * validation, see the {@link HostSpecifier} class.
89     *
90     * @since 20.0 (since 10.0 as {@code getHostText})
91     */
92    public String getHost() {
93      return host;
94    }
95  
96    /** Return true if this instance has a defined port. */
97    public boolean hasPort() {
98      return port >= 0;
99    }
100 
101   /**
102    * Get the current port number, failing if no port is defined.
103    *
104    * @return a validated port number, in the range [0..65535]
105    * @throws IllegalStateException if no port is defined. You can use {@link #withDefaultPort(int)}
106    *     to prevent this from occurring.
107    */
108   public int getPort() {
109     checkState(hasPort());
110     return port;
111   }
112 
113   /**
114    * Returns the current port number, with a default if no port is defined.
115    */
116   public int getPortOrDefault(int defaultPort) {
117     return hasPort() ? port : defaultPort;
118   }
119 
120   /**
121    * Build a HostAndPort instance from separate host and port values.
122    *
123    * <p>Note: Non-bracketed IPv6 literals are allowed. Use {@link #requireBracketsForIPv6()} to
124    * prohibit these.
125    *
126    * @param host the host string to parse. Must not contain a port number.
127    * @param port a port number from [0..65535]
128    * @return if parsing was successful, a populated HostAndPort object.
129    * @throws IllegalArgumentException if {@code host} contains a port number, or {@code port} is out
130    *     of range.
131    */
132   public static HostAndPort fromParts(String host, int port) {
133     checkArgument(isValidPort(port), "Port out of range: %s", port);
134     HostAndPort parsedHost = fromString(host);
135     checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host);
136     return new HostAndPort(parsedHost.host, port, parsedHost.hasBracketlessColons);
137   }
138 
139   /**
140    * Build a HostAndPort instance from a host only.
141    *
142    * <p>Note: Non-bracketed IPv6 literals are allowed. Use {@link #requireBracketsForIPv6()} to
143    * prohibit these.
144    *
145    * @param host the host-only string to parse. Must not contain a port number.
146    * @return if parsing was successful, a populated HostAndPort object.
147    * @throws IllegalArgumentException if {@code host} contains a port number.
148    * @since 17.0
149    */
150   public static HostAndPort fromHost(String host) {
151     HostAndPort parsedHost = fromString(host);
152     checkArgument(!parsedHost.hasPort(), "Host has a port: %s", host);
153     return parsedHost;
154   }
155 
156   /**
157    * Split a freeform string into a host and port, without strict validation.
158    *
159    * Note that the host-only formats will leave the port field undefined. You can use
160    * {@link #withDefaultPort(int)} to patch in a default value.
161    *
162    * @param hostPortString the input string to parse.
163    * @return if parsing was successful, a populated HostAndPort object.
164    * @throws IllegalArgumentException if nothing meaningful could be parsed.
165    */
166   public static HostAndPort fromString(String hostPortString) {
167     checkNotNull(hostPortString);
168     String host;
169     String portString = null;
170     boolean hasBracketlessColons = false;
171 
172     if (hostPortString.startsWith("[")) {
173       String[] hostAndPort = getHostAndPortFromBracketedHost(hostPortString);
174       host = hostAndPort[0];
175       portString = hostAndPort[1];
176     } else {
177       int colonPos = hostPortString.indexOf(':');
178       if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) {
179         // Exactly 1 colon. Split into host:port.
180         host = hostPortString.substring(0, colonPos);
181         portString = hostPortString.substring(colonPos + 1);
182       } else {
183         // 0 or 2+ colons. Bare hostname or IPv6 literal.
184         host = hostPortString;
185         hasBracketlessColons = (colonPos >= 0);
186       }
187     }
188 
189     int port = NO_PORT;
190     if (!Strings.isNullOrEmpty(portString)) {
191       // Try to parse the whole port string as a number.
192       // JDK7 accepts leading plus signs. We don't want to.
193       checkArgument(!portString.startsWith("+"), "Unparseable port number: %s", hostPortString);
194       try {
195         port = Integer.parseInt(portString);
196       } catch (NumberFormatException e) {
197         throw new IllegalArgumentException("Unparseable port number: " + hostPortString);
198       }
199       checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString);
200     }
201 
202     return new HostAndPort(host, port, hasBracketlessColons);
203   }
204 
205   /**
206    * Parses a bracketed host-port string, throwing IllegalArgumentException if parsing fails.
207    *
208    * @param hostPortString the full bracketed host-port specification. Post might not be specified.
209    * @return an array with 2 strings: host and port, in that order.
210    * @throws IllegalArgumentException if parsing the bracketed host-port string fails.
211    */
212   private static String[] getHostAndPortFromBracketedHost(String hostPortString) {
213     int colonIndex = 0;
214     int closeBracketIndex = 0;
215     checkArgument(
216         hostPortString.charAt(0) == '[',
217         "Bracketed host-port string must start with a bracket: %s",
218         hostPortString);
219     colonIndex = hostPortString.indexOf(':');
220     closeBracketIndex = hostPortString.lastIndexOf(']');
221     checkArgument(
222         colonIndex > -1 && closeBracketIndex > colonIndex,
223         "Invalid bracketed host/port: %s",
224         hostPortString);
225 
226     String host = hostPortString.substring(1, closeBracketIndex);
227     if (closeBracketIndex + 1 == hostPortString.length()) {
228       return new String[] {host, ""};
229     } else {
230       checkArgument(
231           hostPortString.charAt(closeBracketIndex + 1) == ':',
232           "Only a colon may follow a close bracket: %s",
233           hostPortString);
234       for (int i = closeBracketIndex + 2; i < hostPortString.length(); ++i) {
235         checkArgument(
236             Character.isDigit(hostPortString.charAt(i)),
237             "Port must be numeric: %s",
238             hostPortString);
239       }
240       return new String[] {host, hostPortString.substring(closeBracketIndex + 2)};
241     }
242   }
243 
244   /**
245    * Provide a default port if the parsed string contained only a host.
246    *
247    * You can chain this after {@link #fromString(String)} to include a port in case the port was
248    * omitted from the input string. If a port was already provided, then this method is a no-op.
249    *
250    * @param defaultPort a port number, from [0..65535]
251    * @return a HostAndPort instance, guaranteed to have a defined port.
252    */
253   public HostAndPort withDefaultPort(int defaultPort) {
254     checkArgument(isValidPort(defaultPort));
255     if (hasPort() || port == defaultPort) {
256       return this;
257     }
258     return new HostAndPort(host, defaultPort, hasBracketlessColons);
259   }
260 
261   /**
262    * Generate an error if the host might be a non-bracketed IPv6 literal.
263    *
264    * <p>URI formatting requires that IPv6 literals be surrounded by brackets, like "[2001:db8::1]".
265    * Chain this call after {@link #fromString(String)} to increase the strictness of the parser, and
266    * disallow IPv6 literals that don't contain these brackets.
267    *
268    * <p>Note that this parser identifies IPv6 literals solely based on the presence of a colon. To
269    * perform actual validation of IP addresses, see the {@link InetAddresses#forString(String)}
270    * method.
271    *
272    * @return {@code this}, to enable chaining of calls.
273    * @throws IllegalArgumentException if bracketless IPv6 is detected.
274    */
275   public HostAndPort requireBracketsForIPv6() {
276     checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host);
277     return this;
278   }
279 
280   @Override
281   public boolean equals(@Nullable Object other) {
282     if (this == other) {
283       return true;
284     }
285     if (other instanceof HostAndPort) {
286       HostAndPort that = (HostAndPort) other;
287       return Objects.equal(this.host, that.host)
288           && this.port == that.port
289           && this.hasBracketlessColons == that.hasBracketlessColons;
290     }
291     return false;
292   }
293 
294   @Override
295   public int hashCode() {
296     return Objects.hashCode(host, port, hasBracketlessColons);
297   }
298 
299   /** Rebuild the host:port string, including brackets if necessary. */
300   @Override
301   public String toString() {
302     // "[]:12345" requires 8 extra bytes.
303     StringBuilder builder = new StringBuilder(host.length() + 8);
304     if (host.indexOf(':') >= 0) {
305       builder.append('[').append(host).append(']');
306     } else {
307       builder.append(host);
308     }
309     if (hasPort()) {
310       builder.append(':').append(port);
311     }
312     return builder.toString();
313   }
314 
315   /** Return true for valid port numbers. */
316   private static boolean isValidPort(int port) {
317     return port >= 0 && port <= 65535;
318   }
319 
320   private static final long serialVersionUID = 0;
321 }