content: package com.anthonynsimon.url;
  
  import com.anthonynsimon.url.exceptions.MalformedURLException;
  
  /**
   * A default URL parser implementation.
   */
  final class DefaultURLParser implements URLParser {
  
      /**
       * Returns a the URL with the new values after parsing the provided URL string.
       */
      public URL parse(String rawUrl) throws MalformedURLException {
          if (rawUrl == null || rawUrl.isEmpty()) {
              throw new MalformedURLException("raw url string is empty");
          }
  
          URLBuilder builder = new URLBuilder();
          String remaining = rawUrl;
  
          int index = remaining.lastIndexOf('#');
          if (index >= 0) {
              String frag = remaining.substring(index + 1, remaining.length());
              builder.setFragment(frag.isEmpty() ? null : frag);
              remaining = remaining.substring(0, index);
          }
  
          if (remaining.isEmpty()) {
              return builder.build();
          }
  
          if ("*".equals(remaining)) {
              builder.setPath("*");
              return builder.build();
          }
  
          index = remaining.indexOf('?');
          if (index > 0) {
              String query = remaining.substring(index + 1, remaining.length());
              if (query.isEmpty()) {
                  builder.setQuery("?");
              } else {
                  builder.setQuery(query);
              }
              remaining = remaining.substring(0, index);
          }
  
          PartialParseResult parsedScheme = parseScheme(remaining);
          String scheme = parsedScheme.result;
          boolean hasScheme = scheme != null && !scheme.isEmpty();
          builder.setScheme(scheme);
          remaining = parsedScheme.remaining;
          
          if (hasScheme && !remaining.startsWith("/")) {
              builder.setOpaque(remaining);
              return builder.build();
          }
          if ((hasScheme || !remaining.startsWith("///")) && remaining.startsWith("//")) {
              remaining = remaining.substring(2, remaining.length());
  
              String authority = remaining;
              int i = remaining.indexOf('/');
              if (i >= 0) {
                  authority = remaining.substring(0, i);
                  remaining = remaining.substring(i, remaining.length());
              } else {
                  remaining = "";
              }
  
              if (!authority.isEmpty()) {
                  UserInfoResult userInfoResult = parseUserInfo(authority);
                  builder.setUsername(userInfoResult.user);
                  builder.setPassword(userInfoResult.password);
                  authority = userInfoResult.remaining;
              }
  
              PartialParseResult hostResult = parseHost(authority);
              builder.setHost(hostResult.result);
          }
  
          if (!remaining.isEmpty()) {
              builder.setPath(PercentEncoder.decode(remaining));
              builder.setRawPath(remaining);
          }
  
          return builder.build();
      }
  
      /**
       * Parses the scheme from the provided string.
       *
       * * @throws MalformedURLException if there was a problem parsing the input string.
       */
      private PartialParseResult parseScheme(String remaining) throws MalformedURLException {
          int indexColon = remaining.indexOf(':');
          if (indexColon == 0) {
              throw new MalformedURLException("missing scheme");
          }
          if (indexColon < 0) {
              return new PartialParseResult("", remaining);
          }
  
          // if first char is special then its not a scheme
          char first = remaining.charAt(0);
          if ('0' <= first && first <= '9' || first == '+' || first == '-' || first == '.') {
              return new PartialParseResult("", remaining);
          }
  
          String scheme = remaining.substring(0, indexColon).toLowerCase();
          String rest = remaining.substring(indexColon + 1, remaining.length());
          
          return new PartialParseResult(scheme, rest);
      }
  
      /**
       * Parses the authority (user:password@host:port) from the provided string.
       *
       * @throws MalformedURLException if there was a problem parsing the input string.
       */
      private UserInfoResult parseUserInfo(String str) throws MalformedURLException {
          int i = str.lastIndexOf('@');
          String username = null;
          String password = null;
          String rest = str;
          if (i >= 0) {
              String credentials = str.substring(0, i);
              if (credentials.indexOf(':') >= 0) {
                  String[] parts = credentials.split(":", 2);
                  username = PercentEncoder.decode(parts[0]);
                  password = PercentEncoder.decode(parts[1]);
              } else {
                  username = PercentEncoder.decode(credentials);
              }
              rest = str.substring(i + 1, str.length());
          }
  
          return new UserInfoResult(username, password, rest);
      }
  
      /**
       * Parses the host from the provided string. The port is considered part of the host and
       * will be checked to ensure that it's a numeric value.
       *
       * @throws MalformedURLException if there was a problem parsing the input string.
       */
      private PartialParseResult parseHost(String str) throws MalformedURLException {
          if (str.length() == 0) {
              return new PartialParseResult("", "");
          }
          if (str.charAt(0) == '[') {
              int i = str.lastIndexOf(']');
              if (i < 0) {
                  throw new MalformedURLException("IPv6 detected, but missing closing ']' token");
              }
              String portPart = str.substring(i + 1, str.length());
              if (!isPortValid(portPart)) {
                  throw new MalformedURLException("invalid port");
              }
          } else {
              if (str.indexOf(':') != -1) {
                  String[] parts = str.split(":", -1);
                  if (parts.length > 2) {
                      throw new MalformedURLException("invalid host in: " + str);
                  }
                  if (parts.length == 2) {
                      try {
                          Integer.valueOf(parts[1]);
                      } catch (NumberFormatException e) {
                          throw new MalformedURLException("invalid port");
                      }
                  }
              }
          }
          return new PartialParseResult(PercentEncoder.decode(str.toLowerCase()), "");
      }
  
      /**
       * Returns true if the provided port string contains a valid port number.
       * Note that an empty string is a valid port number since it's optional.
       * <p>
       * For example:
       * <p>
       * ''      => TRUE
       * null    => TRUE
       * ':8080' => TRUE
       * ':ab80' => FALSE
       * ':abc'  => FALSE
       */
      protected boolean isPortValid(String portStr) {
          if (portStr == null || portStr.isEmpty()) {
              return true;
          }
          int i = portStr.indexOf(':');
          // Port format must be ':8080'
          if (i != 0) {
              return false;
          }
          String segment = portStr.substring(i + 1, portStr.length());
          try {
              Integer.valueOf(segment);
          } catch (NumberFormatException e) {
              return false;
          }
          return true;
      }
  
      private class PartialParseResult {
          public final String result;
          public final String remaining;
  
  
          public PartialParseResult(String result, String remaining) {
              this.result = result;
              this.remaining = remaining;
          }
      }
  
      private class UserInfoResult {
          public final String user;
          public final String password;
          public final String remaining;
  
  
          public UserInfoResult(String user, String password, String remaining) {
              this.user = user;
              this.password = password;
              this.remaining = remaining;
          }
      }
  }
  