• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2011 The Guava Authors
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.google.common.net;
18 
19 import static com.google.common.base.Preconditions.checkArgument;
20 import static com.google.common.base.Preconditions.checkNotNull;
21 import static com.google.common.base.Preconditions.checkState;
22 
23 import com.google.common.annotations.Beta;
24 import com.google.common.annotations.GwtCompatible;
25 import com.google.common.base.Objects;
26 import com.google.common.base.Strings;
27 
28 import java.io.Serializable;
29 
30 import javax.annotation.Nullable;
31 import javax.annotation.concurrent.Immutable;
32 
33 /**
34  * An immutable representation of a host and port.
35  *
36  * <p>Example usage:
37  * <pre>
38  * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]")
39  *     .withDefaultPort(80)
40  *     .requireBracketsForIPv6();
41  * hp.getHostText();  // returns "2001:db8::1"
42  * hp.getPort();      // returns 80
43  * hp.toString();     // returns "[2001:db8::1]:80"
44  * </pre>
45  *
46  * <p>Here are some examples of recognized formats:
47  * <ul>
48  *   <li>example.com
49  *   <li>example.com:80
50  *   <li>192.0.2.1
51  *   <li>192.0.2.1:80
52  *   <li>[2001:db8::1]     - {@link #getHostText()} omits brackets
53  *   <li>[2001:db8::1]:80  - {@link #getHostText()} omits brackets
54  *   <li>2001:db8::1       - Use {@link #requireBracketsForIPv6()} to prohibit this
55  * </ul>
56  *
57  * <p>Note that this is not an exhaustive list, because these methods are only
58  * concerned with brackets, colons, and port numbers.  Full validation of the
59  * host field (if desired) is the caller's responsibility.
60  *
61  * @author Paul Marks
62  * @since 10.0
63  */
64 @Beta
65 @Immutable
66 @GwtCompatible
67 public final class HostAndPort implements Serializable {
68   /** Magic value indicating the absence of a port number. */
69   private static final int NO_PORT = -1;
70 
71   /** Hostname, IPv4/IPv6 literal, or unvalidated nonsense. */
72   private final String host;
73 
74   /** Validated port number in the range [0..65535], or NO_PORT */
75   private final int port;
76 
77   /** True if the parsed host has colons, but no surrounding brackets. */
78   private final boolean hasBracketlessColons;
79 
HostAndPort(String host, int port, boolean hasBracketlessColons)80   private HostAndPort(String host, int port, boolean hasBracketlessColons) {
81     this.host = host;
82     this.port = port;
83     this.hasBracketlessColons = hasBracketlessColons;
84   }
85 
86   /**
87    * Returns the portion of this {@code HostAndPort} instance that should
88    * represent the hostname or IPv4/IPv6 literal.
89    *
90    * <p>A successful parse does not imply any degree of sanity in this field.
91    * For additional validation, see the {@link HostSpecifier} class.
92    */
getHostText()93   public String getHostText() {
94     return host;
95   }
96 
97   /** Return true if this instance has a defined port. */
hasPort()98   public boolean hasPort() {
99     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    */
getPort()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    */
getPortOrDefault(int defaultPort)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    */
fromParts(String host, int port)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    */
fromHost(String host)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    */
fromString(String hostPortString)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    */
getHostAndPortFromBracketedHost(String hostPortString)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    */
withDefaultPort(int defaultPort)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    */
requireBracketsForIPv6()270   public HostAndPort requireBracketsForIPv6() {
271     checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host);
272     return this;
273   }
274 
275   @Override
equals(@ullable Object other)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
hashCode()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
toString()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. */
isValidPort(int port)311   private static boolean isValidPort(int port) {
312     return port >= 0 && port <= 65535;
313   }
314 
315   private static final long serialVersionUID = 0;
316 }
317