    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    }