HostAndPort.java

/*
 * junixsocket
 *
 * Copyright 2009-2024 Christian Kohlschütter
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.newsclub.net.unix;

import java.io.UnsupportedEncodingException;
import java.net.SocketException;
import java.net.URI;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Hostname and port.
 *
 * @author Christian Kohlschütter
 */
public final class HostAndPort {
  private static final Pattern PAT_HOST_AND_PORT = Pattern.compile(
      "^//((?<userinfo>[^/\\@]*)\\@)?(?<host>[^/\\:]+)(?:\\:(?<port>[0-9]+))?");
  private final String hostname;
  private final int port;

  /**
   * Creates a new hostname and port combination.
   *
   * @param hostname The hostname.
   * @param port The port, or {@code -1} for "no port".
   */
  public HostAndPort(String hostname, int port) {
    this.hostname = hostname;
    this.port = port;
  }

  @Override
  public int hashCode() {
    final int prime = 31;
    int result = 1;
    result = prime * result + ((getHostname() == null) ? 0 : getHostname().hashCode());
    result = prime * result + getPort();
    return result;
  }

  @Override
  public boolean equals(Object obj) {
    if (this == obj) {
      return true;
    }
    if (!(obj instanceof HostAndPort)) {
      return false;
    }
    HostAndPort other = (HostAndPort) obj;
    if (getHostname() == null) {
      if (other.getHostname() != null) {
        return false;
      }
    } else if (!getHostname().equals(other.getHostname())) {
      return false;
    }

    return getPort() == other.getPort();
  }

  @Override
  public String toString() {
    if (getPort() == -1) {
      return getHostname();
    } else {
      return getHostname() + ":" + getPort();
    }
  }

  /**
   * Tries to extract hostname and port information from the given URI.
   *
   * @param u The URI to extract from.
   * @return The parsed {@link HostAndPort} instance.
   * @throws SocketException on error.
   */
  public static HostAndPort parseFrom(URI u) throws SocketException {
    String host = u.getHost();
    if (host != null) {
      return new HostAndPort(host, u.getPort());
    }
    String raw = u.getRawSchemeSpecificPart();
    Matcher m = PAT_HOST_AND_PORT.matcher(raw);
    if (!m.find()) {
      throw new SocketException("Cannot parse URI: " + u);
    }
    try {
      host = URLDecoder.decode(m.group("host"), "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new IllegalStateException(e);
    }

    String portStr = m.group("port");
    int port;
    if (portStr == null) {
      port = -1;
    } else {
      port = Integer.parseInt(portStr);
    }

    return new HostAndPort(host, port);
  }

  private static String urlEncode(String s) {
    try {
      return URLEncoder.encode(s, "UTF-8");
    } catch (UnsupportedEncodingException e) {
      throw new IllegalStateException(e);
    }
  }

  /**
   * Returns the hostname.
   *
   * @return The hostname.
   */
  public String getHostname() {
    return hostname;
  }

  /**
   * Returns the port, or {@code -1} for "no port specified".
   *
   * @return The port.
   */
  public int getPort() {
    return port;
  }

  /**
   * Returns a URI with this hostname and port.
   *
   * @param scheme The scheme to use.
   * @return The URI.
   */
  public URI toURI(String scheme) {
    return toURI(scheme, null, null, null, null);
  }

  /**
   * Returns a URI with this hostname and port, potentially reusing other URI parameters from the
   * given template URI (authority, path, query, fragment).
   *
   * @param scheme The scheme to use.
   * @param template The template. or {@code null}.
   * @return The URI.
   */
  public URI toURI(String scheme, URI template) {
    if (template == null) {
      return toURI(scheme, null, null, null, null);
    }

    String rawAuthority = template.getRawAuthority();
    int at = rawAuthority.indexOf('@');
    if (at >= 0) {
      rawAuthority = rawAuthority.substring(0, at);
    } else if (rawAuthority.length() > 0 && template.getHost() == null) {
      // encoded hostname was parsed as authority
      rawAuthority = null;
    } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost())) {
      // hostname was duplicated as authority
      rawAuthority = null;
    } else if (rawAuthority.length() > 0 && template.getAuthority().equals(template.getHost() + ":"
        + template.getPort())) {
      // hostname:port was duplicated as authority
      rawAuthority = null;
    }

    return toURI(scheme, rawAuthority, template.getRawPath(), template.getRawQuery(), template
        .getRawFragment());
  }

  /**
   * Returns a URI with this hostname and port, potentially using other URI parameters from the
   * given set of parameters.
   *
   * @param scheme The scheme to use.
   * @param rawAuthority The raw authority field, or {@code null}.
   * @param rawPath The raw path field, or {@code null}.
   * @param rawQuery The raw query field, or {@code null}.
   * @param rawFragment The raw fragment field, or {@code null}.
   * @return The URI.
   */
  public URI toURI(String scheme, String rawAuthority, String rawPath, String rawQuery,
      String rawFragment) {
    Objects.requireNonNull(scheme);
    if (rawPath != null && !rawPath.isEmpty()) {
      if (!rawPath.startsWith("/")) {
        throw new IllegalArgumentException("Path must be absolute: " + rawPath);
      }
    }

    return URI.create(scheme + "://" + (rawAuthority == null ? "" : rawAuthority + "@") + urlEncode(
        getHostname()).replace("%2C", ",") + (port <= 0 ? "" : (":" + port)) + (rawPath == null ? ""
            : rawPath) + (rawQuery == null ? "" : "?" + rawQuery) + (rawFragment == null ? "" : "#"
                + rawFragment));
  }
}