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