com.mysql.cj.conf.ConnectionUrl Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of mysql-connector-java
Show all versions of mysql-connector-java
JDBC Type 4 driver for MySQL
/*
* Copyright (c) 2016, 2018, Oracle and/or its affiliates. All rights reserved.
*
* 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.conf;
import static com.mysql.cj.util.StringUtils.isNullOrEmpty;
import java.io.IOException;
import java.io.InputStream;
import java.sql.DriverManager;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.TreeMap;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.stream.Collectors;
import com.mysql.cj.Messages;
import com.mysql.cj.conf.PropertyDefinitions.PropertyKey;
import com.mysql.cj.exceptions.CJException;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.InvalidConnectionAttributeException;
import com.mysql.cj.exceptions.UnsupportedConnectionStringException;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.util.LRUCache;
import com.mysql.cj.util.Util;
/**
* A container for a database URL and a collection of given connection arguments.
* The connection string is parsed and split by its components, each of which is then processed and fixed according to the needs of the connection type.
* This abstract class holds all common behavior to all connection string types. Its subclasses must implement their own specifics such as classifying hosts by
* type or apply validation rules.
*/
public abstract class ConnectionUrl implements DatabaseUrlContainer {
private static final String DEFAULT_HOST = "localhost";
private static final int DEFAULT_PORT = 3306;
private static final LRUCache connectionUrlCache = new LRUCache<>(100);
private static final ReadWriteLock rwLock = new ReentrantReadWriteLock();
/**
* The rules describing the number of hosts a database URL may contain.
*/
public enum HostsCardinality {
SINGLE {
@Override
public boolean assertSize(int n) {
return n == 1;
}
},
MULTIPLE {
@Override
public boolean assertSize(int n) {
return n > 1;
}
},
ONE_OR_MORE {
@Override
public boolean assertSize(int n) {
return n >= 1;
}
};
public abstract boolean assertSize(int n);
}
/**
* The database URL type which is determined by the scheme section of the connection string.
*/
public enum Type {
SINGLE_CONNECTION("jdbc:mysql:", HostsCardinality.SINGLE), //
FAILOVER_CONNECTION("jdbc:mysql:", HostsCardinality.MULTIPLE), //
LOADBALANCE_CONNECTION("jdbc:mysql:loadbalance:", HostsCardinality.ONE_OR_MORE), //
REPLICATION_CONNECTION("jdbc:mysql:replication:", HostsCardinality.ONE_OR_MORE), //
XDEVAPI_SESSION("mysqlx:", HostsCardinality.ONE_OR_MORE);
private String scheme;
private HostsCardinality cardinality;
private Type(String scheme, HostsCardinality cardinality) {
this.scheme = scheme;
this.cardinality = cardinality;
}
public String getScheme() {
return this.scheme;
}
public HostsCardinality getCardinality() {
return this.cardinality;
}
/**
* Returns the {@link Type} corresponding to the given scheme and number of hosts, if any.
* Otherwise throws an {@link UnsupportedConnectionStringException}.
* Calling this method with the argument n lower than 0 skips the hosts cardinality validation.
*
* @param scheme
* one of supported schemes
* @param n
* the number of hosts in the database URL
* @return the {@link Type} corresponding to the given protocol and number of hosts
*/
public static Type fromValue(String scheme, int n) {
for (Type t : values()) {
if (t.getScheme().equalsIgnoreCase(scheme) && (n < 0 || t.getCardinality().assertSize(n))) {
return t;
}
}
if (n < 0) {
throw ExceptionFactory.createException(UnsupportedConnectionStringException.class,
Messages.getString("ConnectionString.5", new Object[] { scheme }));
}
throw ExceptionFactory.createException(UnsupportedConnectionStringException.class,
Messages.getString("ConnectionString.6", new Object[] { scheme, n }));
}
/**
* Checks if the given scheme corresponds to one of the connection types the driver supports.
*
* @param scheme
* scheme part from connection string, like "jdbc:mysql:"
* @return true if the given scheme is supported by driver
*/
public static boolean isSupported(String scheme) {
for (Type t : values()) {
if (t.getScheme().equalsIgnoreCase(scheme)) {
return true;
}
}
return false;
}
}
protected Type type;
protected String originalConnStr;
protected String originalDatabase;
protected List hosts = new ArrayList<>();
protected Map properties = new HashMap<>();
ConnectionPropertiesTransform propertiesTransformer;
/**
* Static factory method that returns either a new instance of a {@link ConnectionUrl} or a cached one.
* Returns "null" it can't handle the connection string.
*
* @param connString
* the connection string
* @param info
* the connection arguments map
* @return an instance of a {@link ConnectionUrl} or "null" if isn't able to handle the connection string
*/
public static ConnectionUrl getConnectionUrlInstance(String connString, Properties info) {
if (connString == null) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0"));
}
String connStringCacheKey = buildConnectionStringCacheKey(connString, info);
ConnectionUrl connectionString;
rwLock.readLock().lock();
connectionString = connectionUrlCache.get(connStringCacheKey);
if (connectionString == null) {
rwLock.readLock().unlock();
rwLock.writeLock().lock();
try {
// Check again, in the meantime it could have been cached by another thread.
connectionString = connectionUrlCache.get(connStringCacheKey);
if (connectionString == null) {
ConnectionUrlParser connStrParser = ConnectionUrlParser.parseConnectionString(connString);
switch (Type.fromValue(connStrParser.getScheme(), connStrParser.getHosts().size())) {
case SINGLE_CONNECTION:
connectionString = (ConnectionUrl) Util.getInstance("com.mysql.cj.conf.url.SingleConnectionUrl",
new Class[] { ConnectionUrlParser.class, Properties.class }, new Object[] { connStrParser, info }, null);
break;
case FAILOVER_CONNECTION:
connectionString = (ConnectionUrl) Util.getInstance("com.mysql.cj.conf.url.FailoverConnectionUrl",
new Class[] { ConnectionUrlParser.class, Properties.class }, new Object[] { connStrParser, info }, null);
break;
case LOADBALANCE_CONNECTION:
connectionString = (ConnectionUrl) Util.getInstance("com.mysql.cj.conf.url.LoadbalanceConnectionUrl",
new Class[] { ConnectionUrlParser.class, Properties.class }, new Object[] { connStrParser, info }, null);
break;
case REPLICATION_CONNECTION:
connectionString = (ConnectionUrl) Util.getInstance("com.mysql.cj.conf.url.ReplicationConnectionUrl",
new Class[] { ConnectionUrlParser.class, Properties.class }, new Object[] { connStrParser, info }, null);
break;
case XDEVAPI_SESSION:
connectionString = (ConnectionUrl) Util.getInstance("com.mysql.cj.conf.url.XDevAPIConnectionUrl",
new Class[] { ConnectionUrlParser.class, Properties.class }, new Object[] { connStrParser, info }, null);
break;
default:
return null; // should not happen
}
connectionUrlCache.put(connStringCacheKey, connectionString);
}
rwLock.readLock().lock();
} finally {
rwLock.writeLock().unlock();
}
}
rwLock.readLock().unlock();
return connectionString;
}
/**
* Builds a connection URL cache map key based on the connection string itself plus the string representation of the given connection properties.
*
* @param connString
* the connection string
* @param info
* the connection arguments map
* @return a connection string cache map key
*/
private static String buildConnectionStringCacheKey(String connString, Properties info) {
StringBuilder sbKey = new StringBuilder(connString);
sbKey.append("??");
sbKey.append(
info == null ? null : info.stringPropertyNames().stream().map(k -> k + "=" + info.getProperty(k)).collect(Collectors.joining(", ", "{", "}")));
return sbKey.toString();
}
/**
* Checks if this {@link ConnectionUrl} is able to process the given database URL.
*
* @param connString
* the connection string
* @return true if this class is able to process the given URL, false otherwise
*/
public static boolean acceptsUrl(String connString) {
return ConnectionUrlParser.isConnectionStringSupported(connString);
}
/**
* Empty constructor. Required for subclasses initialization.
*/
protected ConnectionUrl() {
}
/**
* Constructor for unsupported URLs
*
* @param origUrl
* URLs
*/
public ConnectionUrl(String origUrl) {
this.originalConnStr = origUrl;
}
/**
* Constructs an instance of {@link ConnectionUrl}, performing all the required initializations.
*
* @param connStrParser
* a {@link ConnectionUrlParser} instance containing the parsed version of the original connection string
* @param info
* the connection arguments map
*/
protected ConnectionUrl(ConnectionUrlParser connStrParser, Properties info) {
this.originalConnStr = connStrParser.getDatabaseUrl();
this.originalDatabase = connStrParser.getPath() == null ? "" : connStrParser.getPath();
collectProperties(connStrParser, info); // Fill properties before filling hosts info.
collectHostsInfo(connStrParser);
}
/**
* Joins the connection arguments from the connection string with the ones from the given connection arguments map collecting them in a single map.
* Additionally may also collect other connection arguments from configuration files.
*
* @param connStrParser
* the {@link ConnectionUrlParser} from where to collect the properties
* @param info
* the connection arguments map
*/
protected void collectProperties(ConnectionUrlParser connStrParser, Properties info) {
// Fill in the properties from the connection string.
connStrParser.getProperties().entrySet().stream().forEach(e -> this.properties.put(PropertyKey.normalizeCase(e.getKey()), e.getValue()));
// Properties passed in override the ones from the connection string.
if (info != null) {
info.stringPropertyNames().stream().forEach(k -> this.properties.put(PropertyKey.normalizeCase(k), info.getProperty(k)));
}
// Collect properties from additional sources.
processColdFusionAutoConfiguration();
setupPropertiesTransformer();
expandPropertiesFromConfigFiles(this.properties);
injectPerTypeProperties(this.properties);
}
/**
* Checks if the conditions for the Cold Fusion auto configuration file are met. If so, adds a reference to its configuration file so that it can be loaded
* afterwards.
*/
protected void processColdFusionAutoConfiguration() {
if (Util.isColdFusion()) {
String autoConfigCf = this.properties.get(PropertyDefinitions.PNAME_autoConfigureForColdFusion);
if (autoConfigCf == null || autoConfigCf.equalsIgnoreCase("TRUE") || autoConfigCf.equalsIgnoreCase("YES")) {
String currentConfigFiles = this.properties.get(PropertyDefinitions.PNAME_useConfigs);
StringBuilder newConfigFiles = new StringBuilder();
if (currentConfigFiles != null) {
newConfigFiles.append(currentConfigFiles).append(",");
}
newConfigFiles.append("coldFusion");
this.properties.put(PropertyDefinitions.PNAME_useConfigs, newConfigFiles.toString());
}
}
}
/**
* Sets up the {@link ConnectionPropertiesTransform} if one was provided.
*/
protected void setupPropertiesTransformer() {
String propertiesTransformClassName = this.properties.get(PropertyDefinitions.PNAME_propertiesTransform);
if (!isNullOrEmpty(propertiesTransformClassName)) {
try {
this.propertiesTransformer = (ConnectionPropertiesTransform) Class.forName(propertiesTransformClassName).newInstance();
} catch (InstantiationException | IllegalAccessException | ClassNotFoundException | CJException e) {
throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,
Messages.getString("ConnectionString.9", new Object[] { propertiesTransformClassName, e.toString() }), e);
}
}
}
/**
* Expands the connection argument "useConfig" by reading the mentioned configuration files.
*
* @param props
* a connection arguments map from where to read the "useConfig" property and where to save the loaded properties.
*/
protected void expandPropertiesFromConfigFiles(Map props) {
// Properties from config files should not override the existing ones.
String configFiles = props.get(PropertyDefinitions.PNAME_useConfigs);
if (!isNullOrEmpty(configFiles)) {
Properties configProps = getPropertiesFromConfigFiles(configFiles);
configProps.stringPropertyNames().stream().map(PropertyKey::normalizeCase).filter(k -> !props.containsKey(k))
.forEach(k -> props.put(k, configProps.getProperty(k)));
}
}
/**
* Returns a map containing the properties read from the given configuration files. Multiple files can be referenced using a comma as separator.
*
* @param configFiles
* the list of the configuration files to read
* @return the map containing all the properties read
*/
public static Properties getPropertiesFromConfigFiles(String configFiles) {
Properties configProps = new Properties();
for (String configFile : configFiles.split(",")) {
try (InputStream configAsStream = ConnectionUrl.class.getResourceAsStream("/com/mysql/cj/configurations/" + configFile + ".properties")) {
if (configAsStream == null) {
throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,
Messages.getString("ConnectionString.10", new Object[] { configFile }));
}
configProps.load(configAsStream);
} catch (IOException e) {
throw ExceptionFactory.createException(InvalidConnectionAttributeException.class,
Messages.getString("ConnectionString.11", new Object[] { configFile }), e);
}
}
return configProps;
}
/**
* Subclasses must override this method if they need to inject additional properties in the connection arguments map while it's being constructed.
*
* @param props
* the properties already containing all known connection arguments
*/
protected void injectPerTypeProperties(Map props) {
return;
}
/**
* Collects the hosts information from the {@link ConnectionUrlParser}.
*
* @param connStrParser
* the {@link ConnectionUrlParser} from where to collect the hosts information
*/
protected void collectHostsInfo(ConnectionUrlParser connStrParser) {
connStrParser.getHosts().stream().map(this::fixHostInfo).forEach(this.hosts::add);
}
/**
* Fixes the host information by moving data around and filling in missing data.
* Applies properties transformations to the collected properties if {@link ConnectionPropertiesTransform} was declared in the connection arguments.
*
* @param hi
* the host information data to fix
* @return a new {@link HostInfo} with all required data
*/
protected HostInfo fixHostInfo(HostInfo hi) {
Map hostProps = new HashMap<>();
// Add global connection arguments.
hostProps.putAll(this.properties);
// Add/override host specific connection arguments.
hi.getHostProperties().entrySet().stream().forEach(e -> hostProps.put(PropertyKey.normalizeCase(e.getKey()), e.getValue()));
// Add the database name
hostProps.put(PropertyKey.DBNAME.getKeyName(), getDatabase());
preprocessPerTypeHostProperties(hostProps);
String host = hostProps.remove(PropertyKey.HOST.getKeyName());
if (!isNullOrEmpty(hi.getHost())) {
host = hi.getHost();
} else if (isNullOrEmpty(host)) {
host = getDefaultHost();
}
String portAsString = hostProps.remove(PropertyKey.PORT.getKeyName());
int port = hi.getPort();
if (port == -1 && !isNullOrEmpty(portAsString)) {
try {
port = Integer.valueOf(portAsString);
} catch (NumberFormatException e) {
throw ExceptionFactory.createException(WrongArgumentException.class,
Messages.getString("ConnectionString.7", new Object[] { hostProps.get(PropertyKey.PORT.getKeyName()) }), e);
}
}
if (port == -1) {
port = getDefaultPort();
}
String user = hostProps.remove(PropertyKey.USER.getKeyName());
if (!isNullOrEmpty(hi.getUser())) {
user = hi.getUser();
} else if (isNullOrEmpty(user)) {
user = getDefaultUser();
}
boolean isPasswordless = hi.isPasswordless();
String password = hostProps.remove(PropertyKey.PASSWORD.getKeyName());
if (!isPasswordless) {
password = hi.getPassword();
} else if (password == null) {
password = getDefaultPassword();
isPasswordless = true;
} else {
isPasswordless = false;
}
expandPropertiesFromConfigFiles(hostProps);
fixProtocolDependencies(hostProps);
return buildHostInfo(host, port, user, password, isPasswordless, hostProps);
}
/**
* Subclasses should override this to perform any required pre-processing on the host information properties.
*
* @param hostProps
* the host properties map to process
*/
protected void preprocessPerTypeHostProperties(Map hostProps) {
// To be overridden in subclasses if needed.
}
/**
* Returns the default host. Subclasses must override this method if they have different default host value.
*
* @return the default host
*/
public String getDefaultHost() {
return DEFAULT_HOST;
}
/**
* Returns the default port. Subclasses must override this method if they have different default port value.
*
* @return the default port
*/
public int getDefaultPort() {
return DEFAULT_PORT;
}
/**
* Returns the default user. Usually the one provided in the method {@link DriverManager#getConnection(String, String, String)} or as connection argument.
*
* @return the default user
*/
public String getDefaultUser() {
String user = this.properties.get(PropertyKey.USER.getKeyName());
return isNullOrEmpty(user) ? "" : user;
}
/**
* Returns the default password. Usually the one provided in the method {@link DriverManager#getConnection(String, String, String)} or as connection
* argument.
*
* @return the default password
*/
public String getDefaultPassword() {
String password = this.properties.get(PropertyKey.PASSWORD.getKeyName());
return isNullOrEmpty(password) ? "" : password;
}
/**
* Fixes the protocol (TCP vs PIPE) dependencies for the given host properties map.
*
* @param hostProps
* the host properties map to fix
*/
protected void fixProtocolDependencies(Map hostProps) {
String protocol = hostProps.get(PropertyKey.PROTOCOL.getKeyName());
if (!isNullOrEmpty(protocol) && protocol.equalsIgnoreCase("PIPE")) {
if (!hostProps.containsKey(PropertyDefinitions.PNAME_socketFactory)) {
hostProps.put(PropertyDefinitions.PNAME_socketFactory, "com.mysql.cj.protocol.NamedPipeSocketFactory");
}
if (hostProps.containsKey(PropertyKey.PATH.getKeyName()) && !hostProps.containsKey(PropertyDefinitions.NAMED_PIPE_PROP_NAME)) {
hostProps.put(PropertyDefinitions.NAMED_PIPE_PROP_NAME, hostProps.get(PropertyKey.PATH.getKeyName()));
}
}
}
/**
* Returns this connection URL type.
*
* @return the connection URL type
*/
public Type getType() {
return this.type;
}
/**
* Returns the original database URL that produced this connection string.
*
* @return the original database URL
*/
@Override
public String getDatabaseUrl() {
return this.originalConnStr;
}
/**
* Returns the database from this connection URL. Note that a "DBNAME" property overrides the database identified in the connection string.
*
* @return the database name
*/
public String getDatabase() {
return this.properties.containsKey(PropertyKey.DBNAME.getKeyName()) ? this.properties.get(PropertyKey.DBNAME.getKeyName()) : this.originalDatabase;
}
/**
* Returns the number of hosts in this connection URL.
*
* @return the number of hosts
*/
public int hostsCount() {
return this.hosts.size();
}
/**
* Returns the single or first host info structure.
*
* @return the first host info structure
*/
public HostInfo getMainHost() {
return this.hosts.isEmpty() ? null : this.hosts.get(0);
}
/**
* Returns a list of the hosts in this connection URL.
*
* @return the hosts list from this connection URL
*/
public List getHostsList() {
return Collections.unmodifiableList(this.hosts);
}
/**
* Returns an existing host info with the same host:port part or spawns a new isolated host info based on this connection URL if none was found.
*
* @param hostPortPair
* the host:port part to search for
* @return the existing host info or a new independent one
*/
public HostInfo getHostOrSpawnIsolated(String hostPortPair) {
return getHostOrSpawnIsolated(hostPortPair, this.hosts);
}
/**
* Returns an existing host info with the same host:port part or spawns a new isolated host info based on this connection URL if none was found.
*
* @param hostPortPair
* the host:port part to search for
* @param hostsList
* the hosts list from where to search the host list
* @return the existing host info or a new independent one
*/
public HostInfo getHostOrSpawnIsolated(String hostPortPair, List hostsList) {
for (HostInfo hi : hostsList) {
if (hostPortPair.equals(hi.getHostPortPair())) {
return hi;
}
}
ConnectionUrlParser.Pair hostAndPort = ConnectionUrlParser.parseHostPortPair(hostPortPair);
String host = hostAndPort.left;
Integer port = hostAndPort.right;
String user = getDefaultUser();
String password = getDefaultPassword();
return buildHostInfo(host, port, user, password, true, this.properties);
}
/**
* Creates a new {@link HostInfo} structure with the given components, passing through the properties transformer if there is one defined in this connection
* string;
*
* @param host
* the host
* @param port
* the port
* @param user
* the user name
* @param password
* the password
* @param isDefaultPwd
* no password was provided in the connection URL or arguments?
* @param hostProps
* the host properties map
* @return a new instance of {@link HostInfo}
*/
private HostInfo buildHostInfo(String host, int port, String user, String password, boolean isDefaultPwd, Map hostProps) {
// Apply properties transformations if needed.
if (this.propertiesTransformer != null) {
Properties props = new Properties();
props.putAll(hostProps);
props.setProperty(PropertyKey.HOST.getKeyName(), host);
props.setProperty(PropertyKey.PORT.getKeyName(), String.valueOf(port));
props.setProperty(PropertyKey.USER.getKeyName(), user);
props.setProperty(PropertyKey.PASSWORD.getKeyName(), password);
Properties transformedProps = this.propertiesTransformer.transformProperties(props);
host = transformedProps.getProperty(PropertyKey.PORT.getKeyName());
try {
port = Integer.parseInt(transformedProps.getProperty(PropertyKey.PORT.getKeyName()));
} catch (NumberFormatException e) {
throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.8",
new Object[] { PropertyKey.PORT.getKeyName(), transformedProps.getProperty(PropertyKey.PORT.getKeyName()) }), e);
}
user = transformedProps.getProperty(PropertyKey.USER.getKeyName());
password = transformedProps.getProperty(PropertyKey.PASSWORD.getKeyName());
Map transformedHostProps = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
transformedProps.stringPropertyNames().stream().forEach(k -> transformedHostProps.put(k, transformedProps.getProperty(k)));
// Remove surplus keys.
transformedHostProps.remove(PropertyKey.HOST.getKeyName());
transformedHostProps.remove(PropertyKey.PORT.getKeyName());
transformedHostProps.remove(PropertyKey.USER.getKeyName());
transformedHostProps.remove(PropertyKey.PASSWORD.getKeyName());
hostProps = transformedHostProps;
}
return new HostInfo(this, host, port, user, password, isDefaultPwd, hostProps);
}
/**
* Returns the original (common to all hosts) connection arguments as provided in the connection string query section.
*
* @return the original (common to all hosts) connection arguments
*/
public Map getOriginalProperties() {
return Collections.unmodifiableMap(this.properties);
}
/**
* Returns a {@link Properties} instance containing the connection arguments extracted from the URL query section, i.e., per host attributes are excluded.
* Applies properties transformations to the collected properties if {@link ConnectionPropertiesTransform} was declared in the connection arguments.
*
* @return a {@link Properties} instance containing the common connection arguments.
*/
public Properties getConnectionArgumentsAsProperties() {
Properties props = new Properties();
if (this.properties != null) {
props.putAll(this.properties);
}
return this.propertiesTransformer != null ? this.propertiesTransformer.transformProperties(props) : props;
}
/**
* Returns a string representation of this object.
*
* @return a string representation of this object
*/
@Override
public String toString() {
StringBuilder asStr = new StringBuilder(super.toString());
asStr.append(String.format(" :: {type: \"%s\", hosts: %s, database: \"%s\", properties: %s, propertiesTransformer: %s}", this.type, this.hosts,
this.originalDatabase, this.properties, this.propertiesTransformer));
return asStr.toString();
}
}