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    
017    package com.google.common.net;
018    
019    import static com.google.common.base.Preconditions.checkArgument;
020    import static com.google.common.base.Preconditions.checkNotNull;
021    import static com.google.common.base.Preconditions.checkState;
022    
023    import com.google.common.annotations.Beta;
024    import com.google.common.base.Objects;
025    
026    import java.io.Serializable;
027    import java.util.regex.Matcher;
028    import java.util.regex.Pattern;
029    
030    import javax.annotation.concurrent.Immutable;
031    
032    /**
033     * An immutable representation of a host and port.
034     *
035     * <p>Example usage:
036     * <pre>
037     * HostAndPort hp = HostAndPort.fromString("[2001:db8::1]")
038     *     .withDefaultPort(80)
039     *     .requireBracketsForIPv6();
040     * hp.getHostText();  // returns "2001:db8::1"
041     * hp.getPort();      // returns 80
042     * hp.toString();     // returns "[2001:db8::1]:80"
043     * </pre>
044     *
045     * <p>Here are some examples of recognized formats:
046     * <ul>
047     *   <li>example.com
048     *   <li>example.com:80
049     *   <li>192.0.2.1
050     *   <li>192.0.2.1:80
051     *   <li>[2001:db8::1]     - {@link #getHostText()} omits brackets
052     *   <li>[2001:db8::1]:80  - {@link #getHostText()} omits brackets
053     *   <li>2001:db8::1       - Use {@link #requireBracketsForIPv6()} to prohibit this
054     * </ul>
055     *
056     * <p>Note that this is not an exhaustive list, because these methods are only
057     * concerned with brackets, colons, and port numbers.  Full validation of the
058     * host field (if desired) is the caller's responsibility.
059     *
060     * @author Paul Marks
061     * @since 10.0
062     */
063    @Beta @Immutable
064    public 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
085       * represent the hostname or IPv4/IPv6 literal.
086       *
087       * A successful parse does not imply any degree of sanity in this field.
088       * For additional validation, see the {@link HostSpecifier} class.
089       */
090      public String getHostText() {
091        return host;
092      }
093    
094      /** Return true if this instance has a defined port. */
095      public boolean hasPort() {
096        return port >= 0;
097      }
098    
099      /**
100       * Get the current port number, failing if no port is defined.
101       *
102       * @return a validated port number, in the range [0..65535]
103       * @throws IllegalStateException if no port is defined.  You can use
104       *         {@link #withDefaultPort(int)} to prevent this from occurring.
105       */
106      public int getPort() {
107        checkState(hasPort());
108        return port;
109      }
110    
111      /**
112       * Returns the current port number, with a default if no port is defined.
113       */
114      public int getPortOrDefault(int defaultPort) {
115        return hasPort() ? port : defaultPort;
116      }
117    
118      /**
119       * Build a HostAndPort instance from separate host and port values.
120       *
121       * <p>Note: Non-bracketed IPv6 literals are allowed.
122       * Use {@link #requireBracketsForIPv6()} to prohibit these.
123       *
124       * @param host the host string to parse.  Must not contain a port number.
125       * @param port a port number from [0..65535]
126       * @return if parsing was successful, a populated HostAndPort object.
127       * @throws IllegalArgumentException if {@code host} contains a port number,
128       *     or {@code port} is out of range.
129       */
130      public static HostAndPort fromParts(String host, int port) {
131        checkArgument(isValidPort(port));
132        HostAndPort parsedHost = fromString(host);
133        checkArgument(!parsedHost.hasPort());
134        return new HostAndPort(parsedHost.host, port, parsedHost.hasBracketlessColons);
135      }
136    
137      private static final Pattern BRACKET_PATTERN = Pattern.compile("^\\[(.*:.*)\\](?::(\\d*))?$");
138    
139      /**
140       * Split a freeform string into a host and port, without strict validation.
141       *
142       * Note that the host-only formats will leave the port field undefined.  You
143       * can use {@link #withDefaultPort(int)} to patch in a default value.
144       *
145       * @param hostPortString the input string to parse.
146       * @return if parsing was successful, a populated HostAndPort object.
147       * @throws IllegalArgumentException if nothing meaningful could be parsed.
148       */
149      public static HostAndPort fromString(String hostPortString) {
150        checkNotNull(hostPortString);
151        String host;
152        String portString = null;
153        boolean hasBracketlessColons = false;
154    
155        if (hostPortString.startsWith("[")) {
156          // Parse a bracketed host, typically an IPv6 literal.
157          Matcher matcher = BRACKET_PATTERN.matcher(hostPortString);
158          checkArgument(matcher.matches(), "Invalid bracketed host/port: %s", hostPortString);
159          host = matcher.group(1);
160          portString = matcher.group(2);  // could be null
161        } else {
162          int colonPos = hostPortString.indexOf(':');
163          if (colonPos >= 0 && hostPortString.indexOf(':', colonPos + 1) == -1) {
164            // Exactly 1 colon.  Split into host:port.
165            host = hostPortString.substring(0, colonPos);
166            portString = hostPortString.substring(colonPos + 1);
167          } else {
168            // 0 or 2+ colons.  Bare hostname or IPv6 literal.
169            host = hostPortString;
170            hasBracketlessColons = (colonPos >= 0);
171          }
172        }
173    
174        int port = NO_PORT;
175        if (portString != null) {
176          // Try to parse the whole port string as a number.
177          // JDK7 accepts leading plus signs. We don't want to.
178          checkArgument(!portString.startsWith("+"), "Unparseable port number: %s", hostPortString);
179          try {
180            port = Integer.parseInt(portString);
181          } catch (NumberFormatException e) {
182            throw new IllegalArgumentException("Unparseable port number: " + hostPortString);
183          }
184          checkArgument(isValidPort(port), "Port number out of range: %s", hostPortString);
185        }
186    
187        return new HostAndPort(host, port, hasBracketlessColons);
188      }
189    
190      /**
191       * Provide a default port if the parsed string contained only a host.
192       *
193       * You can chain this after {@link #fromString(String)} to include a port in
194       * case the port was omitted from the input string.  If a port was already
195       * provided, then this method is a no-op.
196       *
197       * @param defaultPort a port number, from [0..65535]
198       * @return a HostAndPort instance, guaranteed to have a defined port.
199       */
200      public HostAndPort withDefaultPort(int defaultPort) {
201        checkArgument(isValidPort(defaultPort));
202        if (hasPort() || port == defaultPort) {
203          return this;
204        }
205        return new HostAndPort(host, defaultPort, hasBracketlessColons);
206      }
207    
208      /**
209       * Generate an error if the host might be a non-bracketed IPv6 literal.
210       *
211       * <p>URI formatting requires that IPv6 literals be surrounded by brackets,
212       * like "[2001:db8::1]".  Chain this call after {@link #fromString(String)}
213       * to increase the strictness of the parser, and disallow IPv6 literals
214       * that don't contain these brackets.
215       *
216       * <p>Note that this parser identifies IPv6 literals solely based on the
217       * presence of a colon.  To perform actual validation of IP addresses, see
218       * the {@link InetAddresses#forString(String)} method.
219       *
220       * @return {@code this}, to enable chaining of calls.
221       * @throws IllegalArgumentException if bracketless IPv6 is detected.
222       */
223      public HostAndPort requireBracketsForIPv6() {
224        checkArgument(!hasBracketlessColons, "Possible bracketless IPv6 literal: %s", host);
225        return this;
226      }
227    
228      @Override
229      public boolean equals(Object other) {
230        if (this == other) {
231          return true;
232        }
233        if (other instanceof HostAndPort) {
234          HostAndPort that = (HostAndPort) other;
235          return Objects.equal(this.host, that.host)
236              && this.port == that.port
237              && this.hasBracketlessColons == that.hasBracketlessColons;
238        }
239        return false;
240      }
241    
242      @Override
243      public int hashCode() {
244        return Objects.hashCode(host, port, hasBracketlessColons);
245      }
246    
247      /** Rebuild the host:port string, including brackets if necessary. */
248      @Override
249      public String toString() {
250        StringBuilder builder = new StringBuilder(host.length() + 7);
251        if (host.indexOf(':') >= 0) {
252          builder.append('[').append(host).append(']');
253        } else {
254          builder.append(host);
255        }
256        if (hasPort()) {
257          builder.append(':').append(port);
258        }
259        return builder.toString();
260      }
261    
262      /** Return true for valid port numbers. */
263      private static boolean isValidPort(int port) {
264        return port >= 0 && port <= 65535;
265      }
266    
267      private static final long serialVersionUID = 0;
268    }