content: /*
   * Copyright (c) 2016, 2021, Oracle and/or its affiliates.
   *
   * This program is free software; you can redistribute it and/or modify it under
   * the terms of the GNU General Public License, version 2.0, as published by the
   * Free Software Foundation.
   *
   * This program is also distributed with certain software (including but not
   * limited to OpenSSL) that is licensed under separate terms, as designated in a
   * particular file or component or in included license documentation. The
   * authors of MySQL hereby grant you an additional permission to link the
   * program and your derivative works with the separately licensed software that
   * they have included with MySQL.
   *
   * Without limiting anything contained in the foregoing, this file, which is
   * part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
   * version 1.0, a copy of which can be found at
   * http://oss.oracle.com/licenses/universal-foss-exception.
   *
   * This program is distributed in the hope that it will be useful, but WITHOUT
   * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   * FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0,
   * for more details.
   *
   * You should have received a copy of the GNU General Public License along with
   * this program; if not, write to the Free Software Foundation, Inc.,
   * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA
   */
  
  package com.mysql.cj;
  
  import static org.junit.jupiter.api.Assertions.assertEquals;
  import static org.junit.jupiter.api.Assertions.assertFalse;
  import static org.junit.jupiter.api.Assertions.assertNotSame;
  import static org.junit.jupiter.api.Assertions.assertNull;
  import static org.junit.jupiter.api.Assertions.assertSame;
  import static org.junit.jupiter.api.Assertions.assertTrue;
  import static org.junit.jupiter.api.Assertions.fail;
  
  import java.io.UnsupportedEncodingException;
  import java.net.URLDecoder;
  import java.nio.charset.StandardCharsets;
  import java.util.ArrayList;
  import java.util.Arrays;
  import java.util.HashMap;
  import java.util.Iterator;
  import java.util.List;
  import java.util.Map;
  import java.util.Map.Entry;
  import java.util.NoSuchElementException;
  import java.util.Properties;
  import java.util.concurrent.Callable;
  import java.util.stream.Collectors;
  import java.util.stream.IntStream;
  
  import org.junit.jupiter.api.Test;
  
  import com.mysql.cj.conf.ConnectionPropertiesTransform;
  import com.mysql.cj.conf.ConnectionUrl;
  import com.mysql.cj.conf.ConnectionUrl.Type;
  import com.mysql.cj.conf.ConnectionUrlParser;
  import com.mysql.cj.conf.HostInfo;
  import com.mysql.cj.conf.HostsListView;
  import com.mysql.cj.conf.PropertyDefinitions.ZeroDatetimeBehavior;
  import com.mysql.cj.conf.PropertyKey;
  import com.mysql.cj.conf.url.FailoverConnectionUrl;
  import com.mysql.cj.conf.url.FailoverDnsSrvConnectionUrl;
  import com.mysql.cj.conf.url.LoadBalanceConnectionUrl;
  import com.mysql.cj.conf.url.LoadBalanceDnsSrvConnectionUrl;
  import com.mysql.cj.conf.url.ReplicationConnectionUrl;
  import com.mysql.cj.conf.url.ReplicationDnsSrvConnectionUrl;
  import com.mysql.cj.conf.url.SingleConnectionUrl;
  import com.mysql.cj.conf.url.XDevApiConnectionUrl;
  import com.mysql.cj.conf.url.XDevApiDnsSrvConnectionUrl;
  import com.mysql.cj.exceptions.CJException;
  import com.mysql.cj.exceptions.InvalidConnectionAttributeException;
  import com.mysql.cj.exceptions.WrongArgumentException;
  
  public class ConnectionUrlTest {
      protected static <EX extends Throwable> EX assertThrows(Class<EX> throwable, Callable<?> testRoutine) {
          return assertThrows("", throwable, null, testRoutine);
      }
  
      protected static <EX extends Throwable> EX assertThrows(String message, Class<EX> throwable, Callable<?> testRoutine) {
          return assertThrows(message, throwable, null, testRoutine);
      }
  
      protected static <EX extends Throwable> EX assertThrows(Class<EX> throwable, String msgMatchesRegex, Callable<?> testRoutine) {
          return assertThrows("", throwable, msgMatchesRegex, testRoutine);
      }
  
      protected static <EX extends Throwable> EX assertThrows(String message, Class<EX> throwable, String msgMatchesRegex, Callable<?> testRoutine) {
          if (message.length() > 0) {
              message += " ";
          }
          try {
              testRoutine.call();
          } catch (Throwable t) {
              assertTrue(throwable.isAssignableFrom(t.getClass()), message + "expected exception of type '" + throwable.getName()
                      + "' but instead a exception of type '" + t.getClass().getName() + "' was thrown.");
              assertFalse(msgMatchesRegex != null && !t.getMessage().matches(msgMatchesRegex),
                      message + "the error message «" + t.getMessage() + "» was expected to match «" + msgMatchesRegex + "».");
  
              return throwable.cast(t);
          }
          fail(message + "expected exception of type '" + throwable.getName() + "'.");
  
          // never reaches here
          return null;
      }
  
      /**
       * Internal class for generating hundreds of thousands of connection strings.
       */
      private static class ConnectionStringGenerator implements Iterator<String>, Iterable<String> {
          enum UrlMode {
              SINGLE_HOST(1), OUTER_HOSTS_LIST(2), INNER_HOSTS_LIST(2);
  
              private int hostsCount;
  
              UrlMode(int hostsCount) {
                  this.hostsCount = hostsCount;
              }
  
              int getHostsCount() {
                  return this.hostsCount;
              }
          }
  
          private static final String[] PROTOCOL = new String[] { "jdbc:mysql:", "mysqlx:" };
          private static final String[] USER = new String[] { "", "@", "johndoe@", "johndoe:@", "johndoe:secret@", ":secret@", ":@" };
          private static final String[] STD_HOST = new String[] { "", "myhost", "192.168.0.1", "[1000:abcd::1]",
                  "verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" };
          private static final String[] STD_PORT = new String[] { "", ":", ":1234" };
          private static final String[] KEY_VALUE_HOST = new String[] { "", "()", "(host=[::1],port=1234,prio=1)",
                  "(protocol=tcp,host=myhost,port=1234,key=value%28%29)", "(address=myhost:1234,prio=2)",
                  "(protocol=tcp,host=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789,port=1234,key=value%28%29)",
                  "(address=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234,prio=2)" };
          private static final String[] ADDRESS_EQUALS_HOST = new String[] { "address=", "address=()", "address=(flag)",
                  "address=(protocol=tcp)(host=myhost)(port=1234)", "address=(protocol=tcp)(host=myhost)(port=1234)(key=value%28%29)",
                  "address=(protocol=tcp)(host=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789)(port=1234)",
                  "address=(protocol=tcp)(host=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789)(port=1234)(key=value%28%29)" };
          private static final String[] HOST; // Initialized below.
          private static final String[] DB = new String[] { "", "/", "/mysql" };
          private static final String[] PARAMS = new String[] { "", "?", "?key=value&flag", "?key=value%26&flag&26", "?file=%2Fpath%2Fto%2Ffile&flag&key=value",
                  "?file=(/path/to/file)&flag&key=value" };
  
          static {
              int i = 0;
              String[] hosts = new String[STD_HOST.length * STD_PORT.length + KEY_VALUE_HOST.length + ADDRESS_EQUALS_HOST.length];
              for (String h : STD_HOST) {
                  for (String p : STD_PORT) {
                      hosts[i++] = h + p;
                  }
              }
              for (String h : KEY_VALUE_HOST) {
                  hosts[i++] = h;
              }
              for (String h : ADDRESS_EQUALS_HOST) {
                  hosts[i++] = h;
              }
              HOST = hosts;
          }
  
          UrlMode urlMode;
          private int numberOfHosts;
          private int[] current;
          private int[] next;
          private int[] ceiling;
          boolean hasNext = true;
  
          /**
           * Create an instance of {@link ConnectionStringGenerator} and initializes internal data for the iterator.
           * 
           * @param urlMode
           */
          public ConnectionStringGenerator(UrlMode urlMode) {
              this.urlMode = urlMode;
              this.numberOfHosts = this.urlMode.getHostsCount();
  
              int counterLen = 0;
              switch (this.urlMode) {
                  case SINGLE_HOST:
                      counterLen = 5; // protocol + user + host + db + params
                      break;
                  case OUTER_HOSTS_LIST:
                      counterLen = 3 + 2 * this.numberOfHosts; // protocol + (user + host) * num_of_hosts + db + params
                      break;
                  case INNER_HOSTS_LIST:
                      counterLen = 4 + this.numberOfHosts; // protocol + user + host * num_of_hosts + db + params
                      break;
              }
              this.current = new int[counterLen];
              this.next = new int[counterLen];
              this.ceiling = new int[counterLen];
  
              int counterIndex = 0;
              this.ceiling[counterIndex++] = PROTOCOL.length;
              switch (this.urlMode) {
                  case SINGLE_HOST:
                      this.ceiling[counterIndex++] = USER.length;
                      this.ceiling[counterIndex++] = HOST.length;
                      break;
                  case OUTER_HOSTS_LIST:
                      for (int i = 0; i < this.numberOfHosts; i++) {
                          this.ceiling[counterIndex++] = USER.length;
                          this.ceiling[counterIndex++] = HOST.length;
                      }
                      break;
                  case INNER_HOSTS_LIST:
                      this.ceiling[counterIndex++] = USER.length;
                      for (int i = 0; i < this.numberOfHosts; i++) {
                          this.ceiling[counterIndex++] = HOST.length;
                      }
                      break;
              }
              this.ceiling[counterIndex++] = DB.length;
              this.ceiling[counterIndex++] = PARAMS.length;
          }
  
          /**
           * Increments the counter recursively for each connection string part.
           * 
           * @param i
           *            the part where to increment the counter
           * @return false if the counter reaches the end, true otherwise
           */
          private boolean incrementCounter(int i) {
              if (i >= this.next.length) {
                  return false;
              }
              this.current[i] = this.next[i];
              this.next[i] = (this.next[i] + 1) % this.ceiling[i];
              if (this.next[i] == 0) {
                  return incrementCounter(i + 1);
              }
              return true;
          }
  
          /**
           * Builds a connection string with the parts corresponding to the current counter position.
           * 
           * @return the connection string built from the current counter position
           */
          private String buildConnectionString() {
              StringBuilder sb = new StringBuilder();
              int counterIndex = 0;
              sb.append(PROTOCOL[this.current[counterIndex++]]).append("//");
              if (this.urlMode == UrlMode.SINGLE_HOST || this.urlMode == UrlMode.OUTER_HOSTS_LIST) {
                  for (int i = 0; i < this.numberOfHosts; i++) {
                      if (i != 0) {
                          sb.append(",");
                      }
                      sb.append(USER[this.current[counterIndex++]]);
                      sb.append(HOST[this.current[counterIndex++]]);
                  }
              } else if (this.urlMode == UrlMode.INNER_HOSTS_LIST) {
                  sb.append(USER[this.current[counterIndex++]]).append("[");
                  for (int i = 0; i < this.numberOfHosts; i++) {
                      if (i != 0) {
                          sb.append(",");
                      }
                      sb.append(HOST[this.current[counterIndex++]]);
                  }
                  sb.append("]");
  
              }
              sb.append(DB[this.current[counterIndex++]]);
              sb.append(PARAMS[this.current[counterIndex++]]);
              return sb.toString();
          }
  
          @Override
          public boolean hasNext() {
              return this.hasNext;
          }
  
          @Override
          public String next() {
              if (!this.hasNext) {
                  throw new NoSuchElementException();
              }
              this.hasNext = incrementCounter(0);
              return buildConnectionString();
          }
  
          /**
           * Returns the protocol part (scheme) for the current position.
           * 
           * @return the protocol part
           */
          public String getProtocol() {
              int counterIndex = 0; // protocol
              return PROTOCOL[this.current[counterIndex]];
          }
  
          /**
           * Returns the user info part for the current position and the given host.
           * 
           * @param fromHostIndex
           *            the host from where to get user info
           * @return the user info part
           */
          public String getUserInfo(int fromHostIndex) {
              if (fromHostIndex < 0 || fromHostIndex >= this.numberOfHosts) {
                  throw new IllegalArgumentException("Argument \"fromHostIndex\" out of bounds.");
              }
  
              int counterIndex = 1; // user (single host or inner hosts list)
              if (this.urlMode == UrlMode.OUTER_HOSTS_LIST) {
                  counterIndex += fromHostIndex * 2; // increments of two per additional host
              }
              fromHostIndex--;
              return USER[this.current[counterIndex]];
          }
  
          /**
           * Returns the host info part for the current position and the given host.
           * 
           * @param fromHostIndex
           *            the host from where to get host info
           * @return the host info part
           */
          public String getHostInfo(int fromHostIndex) {
              if (fromHostIndex < 0 || fromHostIndex >= this.numberOfHosts) {
                  throw new IllegalArgumentException("Argument \"fromHostIndex\" out of bounds.");
              }
  
              int counterIndex = 2; // host (single host)
              if (this.urlMode == UrlMode.INNER_HOSTS_LIST) {
                  counterIndex += fromHostIndex; // increments of one per additional host
              } else if (this.urlMode == UrlMode.OUTER_HOSTS_LIST) {
                  counterIndex += fromHostIndex * 2; // increments of two per additional host
              }
              fromHostIndex--;
              return HOST[this.current[counterIndex]];
          }
  
          /**
           * Returns the database part for the current position.
           * 
           * @return the database part
           */
          public String getDatabase() {
              int counterIndex = 3; // db (single host)
              if (this.urlMode == UrlMode.INNER_HOSTS_LIST) {
                  counterIndex += this.numberOfHosts - 1; // increments of one per additional host
              } else if (this.urlMode == UrlMode.OUTER_HOSTS_LIST) {
                  counterIndex += (this.numberOfHosts - 1) * 2; // increments of two per additional host
              }
              return DB[this.current[counterIndex]];
          }
  
          /**
           * Returns the connection parameters part for the current position.
           * 
           * @return the connection parameter part
           */
          public String getParams() {
              int counterIndex = 4; // params (single host)
              if (this.urlMode == UrlMode.INNER_HOSTS_LIST) {
                  counterIndex += this.numberOfHosts - 1; // increments of one per additional host
              } else if (this.urlMode == UrlMode.OUTER_HOSTS_LIST) {
                  counterIndex += (this.numberOfHosts - 1) * 2; // increments of two per additional host
              }
              return PARAMS[this.current[counterIndex]];
          }
  
          /**
           * Checks if current host info contains the given key & value parameter.
           * 
           * @param hostIndex
           *            the host from where the given information will be checked against
           * @param key
           *            the key to check
           * @param value
           *            the value to check
           * @return true if the key/value pair exists, false otherwise
           */
          public boolean hasHostParam(int hostIndex, String key, String value) {
              StringBuilder sbKv = new StringBuilder(key);
              if (value != null) {
                  sbKv.append("=").append(value);
              }
              return getHostInfo(hostIndex).contains(sbKv.toString()) || decode(getHostInfo(hostIndex)).contains(sbKv.toString());
          }
  
          /**
           * Returns the number of host specific parameters existing in the current position and the given host.
           * 
           * @param hostIndex
           *            the host from where to get the count
           * @return the number of host specific parameters
           */
          public int getHostParamsCount(int hostIndex) {
              String hi = getHostInfo(hostIndex);
              if (hi.startsWith("(") && hi.lastIndexOf(")") != 1) {
                  return hi.length() - hi.replace(",", "").length() + 1;
              } else if (hi.startsWith("address=") && hi.length() > 10) { // len("address=()") == 10.
                  return hi.length() - hi.replace(")(", "|").length() + 1;
              }
              return 0;
          }
  
          /**
           * Checks if the current connection properties contain the given key & value.
           * 
           * @param key
           *            the key to check
           * @param value
           *            the value to check
           * @return true if the key/value pair exists, false otherwise
           */
          public boolean hasParam(String key, String value) {
              StringBuilder sbKv = new StringBuilder(key);
              if (value != null) {
                  sbKv.append("=").append(value);
              }
              return getParams().indexOf(sbKv.toString()) != -1 || decode(getParams()).indexOf(sbKv.toString()) != -1;
          }
  
          /**
           * Returns the number of connection parameters existing the the current position.
           * 
           * @return the number of connection parameters
           */
          public int getParamsCount() {
              String params = getParams();
              if (params.startsWith("?")) {
                  params = params.substring(1);
              }
              return params.isEmpty() ? 0 : params.split("&").length;
          }
  
          /**
           * Utility method to URL decode the given string.
           * 
           * @param text
           *            the text to decode
           * @return the decoded text
           */
          private String decode(String text) {
              if (text == null || text.isEmpty()) {
                  return text;
              }
              try {
                  return URLDecoder.decode(text, StandardCharsets.UTF_8.name());
              } catch (UnsupportedEncodingException e) {
                  // Won't happen.
              }
              return "";
          }
  
          @Override
          public Iterator<String> iterator() {
              return this;
          }
  
          @Override
          public String toString() {
              StringBuilder sb = new StringBuilder("{counter: ");
              sb.append(Arrays.toString(this.next));
              sb.append(", connectionString: \"").append(buildConnectionString()).append("\"}");
              return sb.toString();
          }
      }
  
      /**
       * Checks if the values returned from {@link Type#fromValue(String, int)} are correct.
       */
      @Test
      public void testTypeEnumCorrectValues() {
          // Standard schemes:
          assertEquals(ConnectionUrl.Type.SINGLE_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:", 1));
          assertEquals(ConnectionUrl.Type.FAILOVER_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:", 2));
          assertEquals(ConnectionUrl.Type.FAILOVER_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:", 3));
          assertEquals(ConnectionUrl.Type.LOADBALANCE_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:loadbalance:", 1));
          assertEquals(ConnectionUrl.Type.LOADBALANCE_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:loadbalance:", 2));
          assertEquals(ConnectionUrl.Type.REPLICATION_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:replication:", 1));
          assertEquals(ConnectionUrl.Type.REPLICATION_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql:replication:", 2));
          assertEquals(ConnectionUrl.Type.XDEVAPI_SESSION, ConnectionUrl.Type.fromValue("mysqlx:", 1));
          // DNS SRV schemes:
          assertEquals(ConnectionUrl.Type.FAILOVER_DNS_SRV_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql+srv:", 1));
          assertEquals(ConnectionUrl.Type.LOADBALANCE_DNS_SRV_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql+srv:loadbalance:", 1));
          assertEquals(ConnectionUrl.Type.REPLICATION_DNS_SRV_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql+srv:replication:", 1));
          assertEquals(ConnectionUrl.Type.REPLICATION_DNS_SRV_CONNECTION, ConnectionUrl.Type.fromValue("jdbc:mysql+srv:replication:", 2));
          assertEquals(ConnectionUrl.Type.XDEVAPI_DNS_SRV_SESSION, ConnectionUrl.Type.fromValue("mysqlx+srv:", 1));
      }
  
      /**
       * Tests the {@link ConnectionUrlParser} with close to one million of different connection string variations.
       */
      @Test
      public void testConnectionStringParser() {
          for (ConnectionStringGenerator.UrlMode urlMode : ConnectionStringGenerator.UrlMode.values()) {
              ConnectionStringGenerator csg = new ConnectionStringGenerator(urlMode);
              for (String cs : csg) {
                  ConnectionUrlParser cup = ConnectionUrlParser.parseConnectionString(cs);
                  String expected;
                  String actual;
                  // Protocol:
                  assertEquals(csg.getProtocol(), cup.getScheme(), cs);
                  // User & Host:
                  assertEquals(urlMode.getHostsCount(), cup.getHosts().size(), cs);
                  for (int hostIndex = 0; hostIndex < urlMode.getHostsCount(); hostIndex++) {
                      HostInfo hi = cup.getHosts().get(hostIndex);
                      // User(n):
                      expected = testCSParserTrimTail(testCSParserTrimHead(csg.getUserInfo(hostIndex), ":"), "@", ":");
                      actual = new StringBuilder(hi.getUser() == null ? "" : hi.getUser()).append(":").append(hi.getPassword() == null ? "" : hi.getPassword())
                              .toString();
                      actual = testCSParserTrimTail(testCSParserTrimHead(actual, ":"), ":");
                      assertEquals(expected, actual, cs);
                      if (csg.getHostInfo(hostIndex).startsWith("address=") || csg.getHostInfo(hostIndex).startsWith("(")) {
                          // Host props(n):
                          assertEquals(csg.getHostParamsCount(hostIndex), hi.getHostProperties().size(), cs);
                          for (Entry<String, String> kv : hi.getHostProperties().entrySet()) {
                              assertTrue(csg.hasHostParam(hostIndex, kv.getKey(), kv.getValue()), cs);
                          }
                      } else {
                          // Host(n)
                          expected = testCSParserTrimTail(testCSParserTrimHead(csg.getHostInfo(hostIndex), ":"), ":");
                          actual = new StringBuilder(hi.getHost() == null ? "" : hi.getHost()).append(":").append(hi.getPort() == -1 ? "" : hi.getPort())
                                  .toString();
                          actual = testCSParserTrimTail(testCSParserTrimHead(actual, ":"), ":");
                          assertEquals(expected, actual, cs);
                      }
                  }
                  // Database:
                  expected = testCSParserTrimHead(csg.getDatabase(), "/");
                  actual = cup.getPath() == null ? "" : testCSParserTrimHead(cup.getPath(), "/");
                  assertEquals(expected, actual, cs);
                  // Connection arguments:
                  assertEquals(csg.getParamsCount(), cup.getProperties().size(), cs);
                  for (Entry<String, String> kv : cup.getProperties().entrySet()) {
                      assertTrue(csg.hasParam(kv.getKey(), kv.getValue()), cs);
                  }
              }
          }
      }
  
      private String testCSParserTrimHead(String text, String... charsFromHead) {
          for (String c : charsFromHead) {
              if (text.startsWith(c)) {
                  text = text.substring(c.length());
              }
          }
          return text;
      }
  
      private String testCSParserTrimTail(String text, String... charsFromTail) {
          for (String c : charsFromTail) {
              if (text.endsWith(c)) {
                  text = text.substring(0, text.length() - c.length());
              }
          }
          return text;
      }
  
      /**
       * Tests the {@link ConnectionUrl#acceptsUrl(String)} method for supported and non-supported protocols.
       */
      @Test
      public void testConnectionStringAcceptsUrl() {
          // Supported URLs:
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql://127.0.0.1:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:loadbalance:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:loadbalance://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql:loadbalance://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:replication:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:replication://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql:replication://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql:"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://127.0.0.1:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://[::1]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://[fe80::250:56ff:fec0:8]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://johndoe:secret@[::1]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://johndoe:secret@[[::1]:1234]/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx://johndoe:secret@[[::1]:1234,(address=[abcd:1000::f09a]:4321,priority=100)]/db?key=value"));
  
          // Non-supported URLs:
          assertFalse(ConnectionUrl.acceptsUrl(""));
          assertFalse(ConnectionUrl.acceptsUrl("//somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql:"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql:jdbc:"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql:jdbc://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("loadbalance:"));
          assertFalse(ConnectionUrl.acceptsUrl("loadbalance://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("replication:"));
          assertFalse(ConnectionUrl.acceptsUrl("replication://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc: mysql://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:loadbalance:"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:loadbalance://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:replication:"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:replication://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:mysql:unknown:"));
          assertFalse(ConnectionUrl.acceptsUrl("jdbc:mysql:unknown://somehost:1234/db?key=value"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql-x:"));
          assertFalse(ConnectionUrl.acceptsUrl("mysql-x://somehost:1234/db?key=value"));
  
          // Supported DNS-SRV variants:
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql+srv://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv://127.0.0.1:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:loadbalance:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:loadbalance://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql:loadbalance://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:replication:"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:replication://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl(
                  "jdbc:mysql+srv:replication://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("jdbc:mysql+srv:"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://somehost:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://127.0.0.1:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://[::1]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://[fe80::250:56ff:fec0:8]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://johndoe:secret@[::1]:1234/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://johndoe:secret@[[::1]:1234]/db?key=value"));
          assertTrue(ConnectionUrl.acceptsUrl("mysqlx+srv://johndoe:secret@[[::1]:1234,(address=[abcd:1000::f09a]:4321,priority=100)]/db?key=value"));
      }
  
      /**
       * Tests the {@link ConnectionUrl} with close to one million of different connection string variations.
       */
      @Test
      public void testConnectionUrl() {
          Properties props = new Properties();
          props.setProperty("propKey", "propValue");
  
          for (ConnectionStringGenerator.UrlMode urlMode : ConnectionStringGenerator.UrlMode.values()) {
              ConnectionStringGenerator csg = new ConnectionStringGenerator(urlMode);
              for (String cs : csg) {
                  try {
                      ConnectionUrl.getConnectionUrlInstance(cs, props);
                  } catch (WrongArgumentException e) {
                      // X plugin connections ("mysqlx:") don't allow different credentials in different hosts and the generator doesn't account for that.
                      assertEquals(ConnectionUrl.Type.XDEVAPI_SESSION.getScheme(), csg.getProtocol(), cs);
                      boolean first = true;
                      boolean ok = false;
                      String lastUi = "";
                      for (int hostIndex = 0; hostIndex < urlMode.getHostsCount() && !ok; hostIndex++) {
                          if (first) {
                              first = false;
                              lastUi = csg.getUserInfo(hostIndex);
                          } else if (!lastUi.equals(csg.getUserInfo(hostIndex))) {
                              ok = true;
                          }
                      }
                      assertTrue(ok, cs + ": unexpected " + e.getClass().getName() + " thrown with message: " + e.getMessage());
                  }
              }
          }
      }
  
      /**
       * Tests the {@link ConnectionUrlParser} and {@link ConnectionUrl} with non standard, but accepted, connection strings.
       * 
       * @throws Exception
       */
      @Test
      public void testNonStandardConnectionUrl() throws Exception {
          String url;
          ConnectionUrlParser cup;
  
          /*
           * Single host syntax with spaces.
           */
          url = "jdbc:mysql: // johndoe : secret @ [abcd:9876::1:1234] : 1234 / db ? key1 = value1 & key2 = value2 # ignore";
          cup = ConnectionUrlParser.parseConnectionString(url);
          // schema & db
          assertEquals("jdbc:mysql:", cup.getScheme());
          assertEquals("db", cup.getPath());
          // single host: user, password, host name, port and host properties
          assertEquals(1, cup.getHosts().size());
          assertEquals("johndoe", cup.getHosts().get(0).getUser());
          assertEquals("secret", cup.getHosts().get(0).getPassword());
          assertEquals("[abcd:9876::1:1234]", cup.getHosts().get(0).getHost());
          assertEquals(1234, cup.getHosts().get(0).getPort());
          assertEquals(0, cup.getHosts().get(0).getHostProperties().size());
          // properties
          assertEquals(2, cup.getProperties().size());
          assertTrue(cup.getProperties().containsKey("key1"));
          assertEquals("value1", cup.getProperties().get("key1"));
          assertTrue(cup.getProperties().containsKey("key2"));
          assertEquals("value2", cup.getProperties().get("key2"));
          ConnectionUrl.getConnectionUrlInstance(url, null);
  
          /*
           * Single host address-equals syntax with spaces.
           */
          url = "jdbc:mysql: // johndoe : secret @ address= ( host = [abcd:9876::1:1234] ) ( port = 1234 ) / db ? key1 = value1 & key2 = value2 # ignore";
          cup = ConnectionUrlParser.parseConnectionString(url);
          // schema & db
          assertEquals("jdbc:mysql:", cup.getScheme());
          assertEquals("db", cup.getPath());
          // single host: user, password, host name, port and host properties
          assertEquals(1, cup.getHosts().size());
          assertEquals("johndoe", cup.getHosts().get(0).getUser());
          assertEquals("secret", cup.getHosts().get(0).getPassword());
          assertNull(cup.getHosts().get(0).getHost());
          assertEquals(-1, cup.getHosts().get(0).getPort());
          assertEquals(2, cup.getHosts().get(0).getHostProperties().size());
          assertTrue(cup.getHosts().get(0).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:1234]", cup.getHosts().get(0).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(0).getHostProperties().containsKey("port"));
          assertEquals("1234", cup.getHosts().get(0).getHostProperties().get("port"));
          // properties
          assertEquals(2, cup.getProperties().size());
          assertTrue(cup.getProperties().containsKey("key1"));
          assertEquals("value1", cup.getProperties().get("key1"));
          assertTrue(cup.getProperties().containsKey("key2"));
          assertEquals("value2", cup.getProperties().get("key2"));
          ConnectionUrl.getConnectionUrlInstance(url, null);
  
          /*
           * Single host key/value syntax with spaces.
           */
          url = "jdbc:mysql: // johndoe : secret @ ( host = [abcd:9876::1:1234] , port = 1234 ) / db ? key1 = value1 & key2 = value2 # ignore";
          cup = ConnectionUrlParser.parseConnectionString(url);
          // schema & db
          assertEquals("jdbc:mysql:", cup.getScheme());
          assertEquals("db", cup.getPath());
          // single host: user, password, host name, port and host properties
          assertEquals(1, cup.getHosts().size());
          assertEquals("johndoe", cup.getHosts().get(0).getUser());
          assertEquals("secret", cup.getHosts().get(0).getPassword());
          assertNull(cup.getHosts().get(0).getHost());
          assertEquals(-1, cup.getHosts().get(0).getPort());
          assertEquals(2, cup.getHosts().get(0).getHostProperties().size());
          assertTrue(cup.getHosts().get(0).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:1234]", cup.getHosts().get(0).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(0).getHostProperties().containsKey("port"));
          assertEquals("1234", cup.getHosts().get(0).getHostProperties().get("port"));
          // properties
          assertEquals(2, cup.getProperties().size());
          assertTrue(cup.getProperties().containsKey("key1"));
          assertEquals("value1", cup.getProperties().get("key1"));
          assertTrue(cup.getProperties().containsKey("key2"));
          assertEquals("value2", cup.getProperties().get("key2"));
          ConnectionUrl.getConnectionUrlInstance(url, null);
  
          /*
           * Hosts list syntax with spaces combining the above single host syntaxes.
           */
          url = "jdbc:mysql: // johndoe : secret @ [abcd:9876::1:1000] : 1111 , janedoe : secret @ address= ( host = [abcd:9876::1:2000] ) ( port = 2222 ) , "
                  + "jabdoe : secret @ ( host = [abcd:9876::1:3000] , port = 3333 ) / db ? key1 = value1 & key2 = value2 # ignore";
          cup = ConnectionUrlParser.parseConnectionString(url);
          // schema & db
          assertEquals("jdbc:mysql:", cup.getScheme());
          assertEquals("db", cup.getPath());
          assertEquals(3, cup.getHosts().size());
          // first host: user, password, host name, port and host properties
          assertEquals("johndoe", cup.getHosts().get(0).getUser());
          assertEquals("secret", cup.getHosts().get(0).getPassword());
          assertEquals("[abcd:9876::1:1000]", cup.getHosts().get(0).getHost());
          assertEquals(1111, cup.getHosts().get(0).getPort());
          assertEquals(0, cup.getHosts().get(0).getHostProperties().size());
          // second host: user, password, host name, port and host properties
          assertEquals("janedoe", cup.getHosts().get(1).getUser());
          assertEquals("secret", cup.getHosts().get(1).getPassword());
          assertNull(cup.getHosts().get(1).getHost());
          assertEquals(-1, cup.getHosts().get(1).getPort());
          assertEquals(2, cup.getHosts().get(1).getHostProperties().size());
          assertTrue(cup.getHosts().get(1).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:2000]", cup.getHosts().get(1).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(1).getHostProperties().containsKey("port"));
          assertEquals("2222", cup.getHosts().get(1).getHostProperties().get("port"));
          // third host: user, password, host name, port and host properties
          assertEquals("jabdoe", cup.getHosts().get(2).getUser());
          assertEquals("secret", cup.getHosts().get(2).getPassword());
          assertNull(cup.getHosts().get(2).getHost());
          assertEquals(-1, cup.getHosts().get(2).getPort());
          assertEquals(2, cup.getHosts().get(2).getHostProperties().size());
          assertTrue(cup.getHosts().get(2).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:3000]", cup.getHosts().get(2).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(2).getHostProperties().containsKey("port"));
          assertEquals("3333", cup.getHosts().get(2).getHostProperties().get("port"));
          // properties
          assertEquals(2, cup.getProperties().size());
          assertTrue(cup.getProperties().containsKey("key1"));
          assertEquals("value1", cup.getProperties().get("key1"));
          assertTrue(cup.getProperties().containsKey("key2"));
          assertEquals("value2", cup.getProperties().get("key2"));
          ConnectionUrl.getConnectionUrlInstance(url, null);
  
          /*
           * Hosts sub list syntax with spaces combining the above single host syntaxes.
           */
          url = "jdbc:mysql: // johndoe : secret @ "
                  + "[ [abcd:9876::1:1000] : 1111 , address= ( host = [abcd:9876::1:2000] ) ( port = 2222 ) , ( host = [abcd:9876::1:3000] , port = 3333 ) ] "
                  + "/ db ? key1 = value1 & key2 = value2 # ignore";
          cup = ConnectionUrlParser.parseConnectionString(url);
          // schema & db
          assertEquals("jdbc:mysql:", cup.getScheme());
          assertEquals("db", cup.getPath());
          assertEquals(3, cup.getHosts().size());
          // first host: user, password, host name, port and host properties
          assertEquals("johndoe", cup.getHosts().get(0).getUser());
          assertEquals("secret", cup.getHosts().get(0).getPassword());
          assertEquals("[abcd:9876::1:1000]", cup.getHosts().get(0).getHost());
          assertEquals(1111, cup.getHosts().get(0).getPort());
          assertEquals(0, cup.getHosts().get(0).getHostProperties().size());
          // second host: user, password, host name, port and host properties
          assertEquals("johndoe", cup.getHosts().get(1).getUser());
          assertEquals("secret", cup.getHosts().get(1).getPassword());
          assertNull(cup.getHosts().get(1).getHost());
          assertEquals(-1, cup.getHosts().get(1).getPort());
          assertEquals(2, cup.getHosts().get(1).getHostProperties().size());
          assertTrue(cup.getHosts().get(1).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:2000]", cup.getHosts().get(1).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(1).getHostProperties().containsKey("port"));
          assertEquals("2222", cup.getHosts().get(1).getHostProperties().get("port"));
          // third host: user, password, host name, port and host properties
          assertEquals("johndoe", cup.getHosts().get(2).getUser());
          assertEquals("secret", cup.getHosts().get(2).getPassword());
          assertNull(cup.getHosts().get(2).getHost());
          assertEquals(-1, cup.getHosts().get(2).getPort());
          assertEquals(2, cup.getHosts().get(2).getHostProperties().size());
          assertTrue(cup.getHosts().get(2).getHostProperties().containsKey("host"));
          assertEquals("[abcd:9876::1:3000]", cup.getHosts().get(2).getHostProperties().get("host"));
          assertTrue(cup.getHosts().get(2).getHostProperties().containsKey("port"));
          assertEquals("3333", cup.getHosts().get(2).getHostProperties().get("port"));
          // properties
          assertEquals(2, cup.getProperties().size());
          assertTrue(cup.getProperties().containsKey("key1"));
          assertEquals("value1", cup.getProperties().get("key1"));
          assertTrue(cup.getProperties().containsKey("key2"));
          assertEquals("value2", cup.getProperties().get("key2"));
          ConnectionUrl.getConnectionUrlInstance(url, null);
      }
  
      /**
       * Tests the {@link ConnectionUrl} with a few wrong connection strings.
       */
      @Test
      public void testConnectionUrlWithWrongConnectionString() {
          List<String> connStr = new ArrayList<>();
          connStr.add("jdbc:mysql://johndoe:secret@janedoe:secret@myhost:1234/db?key=value");
          connStr.add("jdbc:mysql://johndoe:secret@@myhost:1234/db?key=value");
          connStr.add("jdbc:mysql://johndoe:secret@myhost:abcd/db?key=value");
          connStr.add("jdbc:mysql://johndoe:secret@myhost:1234//db?key=value");
          connStr.add("jdbc:mysql://johndoe:secret@myhost:1234/db??key=value");
          connStr.add("jdbc:mysql://johndoe:secret@myhost:1234/db?=value");
  
          for (String cs : connStr) {
              assertThrows(WrongArgumentException.class, () -> {
                  System.out.println(ConnectionUrl.getConnectionUrlInstance(cs, null));
                  return null;
              });
          }
      }
  
      /**
       * Tests the connection strings internal cache.
       */
      @Test
      public void testConnectionStringCache() {
          Properties props1 = new Properties();
          props1.setProperty("propKey", "propValue");
          Properties props2 = new Properties(props1);
  
          ConnectionUrl cu1 = ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://localhost:3306/?param=value", null);
          ConnectionUrl cu2 = ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://localhost:3306/?param=value", props1);
          ConnectionUrl cu3 = ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://localhost:3306/?param=value", props1);
          ConnectionUrl cu4 = ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://localhost:3306/?param=value", props2);
          ConnectionUrl cu5 = ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://localhost:3306/?param=value&flag", props1);
  
          assertNotSame(cu1, cu2);
          assertSame(cu2, cu3);
          assertSame(cu3, cu4);
          assertNotSame(cu4, cu5);
          assertNotSame(cu5, cu1);
      }
  
      /**
       * Tests default values.
       */
      @Test
      public void testDefaultValues() {
          Map<String, Integer> connStr = new HashMap<>();
          connStr.put("jdbc:mysql:", 3306);
          connStr.put("jdbc:mysql://,", 3306);
          connStr.put("jdbc:mysql:loadbalance://,", 3306);
          connStr.put("jdbc:mysql:replication://,", 3306);
          connStr.put("mysqlx:", 33060);
  
          for (String cs : connStr.keySet()) {
              ConnectionUrl connUrl = ConnectionUrl.getConnectionUrlInstance(cs, null);
              for (HostInfo hi : connUrl.getHostsList()) {
                  assertEquals(cs, hi.getDatabaseUrl(), cs + "#databaseUrl");
                  assertEquals("localhost", hi.getHost(), cs + "#host");
                  assertEquals(connStr.get(cs).intValue(), hi.getPort(), cs + "#port");
                  assertEquals("localhost:" + connStr.get(cs), hi.getHostPortPair(), cs + "#hostPortPair");
                  assertNull(hi.getUser(), cs + "#user");
                  assertNull(hi.getPassword(), cs + "#password");
                  assertEquals("", hi.getDatabase(), cs + "#database");
              }
          }
      }
  
      /**
       * Tests loading properties from config files.
       */
      @Test
      public void testLoadingPropertiesFromConfigFiles() {
          Properties propsFromFile = ConnectionUrl.getPropertiesFromConfigFiles("fullDebug");
  
          List<String> connStr = new ArrayList<>();
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila?useConfigs=fullDebug");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)(useConfigs=fullDebug)/sakila");
  
          for (String cs : connStr) {
              Properties props = new Properties();
              if (cs.indexOf(PropertyKey.useConfigs.getKeyName()) == -1) {
                  // Send "useConfigs" through external properties.
                  props.setProperty(PropertyKey.useConfigs.getKeyName(), "fullDebug");
              }
              ConnectionUrl connUrl = ConnectionUrl.getConnectionUrlInstance(cs, props);
  
              if (cs.indexOf("address=") == -1) {
                  // Properties from file must be found simultaneously in per connection properties and per host.
                  Properties asProps = connUrl.getConnectionArgumentsAsProperties();
                  for (String key : propsFromFile.stringPropertyNames()) {
                      assertEquals(propsFromFile.getProperty(key), asProps.getProperty(key), cs + "#" + key);
                  }
                  Map<String, String> asMap = connUrl.getOriginalProperties();
                  for (String key : propsFromFile.stringPropertyNames()) {
                      assertEquals(propsFromFile.getProperty(key), asMap.get(key), cs + "#" + key);
                  }
              }
              Properties hostProps = connUrl.getMainHost().exposeAsProperties();
              for (String key : propsFromFile.stringPropertyNames()) {
                  assertEquals(propsFromFile.getProperty(key), hostProps.getProperty(key), cs + "#" + key);
              }
          }
      }
  
      /**
       * Tests the usage of a properties transformer.
       */
      @Test
      public void testPropertiesTransformer() {
          String propsTransClassName = ConnectionPropertiesTest.class.getName();
          List<String> connStr = new ArrayList<>();
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila?stars=*");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)(stars=*)/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)/sakila?stars=*");
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila?propertiesTransform=" + propsTransClassName);
          connStr.add("jdbc:mysql://johndoe:secret@mysql:1234/sakila?stars=*&propertiesTransform=" + propsTransClassName);
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)(propertiesTransform=" + propsTransClassName + ")/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)(propertiesTransform=" + propsTransClassName + ")(stars=*)/sakila");
          connStr.add("jdbc:mysql://johndoe:secret@address=(host=mysql)(port=1234)(propertiesTransform=" + propsTransClassName + ")/sakila?stars=*");
  
          for (String cs : connStr) {
              Properties props = new Properties();
              if (cs.indexOf(PropertyKey.propertiesTransform.getKeyName()) == -1) {
                  // Send "propertiesTransform" parameter through external properties.
                  props.setProperty(PropertyKey.propertiesTransform.getKeyName(), propsTransClassName);
              }
              if (cs.indexOf("stars") == -1) {
                  // Send "stars" parameter through external properties.
                  props.setProperty("stars", "*");
  
              }
              ConnectionUrl connUrl = ConnectionUrl.getConnectionUrlInstance(cs, props);
  
              // "propertiesTransform" doesn't apply when set through host internal properties.
              boolean transforms = cs.indexOf("(propertiesTransform") == -1;
  
              assertEquals(transforms ? "**" : "*", connUrl.getMainHost().getProperty("stars"), cs + "#hostProps");
              if (cs.indexOf("(stars") == -1) {
                  assertEquals("*", connUrl.getOriginalProperties().get("stars"), cs + "#originalProps");
                  assertEquals(transforms ? "**" : "*", connUrl.getConnectionArgumentsAsProperties().getProperty("stars"), cs + "#connProps");
              } else {
                  assertNull(connUrl.getOriginalProperties().get("stars"), cs + "#originalProps");
                  assertNull(connUrl.getConnectionArgumentsAsProperties().getProperty("stars"), cs + "#connProps");
              }
          }
      }
  
      public static class ConnectionPropertiesTest implements ConnectionPropertiesTransform {
          public Properties transformProperties(Properties props) {
              if (props.containsKey("stars")) {
                  props.setProperty("stars", props.getProperty("stars") + props.getProperty("stars"));
              }
              return props;
          }
      }
  
      /**
       * Tests specifics for the X Plugin connection strings.
       */
      @Test
      public void testMysqlxConnectionUrl() {
          ConnectionUrl connUrl;
          int hostIdx;
  
          String[] hostNames = new String[] { "host",
                  "verylonghostname0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789"
                          + "0123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789" };
  
          for (String hostName : hostNames) {
              // Hosts sub list with "address" splitting (host3:3333) and priority value.
              connUrl = ConnectionUrl.getConnectionUrlInstance("mysqlx://johndoe:secret@[" + hostName + "1:1111,address=(host=" + hostName
                      + "2)(port=2222)(priority=99)," + "(address=" + hostName + "3:3333,priority=98)]/db?address=host4:4444&priority=100", null);
              hostIdx = 1;
              for (HostInfo hi : connUrl.getHostsList()) {
                  String testCase = "Host " + hostIdx + ":";
                  assertEquals("johndoe", hi.getUser(), testCase);
                  assertEquals("secret", hi.getPassword(), testCase);
                  assertEquals(hostName + hostIdx, hi.getHost(), testCase);
                  assertEquals(1111 * hostIdx, hi.getPort(), testCase);
                  assertEquals("db", hi.getDatabase(), testCase);
                  assertTrue(hi.getHostProperties().containsKey(PropertyKey.PRIORITY.getKeyName()), testCase);
                  assertEquals(Integer.toString(101 - hostIdx), hi.getHostProperties().get(PropertyKey.PRIORITY.getKeyName()), testCase);
                  hostIdx++;
              }
  
              // Hosts sub list with "address" splitting (host3:3333) and without priority value (randomly sorted).
              connUrl = ConnectionUrl.getConnectionUrlInstance("mysqlx://johndoe:secret@[" + hostName + "1:1111,address=(host=" + hostName + "2)(port=2222),"
                      + "(address=" + hostName + "3:3333)]/db?address=host4:4444", null);
              String hosts = IntStream.range(1, 4).mapToObj(i -> hostName + i).collect(Collectors.joining("|"));
              String ports = IntStream.range(1, 4).mapToObj(i -> 1111 * i).map(i -> i.toString()).collect(Collectors.joining("|"));
              hostIdx = 1;
              for (HostInfo hi : connUrl.getHostsList()) {
                  String testCase = "Host " + hostIdx + ":";
                  assertEquals("johndoe", hi.getUser(), testCase);
                  assertEquals("secret", hi.getPassword(), testCase);
                  hosts = hosts.replace(hi.getHost(), "");
                  ports = ports.replace(Integer.toString(hi.getPort()), "");
                  assertEquals("db", hi.getDatabase(), testCase);
                  assertFalse(hi.getHostProperties().containsKey(PropertyKey.PRIORITY.getKeyName()), testCase);
                  hostIdx++;
              }
              assertEquals("||", hosts);
              assertEquals("||", ports);
  
              // Hosts list with "address" splitting (host3:3333) and priority value.
              connUrl = ConnectionUrl.getConnectionUrlInstance("mysqlx://johndoe:secret@" + hostName + "1:1111,johndoe:secret@address=(host=" + hostName
                      + "2)(port=2222)(priority=99)," + "johndoe:secret@(address=" + hostName + "3:3333,priority=98)/db?address=host4:4444&priority=100", null);
              hostIdx = 1;
              for (HostInfo hi : connUrl.getHostsList()) {
                  String testCase = "Host " + hostIdx + ":";
                  assertEquals("johndoe", hi.getUser(), testCase);
                  assertEquals("secret", hi.getPassword(), testCase);
                  assertEquals(hostName + hostIdx, hi.getHost(), testCase);
                  assertEquals(1111 * hostIdx, hi.getPort(), testCase);
                  assertEquals("db", hi.getDatabase(), testCase);
                  assertTrue(hi.getHostProperties().containsKey(PropertyKey.PRIORITY.getKeyName()), testCase);
                  assertEquals(Integer.toString(101 - hostIdx), hi.getHostProperties().get(PropertyKey.PRIORITY.getKeyName()), testCase);
                  hostIdx++;
              }
  
              // Hosts list with "address" splitting (host3:3333) and without priority value (randomly sorted).
              connUrl = ConnectionUrl.getConnectionUrlInstance("mysqlx://johndoe:secret@" + hostName + "1:1111,johndoe:secret@address=(host=" + hostName
                      + "2)(port=2222)," + "johndoe:secret@(address=" + hostName + "3:3333)/db?address=host4:4444", null);
              hosts = IntStream.range(1, 4).mapToObj(i -> hostName + i).collect(Collectors.joining("|"));
              ports = IntStream.range(1, 4).mapToObj(i -> 1111 * i).map(i -> i.toString()).collect(Collectors.joining("|"));
              hostIdx = 1;
              for (HostInfo hi : connUrl.getHostsList()) {
                  String testCase = "Host " + hostIdx + ":";
                  assertEquals("johndoe", hi.getUser(), testCase);
                  assertEquals("secret", hi.getPassword(), testCase);
                  hosts = hosts.replace(hi.getHost(), "");
                  ports = ports.replace(Integer.toString(hi.getPort()), "");
                  assertEquals("db", hi.getDatabase(), testCase);
                  assertFalse(hi.getHostProperties().containsKey(PropertyKey.PRIORITY.getKeyName()), testCase);
                  hostIdx++;
              }
              assertEquals("||", hosts);
              assertEquals("||", ports);
  
              List<String> connStr;
  
              // Error for distinct credentials.
              connStr = new ArrayList<>();
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,janedoe:secret@" + hostName + "2:2222/db");
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,johndoe:public@" + hostName + "2:2222/db");
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,address=(host=" + hostName + "2)(port=2222)(user=janedoe)(password=secret)/db");
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,address=(host=" + hostName + "2)(port=2222)(user=johndoe)(password=public)/db");
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,(host=" + hostName + "2,port=2222,user=janedoe,password=secret)/db");
              connStr.add("mysqlx://johndoe:secret@" + hostName + "1:1111,(host=" + hostName + "2,port=2222,user=johndoe,password=public)/db");
              for (String cs : connStr) {
                  try {
                      connUrl = ConnectionUrl.getConnectionUrlInstance(cs, null);
                      System.out.println(connUrl);
                      fail(cs + ": expected to throw a " + WrongArgumentException.class.getName());
                  } catch (Exception e) {
                      assertTrue(WrongArgumentException.class.isAssignableFrom(e.getClass()),
                              cs + ": expected to throw a " + WrongArgumentException.class.getName());
                      assertEquals(Messages.getString("ConnectionString.14", new Object[] { ConnectionUrl.Type.XDEVAPI_SESSION.getScheme() }), e.getMessage(),
                              cs);
                  }
              }
  
              // Error for missing priority value.
              connStr = new ArrayList<>();
              connStr.add("mysqlx://johndoe:secret@[(address=" + hostName + "1:1111,priority=1)," + hostName + "2:2222]/db");
              connStr.add("mysqlx://johndoe:secret@[(address=" + hostName + "1:1111,priority=1),(address=" + hostName + "2:2222)]/db");
              connStr.add("mysqlx://johndoe:secret@[(address=" + hostName + "1:1111,priority=1),address(host=" + hostName + "2)(port=2222)]/db");
              connStr.add("mysqlx://johndoe:secret@[" + hostName + "1:1111,address=(host=" + hostName + "2)(port=2222)(priority=2)]/db");
              connStr.add("mysqlx://johndoe:secret@[(address=" + hostName + "1:1111),address=(host=" + hostName + "2)(port=2222)(priority=2)]/db");
              connStr.add("mysqlx://johndoe:secret@[address=(host=" + hostName + "1)(port=1111),address=(host=" + hostName + "2)(port=2222)(priority=2)]/db");
              for (String cs : connStr) {
                  try {
                      connUrl = ConnectionUrl.getConnectionUrlInstance(cs, null);
                      System.out.println(connUrl);
                      fail(cs + ": expected to throw a " + WrongArgumentException.class.getName());
                  } catch (Exception e) {
                      assertTrue(WrongArgumentException.class.isAssignableFrom(e.getClass()),
                              cs + ": expected to throw a " + WrongArgumentException.class.getName());
                      assertEquals(Messages.getString("ConnectionString.15", new Object[] { ConnectionUrl.Type.XDEVAPI_SESSION.getScheme() }), e.getMessage(),
                              cs);
                  }
              }
  
              // Error for wrong priority value.
              connStr = new ArrayList<>();
              connStr.add("mysqlx://(address=" + hostName + "1:1111,priority=-1)/db");
              connStr.add("mysqlx://(address=" + hostName + "1:1111,priority=101)/db");
              for (String cs : connStr) {
                  try {
                      connUrl = ConnectionUrl.getConnectionUrlInstance(cs, null);
                      System.out.println(connUrl);
                      fail(cs + ": expected to throw a " + WrongArgumentException.class.getName());
                  } catch (Exception e) {
                      assertTrue(WrongArgumentException.class.isAssignableFrom(e.getClass()),
                              cs + ": expected to throw a " + WrongArgumentException.class.getName());
                      assertEquals(Messages.getString("ConnectionString.16", new Object[] { ConnectionUrl.Type.XDEVAPI_SESSION.getScheme() }), e.getMessage(),
                              cs);
                  }
              }
  
              // Sorting hosts by default priority (random).
              connUrl = ConnectionUrl.getConnectionUrlInstance(
                      "mysqlx://johndoe:secret@[" + hostName + "2," + hostName + "3," + hostName + "1," + hostName + "5," + hostName + "4]/db", null);
              List<HostInfo> hostsList = connUrl.getHostsList();
              assertEquals(connUrl.getMainHost().getHost(), hostsList.get(0).getHost());
              hosts = IntStream.range(1, 6).mapToObj(i -> hostName + i).collect(Collectors.joining("|"));
              for (HostInfo hi : connUrl.getHostsList()) {
                  hosts = hosts.replace(hi.getHost(), "");
              }
              assertEquals("||||", hosts);
  
              // Sorting hosts by defined priority.
              connUrl = ConnectionUrl.getConnectionUrlInstance(
                      "mysqlx://johndoe:secret@[(address=" + hostName + "1,priority=50),(address=" + hostName + "2,priority=100),(address=" + hostName
                              + "3,priority=75)," + "(address=" + hostName + "4,priority=0),(address=" + hostName + "5,priority=25)]/db",
                      null);
              hostsList = connUrl.getHostsList();
              assertEquals(hostName + "2", connUrl.getMainHost().getHost());
              assertEquals(hostName + "2", hostsList.get(0).getHost());
              assertEquals(hostName + "3", hostsList.get(1).getHost());
              assertEquals(hostName + "1", hostsList.get(2).getHost());
              assertEquals(hostName + "5", hostsList.get(3).getHost());
              assertEquals(hostName + "4", hostsList.get(4).getHost());
  
              // Sorting hosts by defined priority, with duplicates.
              connUrl = ConnectionUrl.getConnectionUrlInstance("mysqlx://johndoe:secret@[(address=" + hostName + "1,priority=50),(address=" + hostName
                      + "2,priority=100),(address=" + hostName + "3,priority=10)," + "(address=" + hostName + "4,priority=50),(address=" + hostName
                      + "5,priority=50),(address=" + hostName + "6,priority=75)]/db", null);
              hostsList = connUrl.getHostsList();
              assertEquals(hostName + "2", connUrl.getMainHost().getHost());
              assertEquals(hostName + "2", hostsList.get(0).getHost());
              assertEquals(hostName + "6", hostsList.get(1).getHost());
              hosts = IntStream.of(1, 4, 5).mapToObj(i -> hostName + i).collect(Collectors.joining("|"));
              hosts = hosts.replace(hostsList.get(2).getHost(), "");
              hosts = hosts.replace(hostsList.get(3).getHost(), "");
              hosts = hosts.replace(hostsList.get(4).getHost(), "");
              assertEquals("||", hosts);
              assertEquals(hostName + "3", hostsList.get(5).getHost());
          }
      }
  
      @Test
      public void testReplaceLegacyPropertyValues() throws Exception {
          /* Test zeroDateTimeBehavior convertToNull-> CONVERT_TO_NULL replacement (BUG#91421) */
          List<String> connStr = new ArrayList<>();
          connStr.add("jdbc:mysql://somehost:1234/db");
          connStr.add("jdbc:mysql://somehost:1234/db?key=value&zeroDateTimeBehavior=convertToNull");
          connStr.add("jdbc:mysql://127.0.0.1:1234/db");
          connStr.add("jdbc:mysql://127.0.0.1:1234/db?key=value&zeroDateTimeBehavior=convertToNull");
          connStr.add("jdbc:mysql://(port=3306,user=root,password=pwd,zeroDateTimeBehavior=convertToNull)/test");
          connStr.add("jdbc:mysql://address=(port=3306)(user=root)(password=pwd)(zeroDateTimeBehavior=convertToNull)/test");
  
          Properties props = new Properties();
          props.setProperty(PropertyKey.zeroDateTimeBehavior.getKeyName(), "convertToNull");
  
          for (String cs : connStr) {
              ConnectionUrl connUrl = ConnectionUrl.getConnectionUrlInstance(cs, props);
              assertEquals(ZeroDatetimeBehavior.CONVERT_TO_NULL.name(), connUrl.getMainHost().getProperty(PropertyKey.zeroDateTimeBehavior.getKeyName()));
          }
      }
  
      /**
       * Tests jdbc:mysql+srv: connection strings.
       */
      @Test
      public void testMysqlFailoverDnsSrvConnectionUrl() {
          Properties props;
          props = new Properties();
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Host missing.
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:", props));
  
          // More than one host.
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname1,hostname2", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2", props));
  
          // Port specified.
          props.setProperty(PropertyKey.PORT.getKeyName(), "12345");
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname:12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=true&port=12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname", props));
          props.remove(PropertyKey.PORT.getKeyName());
  
          // Conflicting options.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname?dnsSrv=false", null));
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname", props));
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Setting protocol=PIPE not allowed.
          props.setProperty(PropertyKey.PROTOCOL.getKeyName(), "pipe");
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname?protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=true&protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname", props));
          props.remove(PropertyKey.PROTOCOL.getKeyName());
  
          // Resolving hosts fails.
          assertThrows(CJException.class, "Unable to locate any hosts for hostname\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname", null).getHostsList());
  
          // Correct ConnectionUrl instances - jdbc:mysql+srv:.
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname", null).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname?dnsSrv=true", null).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname?dnsSrv=true", props).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv://hostname?dnsSrv=false", props).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=true", null).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname", props).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=true", props).getClass());
          assertEquals(FailoverDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=false", props).getClass());
  
          // Correct ConnectionUrl instances - jdbc:mysql:.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertEquals(SingleConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=false", null).getClass());
          assertEquals(SingleConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname", props).getClass());
          assertEquals(SingleConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsArv=false", props).getClass());
          assertEquals(SingleConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname?dnsSrv=true", props).getClass());
          assertEquals(FailoverConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2?dnsSrv=false", null).getClass());
          assertEquals(FailoverConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2", props).getClass());
          assertEquals(FailoverConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2?dnsArv=false", props).getClass());
          assertEquals(FailoverConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql://hostname1,hostname2?dnsSrv=true", props).getClass());
      }
  
      /**
       * Tests jdbc:mysql+srv:loadbalance: connection strings.
       */
      @Test
      public void testMysqlLoadBalanceDnsSrvConnectionUrl() {
          Properties props;
          props = new Properties();
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Host missing.
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance:", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance:?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance:", props));
  
          // More than one host.
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname1,hostname2", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2", props));
  
          // Port specified.
          props.setProperty(PropertyKey.PORT.getKeyName(), "12345");
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname:12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true&port=12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname", props));
          props.remove(PropertyKey.PORT.getKeyName());
  
          // Conflicting options.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?dnsSrv=false", null));
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname", props));
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Setting protocol=PIPE not allowed.
          props.setProperty(PropertyKey.PROTOCOL.getKeyName(), "pipe");
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true&protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname", props));
          props.remove(PropertyKey.PROTOCOL.getKeyName());
  
          // Setting loadBalanceConnectionGroup not allowed.
          props.setProperty(PropertyKey.loadBalanceConnectionGroup.getKeyName(), "lbgrp");
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'loadBalanceConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?loadBalanceConnectionGroup=lbgrp", null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'loadBalanceConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true&loadBalanceConnectionGroup=lbgrp", null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'loadBalanceConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname", props));
          props.remove(PropertyKey.loadBalanceConnectionGroup.getKeyName());
  
          // Resolving hosts fails.
          assertThrows(CJException.class, "Unable to locate any hosts for hostname\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname", null).getHostsList());
  
          // Correct ConnectionUrl instances - jdbc:mysql+srv:loadbalance:.
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname", null).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?dnsSrv=true", null).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?dnsSrv=true", props).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:loadbalance://hostname?dnsSrv=false", props).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true", null).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname", props).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true", props).getClass());
          assertEquals(LoadBalanceDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=false", props).getClass());
  
          // Correct ConnectionUrl instances - jdbc:mysql:loadbalance:.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertEquals(LoadBalanceConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=false", null).getClass());
          assertEquals(LoadBalanceConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname", props).getClass());
          assertEquals(LoadBalanceConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsArv=false", props).getClass());
          assertEquals(LoadBalanceConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname?dnsSrv=true", props).getClass());
          assertEquals(LoadBalanceConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2?dnsSrv=false", null).getClass());
          assertEquals(LoadBalanceConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2", props).getClass());
          assertEquals(LoadBalanceConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2?dnsArv=false", props).getClass());
          assertEquals(LoadBalanceConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:loadbalance://hostname1,hostname2?dnsSrv=true", props).getClass());
      }
  
      /**
       * Tests jdbc:mysql+srv:replication: connection strings with deprecated types.
       */
      @Test
      public void testDeprecatedMysqlLoadReplicationDnsSrvConnectionUrl() {
          Properties props = new Properties();
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // More than one host of the same type.
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql+srv:replication://(host=hostname1,type=master),(host=hostname2,type=master),(host=hostname3,type=slave)", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=master),(host=hostname2,type=master),(host=hostname3,type=slave)?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=master),(host=hostname2,type=master),(host=hostname3,type=slave)", props));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql+srv:replication://(host=hostname1,type=master),(host=hostname2,type=slave),(host=hostname3,type=slave)", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=master),(host=hostname2,type=slave),(host=hostname3,type=slave)?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=master),(host=hostname2,type=slave),(host=hostname3,type=slave)", props));
  
          // Correct ConnectionUrl instances - jdbc:mysql:replication:.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=master)(host=hostname2,type=master)(host=hostname2,type=slave)(host=hostname4,type=slave)?dnsSrv=false",
                  null).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=master)(host=hostname2,type=master)(host=hostname2,type=slave)(host=hostname4,type=slave)",
                  props).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=master)(host=hostname2,type=master)(host=hostname2,type=slave)(host=hostname4,type=slave)?dnsArv=false",
                  props).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=master)(host=hostname2,type=master)(host=hostname2,type=slave)(host=hostname4,type=slave)?dnsSrv=true",
                  props).getClass());
      }
  
      /**
       * Tests jdbc:mysql+srv:replication: connection strings.
       */
      @Test
      public void testMysqlLoadReplicationDnsSrvConnectionUrl() {
          Properties props = new Properties();
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Hosts missing.
          assertThrows(InvalidConnectionAttributeException.class,
                  "Exactly two host names of different types are required for DNS SRV lookup enabled replication connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication:", null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "Exactly two host names of different types are required for DNS SRV lookup enabled replication connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication:?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "Exactly two host names of different types are required for DNS SRV lookup enabled replication connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication:", props));
  
          // More than one host of the same type.
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2,hostname3", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2,hostname3?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2,hostname3", props));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql+srv:replication://(host=hostname1,type=source),(host=hostname2,type=source),(host=hostname3,type=replica)", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=source),(host=hostname2,type=source),(host=hostname3,type=replica)?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=source),(host=hostname2,type=source),(host=hostname3,type=replica)", props));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql+srv:replication://(host=hostname1,type=source),(host=hostname2,type=replica),(host=hostname3,type=replica)", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=source),(host=hostname2,type=replica),(host=hostname3,type=replica)?dnsSrv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names for the same type with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance(
                          "jdbc:mysql:replication://(host=hostname1,type=source),(host=hostname2,type=replica),(host=hostname3,type=replica)", props));
  
          // Port specified.
          props.setProperty(PropertyKey.PORT.getKeyName(), "12345");
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1:12345,hostname2", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2:12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true&port=12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2", props));
          props.remove(PropertyKey.PORT.getKeyName());
  
          // Conflicting options.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?dnsSrv=false", null));
          assertThrows(InvalidConnectionAttributeException.class, "'dnsSrv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2", props));
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "true");
  
          // Setting protocol=PIPE not allowed.
          props.setProperty(PropertyKey.PROTOCOL.getKeyName(), "pipe");
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true&protocol=pipe", null));
          assertThrows(InvalidConnectionAttributeException.class, "Using named pipes with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2", props));
          props.remove(PropertyKey.PROTOCOL.getKeyName());
  
          // Setting replicationConnectionGroup not allowed.
          props.setProperty(PropertyKey.replicationConnectionGroup.getKeyName(), "lbgrp");
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'replicationConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?replicationConnectionGroup=lbgrp", null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'replicationConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true&replicationConnectionGroup=lbgrp",
                          null));
          assertThrows(InvalidConnectionAttributeException.class,
                  "The option 'replicationConnectionGroup' cannot be set\\. Live management of connections is not supported with DNS SRV lookup\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2", props));
          props.remove(PropertyKey.replicationConnectionGroup.getKeyName());
  
          // Resolving hosts fails.
          assertThrows(CJException.class, "Unable to locate any hosts for hostname1\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2", null).getHostsList(HostsListView.SOURCES));
          assertThrows(CJException.class, "Unable to locate any hosts for hostname2\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2", null).getHostsList(HostsListView.REPLICAS));
  
          // Correct ConnectionUrl instances - jdbc:mysql+srv:replication:.
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2", null).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?dnsSrv=true", null).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?dnsSrv=true", props).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql+srv:replication://hostname1,hostname2?dnsSrv=false", props).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true", null).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2", props).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true", props).getClass());
          assertEquals(ReplicationDnsSrvConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=false", props).getClass());
  
          // Correct ConnectionUrl instances - jdbc:mysql:replication:.
          props.setProperty(PropertyKey.dnsSrv.getKeyName(), "false");
          assertEquals(ReplicationConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=false", null).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2", props).getClass());
          assertEquals(ReplicationConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsArv=false", props).getClass());
          assertEquals(ReplicationConnectionUrl.class,
                  ConnectionUrl.getConnectionUrlInstance("jdbc:mysql:replication://hostname1,hostname2?dnsSrv=true", props).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=source)(host=hostname2,type=source)(host=hostname2,type=replica)(host=hostname4,type=replica)?dnsSrv=false",
                  null).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=source)(host=hostname2,type=source)(host=hostname2,type=replica)(host=hostname4,type=replica)",
                  props).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=source)(host=hostname2,type=source)(host=hostname2,type=replica)(host=hostname4,type=replica)?dnsArv=false",
                  props).getClass());
          assertEquals(ReplicationConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance(
                  "jdbc:mysql:replication://(host=hostname1,type=source)(host=hostname2,type=source)(host=hostname2,type=replica)(host=hostname4,type=replica)?dnsSrv=true",
                  props).getClass());
      }
  
      /**
       * Tests mysqlx+srv: connection strings.
       */
      @Test
      public void testMysqlxDnsSrvConnectionUrl() {
          Properties props;
          props = new Properties();
          props.setProperty(PropertyKey.xdevapiDnsSrv.getKeyName(), "true");
  
          // Host missing.
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv:", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx:?xdevapi.dns-srv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "A host name is required for DNS SRV lookup enabled connections\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx:", props));
  
          // More than one host.
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname1,hostname2", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname1,hostname2?xdevapi.dns-srv=true", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying multiple host names with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname1,hostname2", props));
  
          // Port specified.
          props.setProperty(PropertyKey.PORT.getKeyName(), "12345");
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname:12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=true&port=12345", null));
          assertThrows(InvalidConnectionAttributeException.class, "Specifying a port number with DNS SRV lookup is not allowed\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname", props));
          props.remove(PropertyKey.PORT.getKeyName());
  
          // Conflicting options.
          props.setProperty(PropertyKey.xdevapiDnsSrv.getKeyName(), "false");
          assertThrows(InvalidConnectionAttributeException.class, "'xdevapi\\.dns-srv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname?xdevapi.dns-srv=false", null));
          assertThrows(InvalidConnectionAttributeException.class, "'xdevapi\\.dns-srv' cannot be set to false with DNS SRV lookup enabled\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname", props));
          props.setProperty(PropertyKey.xdevapiDnsSrv.getKeyName(), "true");
  
          // Resolving hosts fails.
          assertThrows(CJException.class, "Unable to locate any hosts for hostname\\.",
                  () -> ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname", null).getHostsList());
  
          // Correct ConnectionUrl instances - mysqlx+srv:.
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname", null).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname?xdevapi.dns-srv=true", null).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname?xdevapi.dns-srv=true", props).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx+srv://hostname?xdevapi.dns-srv=false", props).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=true", null).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname", props).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=true", props).getClass());
          assertEquals(XDevApiDnsSrvConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=false", props).getClass());
  
          // Correct ConnectionUrl instances - mysqlx:.
          props.setProperty(PropertyKey.xdevapiDnsSrv.getKeyName(), "false");
          assertEquals(XDevApiConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=false", null).getClass());
          assertEquals(XDevApiConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname", props).getClass());
          assertEquals(XDevApiConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=false", props).getClass());
          assertEquals(XDevApiConnectionUrl.class, ConnectionUrl.getConnectionUrlInstance("mysqlx://hostname?xdevapi.dns-srv=true", props).getClass());
      }
  
      /**
       * Tests fix for BUG#28150662, CONNECTOR/J 8 MALFORMED DATABASE URL EXCEPTION WHIT CORRECT URL STRING.
       */
      @Test
      public void testBug28150662() {
          List<String> connStr = new ArrayList<>();
          connStr.add(
                  "jdbc:mysql://localhost:3306/db1?connectionCollation=utf8mb4_unicode_ci&user=user1&sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0");
          connStr.add(
                  "jdbc:mysql://localhost:3306/db1?connectionCollation=utf8mb4_unicode_ci&sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0&user=user1");
          connStr.add(
                  "jdbc:mysql://address=(host=localhost)(port=3306)(connectionCollation=utf8mb4_unicode_ci)(sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0)(user=user1)/db1");
          connStr.add(
                  "jdbc:mysql://(host=localhost,port=3306,connectionCollation=utf8mb4_unicode_ci,sessionVariables=sql_mode='IGNORE_SPACE%2CANSI'%2CFOREIGN_KEY_CHECKS=0,user=user1)/db1");
  
          connStr.add(
                  "jdbc:mysql://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:3306/db1?connectionCollation=utf8mb4_unicode_ci&user=user1&sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0");
          connStr.add(
                  "jdbc:mysql://verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789:3306/db1?connectionCollation=utf8mb4_unicode_ci&sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0&user=user1");
          connStr.add(
                  "jdbc:mysql://address=(host=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789)(port=3306)(connectionCollation=utf8mb4_unicode_ci)(sessionVariables=sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0)(user=user1)/db1");
          connStr.add(
                  "jdbc:mysql://(host=verylonghostname01234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789,port=3306,connectionCollation=utf8mb4_unicode_ci,sessionVariables=sql_mode='IGNORE_SPACE%2CANSI'%2CFOREIGN_KEY_CHECKS=0,user=user1)/db1");
  
          for (String cs : connStr) {
              ConnectionUrl url = ConnectionUrl.getConnectionUrlInstance(cs, null);
              HostInfo hi = url.getMainHost();
              assertEquals("utf8mb4_unicode_ci", hi.getHostProperties().get("connectionCollation"));
              assertEquals("user1", hi.getUser());
              assertEquals("sql_mode='IGNORE_SPACE,ANSI',FOREIGN_KEY_CHECKS=0", hi.getHostProperties().get("sessionVariables"));
          }
      }
  }
  