/*
* Copyright 2008-present MongoDB, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.mongodb;
import com.mongodb.connection.StreamFactoryFactory;
import com.mongodb.diagnostics.logging.Logger;
import com.mongodb.diagnostics.logging.Loggers;
import com.mongodb.internal.dns.DefaultDnsResolver;
import com.mongodb.lang.Nullable;
import org.bson.UuidRepresentation;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import static java.lang.String.format;
import static java.util.Arrays.asList;
import static java.util.Collections.singletonList;
import static java.util.Collections.unmodifiableList;
Represents a Connection String. The Connection String describes the
hosts to be used and options.
The format of the Connection String is:
mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]]
mongodb://
is a required prefix to identify that this is a string in the standard connection format.
username:password@
are optional. If given, the driver will attempt to login to a database after connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not, in which case the ":" after the username is left off as well
host1
is the only required part of the connection string. It identifies a server address to connect to. Support for Unix domain sockets was added in 3.7. Note: The path must be urlencoded eg: mongodb://%2Ftmp%2Fmongodb-27017.sock
and the jnr.unixsocket
library installed.
:portX
is optional and defaults to :27017 if not provided.
/database
is the name of the database to login to and thus is only relevant if the username:password@
syntax is used. If not specified the "admin" database will be used by default.
?options
are connection options. Note that if database
is absent there is still a /
required between the last host and the ?
introducing the options. Options are name=value pairs and the pairs are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&", but should be considered as deprecated.
An alternative format, using the mongodb+srv protocol, is:
mongodb+srv://[username:password@]host[/[database][?options]]
mongodb+srv://
is a required prefix for this format.
username:password@
are optional. If given, the driver will attempt to login to a database after connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not, in which case the ":" after the username is left off as well
host
is the only required part of the URI. It identifies a single host name for which SRV records are looked up from a Domain Name Server after prefixing the host name with "_mongodb._tcp"
. The host/port for each SRV record becomes the seed list used to connect, as if each one were provided as host/port pair in a URI using the normal mongodb protocol.
/database
is the name of the database to login to and thus is only relevant if the username:password@
syntax is used. If not specified the "admin" database will be used by default.
?options
are connection options. Note that if database
is absent there is still a /
required between the last host and the ?
introducing the options. Options are name=value pairs and the pairs are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&", but should be considered as deprecated. Additionally with the mongodb+srv protocol, TXT records are looked up from a Domain Name Server for the given host, and the text value of each one is prepended to any options on the URI itself. Because the last specified value for any option wins, that means that options provided on the URI will override any that are provided via TXT records.
The following options are supported (case insensitive):
Server Selection Configuration:
serverSelectionTimeoutMS=ms
: How long the driver will wait for server selection to succeed before throwing an exception.
localThresholdMS=ms
: When choosing among multiple MongoDB servers to send a request, the driver will only send that request to a server whose ping time is less than or equal to the server with the fastest ping time plus the local threshold.
Server Monitoring Configuration:
heartbeatFrequencyMS=ms
: The frequency that the driver will attempt to determine the current state of each server in the cluster.
Replica set configuration:
replicaSet=name
: Implies that the hosts given are a seed list, and the driver will attempt to find all members of the set.
Connection Configuration:
ssl=true|false
: Whether to connect using TLS.
tls=true|false
: Whether to connect using TLS. Supersedes the ssl option
tlsInsecure=true|false
: If connecting with TLS, this option enables insecure TLS connections. Currently this has the same effect of setting tlsAllowInvalidHostnames to true. Other mechanism for relaxing TLS security constraints must be handled in the application by customizing the SSLContext
sslInvalidHostNameAllowed=true|false
: Whether to allow invalid host names for TLS connections.
tlsAllowInvalidHostnames=true|false
: Whether to allow invalid host names for TLS connections. Supersedes the sslInvalidHostNameAllowed option
connectTimeoutMS=ms
: How long a connection can take to be opened before timing out.
socketTimeoutMS=ms
: How long a send or receive on a socket can take before timing out.
maxIdleTimeMS=ms
: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed
maxLifeTimeMS=ms
: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed
streamType=nio2|netty
: The stream type to use for connections. If unspecified, nio2 will be used for asynchronous clients. Note that this query parameter has been deprecated and applications should use Builder.streamFactoryFactory(StreamFactoryFactory)
instead.
Connection pool configuration:
maxPoolSize=n
: The maximum number of connections in the connection pool.
waitQueueMultiple=n
: this multiplier, multiplied with the maxPoolSize setting, gives the maximum number of threads that may be waiting for a connection to become available from the pool. All further threads will get an exception right away. Note that this configuration option is deprecated and will be removed in the next major release.
waitQueueTimeoutMS=ms
: The maximum wait time in milliseconds that a thread may wait for a connection to become available.
Write concern configuration:
safe=true|false
true
: the driver ensures that all writes are acknowledged by the MongoDB server, or else throws an exception. (see also w
and wtimeoutMS
).
false
: the driver does not ensure that all writes are acknowledged by the MongoDB server.
journal=true|false
true
: the driver waits for the server to group commit to the journal file on disk.
false
: the driver does not wait for the server to group commit to the journal file on disk.
w=wValue
- The driver adds { w : wValue } to all write commands. Implies
safe=true
.
- wValue is typically a number, but can be any string in order to allow for specifications like
"majority"
wtimeoutMS=ms
- The driver adds { wtimeout : ms } to all write commands. Implies
safe=true
.
- Used in combination with
w
Read preference configuration:
readPreference=enum
: The read preference for this connection.
- Enumerated values:
primary
primaryPreferred
secondary
secondaryPreferred
nearest
readPreferenceTags=string
. A representation of a tag set as a comma-separated list of colon-separated key-value pairs, e.g. "dc:ny,rack:1
". Spaces are stripped from beginning and end of all keys and values. To specify a list of tag sets, using multiple readPreferenceTags, e.g. readPreferenceTags=dc:ny,rack:1;readPreferenceTags=dc:ny;readPreferenceTags=
- Note the empty value for the last one, which means match any secondary as a last resort.
- Order matters when using multiple readPreferenceTags.
maxStalenessSeconds=seconds
. The maximum staleness in seconds. For use with any non-primary read preference, the driver estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses, and selects only those secondaries whose staleness is less than or equal to maxStalenessSeconds. Not providing the parameter or explicitly setting it to -1 indicates that there should be no max staleness check. The maximum staleness feature is designed to prevent badly-lagging servers from being selected. The staleness estimate is imprecise and shouldn't be used to try to select "up-to-date" secondaries. The minimum value is either 90 seconds, or the heartbeat frequency plus 10 seconds, whichever is greatest.
Authentication configuration:
authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509
: The authentication mechanism to use if a credential was supplied. The default is unspecified, in which case the client will pick the most secure mechanism available based on the sever version. For the GSSAPI and MONGODB-X509 mechanisms, no password is accepted, only the username.
authSource=string
: The source of the authentication credentials. This is typically the database that the credentials have been created. The value defaults to the database specified in the path portion of the connection string. If the database is specified in neither place, the default value is "admin". This option is only respected when using the MONGO-CR mechanism (the default).
authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2
: This option allows authentication mechanism properties to be set on the connection string.
gssapiServiceName=string
: This option only applies to the GSSAPI mechanism and is used to alter the service name. Deprecated, please use authMechanismProperties=SERVICE_NAME:string
instead.
Server Handshake configuration:
appName=string
: Sets the logical name of the application. The application name may be used by the client to identify the application to the server, for use in server logs, slow query logs, and profile collection.
Compressor configuration:
compressors=string
: A comma-separated list of compressors to request from the server. The supported compressors currently are 'zlib', 'snappy' and 'zstd'.
zlibCompressionLevel=integer
: Integer value from -1 to 9 representing the zlib compression level. Lower values will make compression faster, while higher values will make compression better.
General configuration:
retryWrites=true|false
. If true the driver will retry supported write operations if they fail due to a network error. Defaults to true.
retryReads=true|false
. If true the driver will retry supported read operations if they fail due to a network error. Defaults to true.
uuidRepresentation=unspecified|standard|javaLegacy|csharpLegacy|pythonLegacy
. See MongoClientSettings.getUuidRepresentation()
for documentation of semantics of this parameter. Defaults to "javaLegacy", but will change to "unspecified" in the next major release.
@mongodb.driver.manual reference/connection-string Connection String Format Since: 3.0.0
/**
* <p>Represents a <a href="http://www.mongodb.org/display/DOCS/Connections">Connection String</a>. The Connection String describes the
* hosts to be used and options.</p>
*
* <p>The format of the Connection String is:</p>
* <pre>
* mongodb://[username:password@]host1[:port1][,host2[:port2],...[,hostN[:portN]]][/[database.collection][?options]]
* </pre>
* <ul>
* <li>{@code mongodb://} is a required prefix to identify that this is a string in the standard connection format.</li>
* <li>{@code username:password@} are optional. If given, the driver will attempt to login to a database after
* connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not,
* in which case the ":" after the username is left off as well</li>
* <li>{@code host1} is the only required part of the connection string. It identifies a server address to connect to.
* Support for Unix domain sockets was added in 3.7. Note: The path must be urlencoded eg: {@code mongodb://%2Ftmp%2Fmongodb-27017.sock}
* and the {@code jnr.unixsocket} library installed.
* </li>
* <li>{@code :portX} is optional and defaults to :27017 if not provided.</li>
* <li>{@code /database} is the name of the database to login to and thus is only relevant if the
* {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.</li>
* <li>{@code ?options} are connection options. Note that if {@code database} is absent there is still a {@code /}
* required between the last host and the {@code ?} introducing the options. Options are name=value pairs and the pairs
* are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
* but should be considered as deprecated.</li>
* </ul>
* <p>An alternative format, using the mongodb+srv protocol, is:
* <pre>
* mongodb+srv://[username:password@]host[/[database][?options]]
* </pre>
* <ul>
* <li>{@code mongodb+srv://} is a required prefix for this format.</li>
* <li>{@code username:password@} are optional. If given, the driver will attempt to login to a database after
* connecting to a database server. For some authentication mechanisms, only the username is specified and the password is not,
* in which case the ":" after the username is left off as well</li>
* <li>{@code host} is the only required part of the URI. It identifies a single host name for which SRV records are looked up
* from a Domain Name Server after prefixing the host name with {@code "_mongodb._tcp"}. The host/port for each SRV record becomes the
* seed list used to connect, as if each one were provided as host/port pair in a URI using the normal mongodb protocol.</li>
* <li>{@code /database} is the name of the database to login to and thus is only relevant if the
* {@code username:password@} syntax is used. If not specified the "admin" database will be used by default.</li>
* <li>{@code ?options} are connection options. Note that if {@code database} is absent there is still a {@code /}
* required between the last host and the {@code ?} introducing the options. Options are name=value pairs and the pairs
* are separated by "&". For backwards compatibility, ";" is accepted as a separator in addition to "&",
* but should be considered as deprecated. Additionally with the mongodb+srv protocol, TXT records are looked up from a Domain Name
* Server for the given host, and the text value of each one is prepended to any options on the URI itself. Because the last specified
* value for any option wins, that means that options provided on the URI will override any that are provided via TXT records.</li>
* </ul>
* <p>The following options are supported (case insensitive):</p>
*
* <p>Server Selection Configuration:</p>
* <ul>
* <li>{@code serverSelectionTimeoutMS=ms}: How long the driver will wait for server selection to succeed before throwing an exception.</li>
* <li>{@code localThresholdMS=ms}: When choosing among multiple MongoDB servers to send a request, the driver will only
* send that request to a server whose ping time is less than or equal to the server with the fastest ping time plus the local
* threshold.</li>
* </ul>
* <p>Server Monitoring Configuration:</p>
* <ul>
* <li>{@code heartbeatFrequencyMS=ms}: The frequency that the driver will attempt to determine the current state of each server in the
* cluster.</li>
* </ul>
* <p>Replica set configuration:</p>
* <ul>
* <li>{@code replicaSet=name}: Implies that the hosts given are a seed list, and the driver will attempt to find
* all members of the set.</li>
* </ul>
* <p>Connection Configuration:</p>
* <ul>
* <li>{@code ssl=true|false}: Whether to connect using TLS.</li>
* <li>{@code tls=true|false}: Whether to connect using TLS. Supersedes the ssl option</li>
* <li>{@code tlsInsecure=true|false}: If connecting with TLS, this option enables insecure TLS connections. Currently this has the
* same effect of setting tlsAllowInvalidHostnames to true. Other mechanism for relaxing TLS security constraints must be handled in
* the application by customizing the {@link javax.net.ssl.SSLContext}</li>
* <li>{@code sslInvalidHostNameAllowed=true|false}: Whether to allow invalid host names for TLS connections.</li>
* <li>{@code tlsAllowInvalidHostnames=true|false}: Whether to allow invalid host names for TLS connections. Supersedes the
* sslInvalidHostNameAllowed option</li>
* <li>{@code connectTimeoutMS=ms}: How long a connection can take to be opened before timing out.</li>
* <li>{@code socketTimeoutMS=ms}: How long a send or receive on a socket can take before timing out.</li>
* <li>{@code maxIdleTimeMS=ms}: Maximum idle time of a pooled connection. A connection that exceeds this limit will be closed</li>
* <li>{@code maxLifeTimeMS=ms}: Maximum life time of a pooled connection. A connection that exceeds this limit will be closed</li>
* <li>{@code streamType=nio2|netty}: The stream type to use for connections. If unspecified, nio2 will be used for asynchronous
* clients. Note that this query parameter has been deprecated and applications should use
* {@link MongoClientSettings.Builder#streamFactoryFactory(StreamFactoryFactory)} instead.</li>
* </ul>
* <p>Connection pool configuration:</p>
* <ul>
* <li>{@code maxPoolSize=n}: The maximum number of connections in the connection pool.</li>
* <li>{@code waitQueueMultiple=n} : this multiplier, multiplied with the maxPoolSize setting, gives the maximum number of
* threads that may be waiting for a connection to become available from the pool. All further threads will get an
* exception right away. Note that this configuration option is deprecated and will be removed in the next major release.</li>
* <li>{@code waitQueueTimeoutMS=ms}: The maximum wait time in milliseconds that a thread may wait for a connection to
* become available.</li>
* </ul>
* <p>Write concern configuration:</p>
* <ul>
* <li>{@code safe=true|false}
* <ul>
* <li>{@code true}: the driver ensures that all writes are acknowledged by the MongoDB server, or else throws an exception.
* (see also {@code w} and {@code wtimeoutMS}).</li>
* <li>{@code false}: the driver does not ensure that all writes are acknowledged by the MongoDB server.</li>
* </ul>
* </li>
* <li>{@code journal=true|false}
* <ul>
* <li>{@code true}: the driver waits for the server to group commit to the journal file on disk.</li>
* <li>{@code false}: the driver does not wait for the server to group commit to the journal file on disk.</li>
* </ul>
* </li>
* <li>{@code w=wValue}
* <ul>
* <li>The driver adds { w : wValue } to all write commands. Implies {@code safe=true}.</li>
* <li>wValue is typically a number, but can be any string in order to allow for specifications like
* {@code "majority"}</li>
* </ul>
* </li>
* <li>{@code wtimeoutMS=ms}
* <ul>
* <li>The driver adds { wtimeout : ms } to all write commands. Implies {@code safe=true}.</li>
* <li>Used in combination with {@code w}</li>
* </ul>
* </li>
* </ul>
* <p>Read preference configuration:</p>
* <ul>
* <li>{@code readPreference=enum}: The read preference for this connection.
* <ul>
* <li>Enumerated values:
* <ul>
* <li>{@code primary}</li>
* <li>{@code primaryPreferred}</li>
* <li>{@code secondary}</li>
* <li>{@code secondaryPreferred}</li>
* <li>{@code nearest}</li>
* </ul>
* </li>
* </ul>
* </li>
* <li>{@code readPreferenceTags=string}. A representation of a tag set as a comma-separated list of colon-separated
* key-value pairs, e.g. {@code "dc:ny,rack:1}". Spaces are stripped from beginning and end of all keys and values.
* To specify a list of tag sets, using multiple readPreferenceTags,
* e.g. {@code readPreferenceTags=dc:ny,rack:1;readPreferenceTags=dc:ny;readPreferenceTags=}
* <ul>
* <li>Note the empty value for the last one, which means match any secondary as a last resort.</li>
* <li>Order matters when using multiple readPreferenceTags.</li>
* </ul>
* </li>
* <li>{@code maxStalenessSeconds=seconds}. The maximum staleness in seconds. For use with any non-primary read preference, the driver
* estimates the staleness of each secondary, based on lastWriteDate values provided in server isMaster responses, and selects only those
* secondaries whose staleness is less than or equal to maxStalenessSeconds. Not providing the parameter or explicitly setting it to -1
* indicates that there should be no max staleness check. The maximum staleness feature is designed to prevent badly-lagging servers from
* being selected. The staleness estimate is imprecise and shouldn't be used to try to select "up-to-date" secondaries. The minimum value
* is either 90 seconds, or the heartbeat frequency plus 10 seconds, whichever is greatest.
* </li>
* </ul>
* <p>Authentication configuration:</p>
* <ul>
* <li>{@code authMechanism=MONGO-CR|GSSAPI|PLAIN|MONGODB-X509}: The authentication mechanism to use if a credential was supplied.
* The default is unspecified, in which case the client will pick the most secure mechanism available based on the sever version. For the
* GSSAPI and MONGODB-X509 mechanisms, no password is accepted, only the username.
* </li>
* <li>{@code authSource=string}: The source of the authentication credentials. This is typically the database that
* the credentials have been created. The value defaults to the database specified in the path portion of the connection string.
* If the database is specified in neither place, the default value is "admin". This option is only respected when using the MONGO-CR
* mechanism (the default).
* </li>
* <li>{@code authMechanismProperties=PROPERTY_NAME:PROPERTY_VALUE,PROPERTY_NAME2:PROPERTY_VALUE2}: This option allows authentication
* mechanism properties to be set on the connection string.
* </li>
* <li>{@code gssapiServiceName=string}: This option only applies to the GSSAPI mechanism and is used to alter the service name.
* Deprecated, please use {@code authMechanismProperties=SERVICE_NAME:string} instead.
* </li>
* </ul>
* <p>Server Handshake configuration:</p>
* <ul>
* <li>{@code appName=string}: Sets the logical name of the application. The application name may be used by the client to identify
* the application to the server, for use in server logs, slow query logs, and profile collection.</li>
* </ul>
* <p>Compressor configuration:</p>
* <ul>
* <li>{@code compressors=string}: A comma-separated list of compressors to request from the server. The supported compressors
* currently are 'zlib', 'snappy' and 'zstd'.</li>
* <li>{@code zlibCompressionLevel=integer}: Integer value from -1 to 9 representing the zlib compression level. Lower values will make
* compression faster, while higher values will make compression better.</li>
* </ul>
* <p>General configuration:</p>
* <ul>
* <li>{@code retryWrites=true|false}. If true the driver will retry supported write operations if they fail due to a network error.
* Defaults to true.</li>
* <li>{@code retryReads=true|false}. If true the driver will retry supported read operations if they fail due to a network error.
* Defaults to true.</li>
* <li>{@code uuidRepresentation=unspecified|standard|javaLegacy|csharpLegacy|pythonLegacy}. See
* {@link MongoClientSettings#getUuidRepresentation()} for documentation of semantics of this parameter. Defaults to "javaLegacy", but
* will change to "unspecified" in the next major release.</li>
* </ul>
*
* @mongodb.driver.manual reference/connection-string Connection String Format
* @since 3.0.0
*/
public class ConnectionString {
private static final String MONGODB_PREFIX = "mongodb://";
private static final String MONGODB_SRV_PREFIX = "mongodb+srv://";
private static final Set<String> ALLOWED_OPTIONS_IN_TXT_RECORD = new HashSet<String>(asList("authsource", "replicaset"));
private static final String UTF_8 = "UTF-8";
private static final Logger LOGGER = Loggers.getLogger("uri");
private final MongoCredential credential;
private final boolean isSrvProtocol;
private final List<String> hosts;
private final String database;
private final String collection;
private final String connectionString;
private ReadPreference readPreference;
private WriteConcern writeConcern;
private Boolean retryWrites;
private Boolean retryReads;
private ReadConcern readConcern;
private Integer minConnectionPoolSize;
private Integer maxConnectionPoolSize;
private Integer threadsAllowedToBlockForConnectionMultiplier;
private Integer maxWaitTime;
private Integer maxConnectionIdleTime;
private Integer maxConnectionLifeTime;
private Integer connectTimeout;
private Integer socketTimeout;
private Boolean sslEnabled;
private Boolean sslInvalidHostnameAllowed;
private String streamType;
private String requiredReplicaSetName;
private Integer serverSelectionTimeout;
private Integer localThreshold;
private Integer heartbeatFrequency;
private String applicationName;
private List<MongoCompressor> compressorList;
private UuidRepresentation uuidRepresentation;
Creates a ConnectionString from the given string.
Params: - connectionString – the connection string
Since: 3.0
/**
* Creates a ConnectionString from the given string.
*
* @param connectionString the connection string
* @since 3.0
*/
public ConnectionString(final String connectionString) {
this.connectionString = connectionString;
boolean isMongoDBProtocol = connectionString.startsWith(MONGODB_PREFIX);
isSrvProtocol = connectionString.startsWith(MONGODB_SRV_PREFIX);
if (!isMongoDBProtocol && !isSrvProtocol) {
throw new IllegalArgumentException(format("The connection string is invalid. "
+ "Connection strings must start with either '%s' or '%s", MONGODB_PREFIX, MONGODB_SRV_PREFIX));
}
String unprocessedConnectionString;
if (isMongoDBProtocol) {
unprocessedConnectionString = connectionString.substring(MONGODB_PREFIX.length());
} else {
unprocessedConnectionString = connectionString.substring(MONGODB_SRV_PREFIX.length());
}
// Split out the user and host information
String userAndHostInformation;
int idx = unprocessedConnectionString.indexOf("/");
if (idx == -1) {
if (unprocessedConnectionString.contains("?")) {
throw new IllegalArgumentException("The connection string contains options without trailing slash");
}
userAndHostInformation = unprocessedConnectionString;
unprocessedConnectionString = "";
} else {
userAndHostInformation = unprocessedConnectionString.substring(0, idx);
unprocessedConnectionString = unprocessedConnectionString.substring(idx + 1);
}
// Split the user and host information
String userInfo;
String hostIdentifier;
String userName = null;
char[] password = null;
idx = userAndHostInformation.lastIndexOf("@");
if (idx > 0) {
userInfo = userAndHostInformation.substring(0, idx).replace("+", "%2B");
hostIdentifier = userAndHostInformation.substring(idx + 1);
int colonCount = countOccurrences(userInfo, ":");
if (userInfo.contains("@") || colonCount > 1) {
throw new IllegalArgumentException("The connection string contains invalid user information. "
+ "If the username or password contains a colon (:) or an at-sign (@) then it must be urlencoded");
}
if (colonCount == 0) {
userName = urldecode(userInfo);
} else {
idx = userInfo.indexOf(":");
userName = urldecode(userInfo.substring(0, idx));
password = urldecode(userInfo.substring(idx + 1), true).toCharArray();
}
} else {
hostIdentifier = userAndHostInformation;
}
// Validate the hosts
List<String> unresolvedHosts = unmodifiableList(parseHosts(asList(hostIdentifier.split(","))));
if (isSrvProtocol) {
if (unresolvedHosts.size() > 1) {
throw new IllegalArgumentException("Only one host allowed when using mongodb+srv protocol");
}
if (unresolvedHosts.get(0).contains(":")) {
throw new IllegalArgumentException("Host for when using mongodb+srv protocol can not contain a port");
}
}
this.hosts = unresolvedHosts;
// Process the authDB section
String nsPart;
idx = unprocessedConnectionString.indexOf("?");
if (idx == -1) {
nsPart = unprocessedConnectionString;
unprocessedConnectionString = "";
} else {
nsPart = unprocessedConnectionString.substring(0, idx);
unprocessedConnectionString = unprocessedConnectionString.substring(idx + 1);
}
if (nsPart.length() > 0) {
nsPart = urldecode(nsPart);
idx = nsPart.indexOf(".");
if (idx < 0) {
database = nsPart;
collection = null;
} else {
database = nsPart.substring(0, idx);
collection = nsPart.substring(idx + 1);
}
MongoNamespace.checkDatabaseNameValidity(database);
} else {
database = null;
collection = null;
}
String txtRecordsQueryParameters = isSrvProtocol
? new DefaultDnsResolver().resolveAdditionalQueryParametersFromTxtRecords(unresolvedHosts.get(0)) : "";
String connectionStringQueryParamenters = unprocessedConnectionString;
Map<String, List<String>> connectionStringOptionsMap = parseOptions(connectionStringQueryParamenters);
Map<String, List<String>> txtRecordsOptionsMap = parseOptions(txtRecordsQueryParameters);
if (!ALLOWED_OPTIONS_IN_TXT_RECORD.containsAll(txtRecordsOptionsMap.keySet())) {
throw new MongoConfigurationException(format("A TXT record is only permitted to contain the keys %s, but the TXT record for "
+ "'%s' contains the keys %s", ALLOWED_OPTIONS_IN_TXT_RECORD, unresolvedHosts.get(0), txtRecordsOptionsMap.keySet()));
}
Map<String, List<String>> combinedOptionsMaps = combineOptionsMaps(txtRecordsOptionsMap, connectionStringOptionsMap);
if (isSrvProtocol && !combinedOptionsMaps.containsKey("ssl")) {
combinedOptionsMaps.put("ssl", singletonList("true"));
}
translateOptions(combinedOptionsMaps);
credential = createCredentials(combinedOptionsMaps, userName, password);
warnOnUnsupportedOptions(combinedOptionsMaps);
}
private static final Set<String> GENERAL_OPTIONS_KEYS = new LinkedHashSet<String>();
private static final Set<String> AUTH_KEYS = new HashSet<String>();
private static final Set<String> READ_PREFERENCE_KEYS = new HashSet<String>();
private static final Set<String> WRITE_CONCERN_KEYS = new HashSet<String>();
private static final Set<String> COMPRESSOR_KEYS = new HashSet<String>();
private static final Set<String> ALL_KEYS = new HashSet<String>();
static {
GENERAL_OPTIONS_KEYS.add("minpoolsize");
GENERAL_OPTIONS_KEYS.add("maxpoolsize");
GENERAL_OPTIONS_KEYS.add("waitqueuemultiple");
GENERAL_OPTIONS_KEYS.add("waitqueuetimeoutms");
GENERAL_OPTIONS_KEYS.add("connecttimeoutms");
GENERAL_OPTIONS_KEYS.add("maxidletimems");
GENERAL_OPTIONS_KEYS.add("maxlifetimems");
GENERAL_OPTIONS_KEYS.add("sockettimeoutms");
// Order matters here: Having tls after ssl means than the tls option will supersede the ssl option when both are set
GENERAL_OPTIONS_KEYS.add("ssl");
GENERAL_OPTIONS_KEYS.add("tls");
// Order matters here: Having tlsinsecure before sslinvalidhostnameallowed and tlsallowinvalidhostnames means that those options
// will supersede this one when both are set.
GENERAL_OPTIONS_KEYS.add("tlsinsecure");
// Order matters here: Having tlsallowinvalidhostnames after sslinvalidhostnameallowed means than the tlsallowinvalidhostnames
// option will supersede the sslinvalidhostnameallowed option when both are set
GENERAL_OPTIONS_KEYS.add("sslinvalidhostnameallowed");
GENERAL_OPTIONS_KEYS.add("tlsallowinvalidhostnames");
GENERAL_OPTIONS_KEYS.add("replicaset");
GENERAL_OPTIONS_KEYS.add("readconcernlevel");
GENERAL_OPTIONS_KEYS.add("streamtype");
GENERAL_OPTIONS_KEYS.add("serverselectiontimeoutms");
GENERAL_OPTIONS_KEYS.add("localthresholdms");
GENERAL_OPTIONS_KEYS.add("heartbeatfrequencyms");
GENERAL_OPTIONS_KEYS.add("retrywrites");
GENERAL_OPTIONS_KEYS.add("retryreads");
GENERAL_OPTIONS_KEYS.add("appname");
GENERAL_OPTIONS_KEYS.add("uuidrepresentation");
COMPRESSOR_KEYS.add("compressors");
COMPRESSOR_KEYS.add("zlibcompressionlevel");
READ_PREFERENCE_KEYS.add("readpreference");
READ_PREFERENCE_KEYS.add("readpreferencetags");
READ_PREFERENCE_KEYS.add("maxstalenessseconds");
WRITE_CONCERN_KEYS.add("safe");
WRITE_CONCERN_KEYS.add("w");
WRITE_CONCERN_KEYS.add("wtimeoutms");
WRITE_CONCERN_KEYS.add("fsync");
WRITE_CONCERN_KEYS.add("journal");
AUTH_KEYS.add("authmechanism");
AUTH_KEYS.add("authsource");
AUTH_KEYS.add("gssapiservicename");
AUTH_KEYS.add("authmechanismproperties");
ALL_KEYS.addAll(GENERAL_OPTIONS_KEYS);
ALL_KEYS.addAll(AUTH_KEYS);
ALL_KEYS.addAll(READ_PREFERENCE_KEYS);
ALL_KEYS.addAll(WRITE_CONCERN_KEYS);
ALL_KEYS.addAll(COMPRESSOR_KEYS);
}
// Any options contained in the connection string completely replace the corresponding options specified in TXT records,
// even for options which multiple values, e.g. readPreferenceTags
private Map<String, List<String>> combineOptionsMaps(final Map<String, List<String>> txtRecordsOptionsMap,
final Map<String, List<String>> connectionStringOptionsMap) {
Map<String, List<String>> combinedOptionsMaps = new HashMap<String, List<String>>(txtRecordsOptionsMap);
for (Map.Entry<String, List<String>> entry : connectionStringOptionsMap.entrySet()) {
combinedOptionsMaps.put(entry.getKey(), entry.getValue());
}
return combinedOptionsMaps;
}
private void warnOnUnsupportedOptions(final Map<String, List<String>> optionsMap) {
for (final String key : optionsMap.keySet()) {
if (!ALL_KEYS.contains(key)) {
if (LOGGER.isWarnEnabled()) {
LOGGER.warn(format("Connection string contains unsupported option '%s'.", key));
}
}
}
}
private void translateOptions(final Map<String, List<String>> optionsMap) {
boolean tlsInsecureSet = false;
boolean tlsAllowInvalidHostnamesSet = false;
for (final String key : GENERAL_OPTIONS_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("maxpoolsize")) {
maxConnectionPoolSize = parseInteger(value, "maxpoolsize");
} else if (key.equals("minpoolsize")) {
minConnectionPoolSize = parseInteger(value, "minpoolsize");
} else if (key.equals("maxidletimems")) {
maxConnectionIdleTime = parseInteger(value, "maxidletimems");
} else if (key.equals("maxlifetimems")) {
maxConnectionLifeTime = parseInteger(value, "maxlifetimems");
} else if (key.equals("waitqueuemultiple")) {
threadsAllowedToBlockForConnectionMultiplier = parseInteger(value, "waitqueuemultiple");
} else if (key.equals("waitqueuetimeoutms")) {
maxWaitTime = parseInteger(value, "waitqueuetimeoutms");
} else if (key.equals("connecttimeoutms")) {
connectTimeout = parseInteger(value, "connecttimeoutms");
} else if (key.equals("sockettimeoutms")) {
socketTimeout = parseInteger(value, "sockettimeoutms");
} else if (key.equals("tlsallowinvalidhostnames")) {
sslInvalidHostnameAllowed = parseBoolean(value, "tlsAllowInvalidHostnames");
tlsAllowInvalidHostnamesSet = true;
} else if (key.equals("sslinvalidhostnameallowed")) {
sslInvalidHostnameAllowed = parseBoolean(value, "sslinvalidhostnameallowed");
tlsAllowInvalidHostnamesSet = true;
} else if (key.equals("tlsinsecure")) {
sslInvalidHostnameAllowed = parseBoolean(value, "tlsinsecure");
tlsInsecureSet = true;
} else if (key.equals("ssl")) {
initializeSslEnabled("ssl", value);
} else if (key.equals("tls")) {
initializeSslEnabled("tls", value);
} else if (key.equals("streamtype")) {
streamType = value;
LOGGER.warn("The streamType query parameter is deprecated and support for it will be removed"
+ " in the next major release.");
} else if (key.equals("replicaset")) {
requiredReplicaSetName = value;
} else if (key.equals("readconcernlevel")) {
readConcern = new ReadConcern(ReadConcernLevel.fromString(value));
} else if (key.equals("serverselectiontimeoutms")) {
serverSelectionTimeout = parseInteger(value, "serverselectiontimeoutms");
} else if (key.equals("localthresholdms")) {
localThreshold = parseInteger(value, "localthresholdms");
} else if (key.equals("heartbeatfrequencyms")) {
heartbeatFrequency = parseInteger(value, "heartbeatfrequencyms");
} else if (key.equals("appname")) {
applicationName = value;
} else if (key.equals("retrywrites")) {
retryWrites = parseBoolean(value, "retrywrites");
} else if (key.equals("retryreads")) {
retryReads = parseBoolean(value, "retryreads");
} else if (key.equals("uuidrepresentation")) {
uuidRepresentation = createUuidRepresentation(value);
}
}
if (tlsInsecureSet && tlsAllowInvalidHostnamesSet) {
throw new IllegalArgumentException("tlsAllowInvalidHostnames or sslInvalidHostnameAllowed set along with tlsInsecure "
+ "is not allowed");
}
writeConcern = createWriteConcern(optionsMap);
readPreference = createReadPreference(optionsMap);
compressorList = createCompressors(optionsMap);
}
private void initializeSslEnabled(final String key, final String value) {
Boolean booleanValue = parseBoolean(value, key);
if (sslEnabled != null && !sslEnabled.equals(booleanValue)) {
throw new IllegalArgumentException("Conflicting tls and ssl parameter values are not allowed");
}
sslEnabled = booleanValue;
}
private List<MongoCompressor> createCompressors(final Map<String, List<String>> optionsMap) {
String compressors = "";
Integer zlibCompressionLevel = null;
for (final String key : COMPRESSOR_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("compressors")) {
compressors = value;
} else if (key.equals("zlibcompressionlevel")) {
zlibCompressionLevel = Integer.parseInt(value);
}
}
return buildCompressors(compressors, zlibCompressionLevel);
}
private List<MongoCompressor> buildCompressors(final String compressors, @Nullable final Integer zlibCompressionLevel) {
List<MongoCompressor> compressorsList = new ArrayList<MongoCompressor>();
for (String cur : compressors.split(",")) {
if (cur.equals("zlib")) {
MongoCompressor zlibCompressor = MongoCompressor.createZlibCompressor();
if (zlibCompressionLevel != null) {
zlibCompressor = zlibCompressor.withProperty(MongoCompressor.LEVEL, zlibCompressionLevel);
}
compressorsList.add(zlibCompressor);
} else if (cur.equals("snappy")) {
compressorsList.add(MongoCompressor.createSnappyCompressor());
} else if (cur.equals("zstd")) {
compressorsList.add(MongoCompressor.createZstdCompressor());
} else if (!cur.isEmpty()) {
throw new IllegalArgumentException("Unsupported compressor '" + cur + "'");
}
}
return unmodifiableList(compressorsList);
}
@Nullable
private WriteConcern createWriteConcern(final Map<String, List<String>> optionsMap) {
String w = null;
Integer wTimeout = null;
Boolean safe = null;
Boolean fsync = null;
Boolean journal = null;
for (final String key : WRITE_CONCERN_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("safe")) {
safe = parseBoolean(value, "safe");
} else if (key.equals("w")) {
w = value;
} else if (key.equals("wtimeoutms")) {
wTimeout = Integer.parseInt(value);
} else if (key.equals("fsync")) {
fsync = parseBoolean(value, "fsync");
} else if (key.equals("journal")) {
journal = parseBoolean(value, "journal");
}
}
return buildWriteConcern(safe, w, wTimeout, fsync, journal);
}
@Nullable
private ReadPreference createReadPreference(final Map<String, List<String>> optionsMap) {
String readPreferenceType = null;
List<TagSet> tagSetList = new ArrayList<TagSet>();
long maxStalenessSeconds = -1;
for (final String key : READ_PREFERENCE_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("readpreference")) {
readPreferenceType = value;
} else if (key.equals("maxstalenessseconds")) {
maxStalenessSeconds = parseInteger(value, "maxstalenessseconds");
} else if (key.equals("readpreferencetags")) {
for (final String cur : optionsMap.get(key)) {
TagSet tagSet = getTags(cur.trim());
tagSetList.add(tagSet);
}
}
}
return buildReadPreference(readPreferenceType, tagSetList, maxStalenessSeconds);
}
private UuidRepresentation createUuidRepresentation(final String value) {
if (value.equalsIgnoreCase("unspecified")) {
return UuidRepresentation.UNSPECIFIED;
}
if (value.equalsIgnoreCase("javaLegacy")) {
return UuidRepresentation.JAVA_LEGACY;
}
if (value.equalsIgnoreCase("csharpLegacy")) {
return UuidRepresentation.C_SHARP_LEGACY;
}
if (value.equalsIgnoreCase("pythonLegacy")) {
return UuidRepresentation.PYTHON_LEGACY;
}
if (value.equalsIgnoreCase("standard")) {
return UuidRepresentation.STANDARD;
}
throw new IllegalArgumentException("Unknown uuid representation: " + value);
}
@Nullable
private MongoCredential createCredentials(final Map<String, List<String>> optionsMap, @Nullable final String userName,
@Nullable final char[] password) {
AuthenticationMechanism mechanism = null;
String authSource = null;
String gssapiServiceName = null;
String authMechanismProperties = null;
for (final String key : AUTH_KEYS) {
String value = getLastValue(optionsMap, key);
if (value == null) {
continue;
}
if (key.equals("authmechanism")) {
mechanism = AuthenticationMechanism.fromMechanismName(value);
} else if (key.equals("authsource")) {
authSource = value;
} else if (key.equals("gssapiservicename")) {
gssapiServiceName = value;
} else if (key.equals("authmechanismproperties")) {
authMechanismProperties = value;
}
}
MongoCredential credential = null;
if (mechanism != null) {
credential = createMongoCredentialWithMechanism(mechanism, userName, password, authSource, gssapiServiceName);
} else if (userName != null) {
credential = MongoCredential.createCredential(userName,
getAuthSourceOrDefault(authSource, database != null ? database : "admin"), password);
}
if (credential != null && authMechanismProperties != null) {
for (String part : authMechanismProperties.split(",")) {
String[] mechanismPropertyKeyValue = part.split(":");
if (mechanismPropertyKeyValue.length != 2) {
throw new IllegalArgumentException(format("The connection string contains invalid authentication properties. "
+ "'%s' is not a key value pair", part));
}
String key = mechanismPropertyKeyValue[0].trim().toLowerCase();
String value = mechanismPropertyKeyValue[1].trim();
if (key.equals("canonicalize_host_name")) {
credential = credential.withMechanismProperty(key, Boolean.valueOf(value));
} else {
credential = credential.withMechanismProperty(key, value);
}
}
}
return credential;
}
@SuppressWarnings("deprecation")
private MongoCredential createMongoCredentialWithMechanism(final AuthenticationMechanism mechanism, final String userName,
@Nullable final char[] password,
@Nullable final String authSource,
@Nullable final String gssapiServiceName) {
MongoCredential credential;
String mechanismAuthSource;
switch (mechanism) {
case PLAIN:
mechanismAuthSource = getAuthSourceOrDefault(authSource, database != null ? database : "$external");
break;
case GSSAPI:
case MONGODB_X509:
mechanismAuthSource = getAuthSourceOrDefault(authSource, "$external");
if (!mechanismAuthSource.equals("$external")) {
throw new IllegalArgumentException(format("Invalid authSource for %s, it must be '$external'", mechanism));
}
break;
default:
mechanismAuthSource = getAuthSourceOrDefault(authSource, database != null ? database : "admin");
}
switch (mechanism) {
case GSSAPI:
credential = MongoCredential.createGSSAPICredential(userName);
if (gssapiServiceName != null) {
credential = credential.withMechanismProperty("SERVICE_NAME", gssapiServiceName);
}
if (password != null && LOGGER.isWarnEnabled()) {
LOGGER.warn("Password in connection string not used with MONGODB_X509 authentication mechanism.");
}
break;
case PLAIN:
credential = MongoCredential.createPlainCredential(userName, mechanismAuthSource, password);
break;
case MONGODB_CR:
credential = MongoCredential.createMongoCRCredential(userName, mechanismAuthSource, password);
break;
case MONGODB_X509:
if (password != null) {
throw new IllegalArgumentException("Invalid mechanism, MONGODB_x509 does not support passwords");
}
credential = MongoCredential.createMongoX509Credential(userName);
break;
case SCRAM_SHA_1:
credential = MongoCredential.createScramSha1Credential(userName, mechanismAuthSource, password);
break;
case SCRAM_SHA_256:
credential = MongoCredential.createScramSha256Credential(userName, mechanismAuthSource, password);
break;
default:
throw new UnsupportedOperationException(format("The connection string contains an invalid authentication mechanism'. "
+ "'%s' is not a supported authentication mechanism",
mechanism));
}
return credential;
}
private String getAuthSourceOrDefault(@Nullable final String authSource, final String defaultAuthSource) {
if (authSource != null) {
return authSource;
} else {
return defaultAuthSource;
}
}
@Nullable
private String getLastValue(final Map<String, List<String>> optionsMap, final String key) {
List<String> valueList = optionsMap.get(key);
if (valueList == null) {
return null;
}
return valueList.get(valueList.size() - 1);
}
private Map<String, List<String>> parseOptions(final String optionsPart) {
Map<String, List<String>> optionsMap = new HashMap<String, List<String>>();
if (optionsPart.length() == 0) {
return optionsMap;
}
for (final String part : optionsPart.split("&|;")) {
if (part.length() == 0) {
continue;
}
int idx = part.indexOf("=");
if (idx >= 0) {
String key = part.substring(0, idx).toLowerCase();
String value = part.substring(idx + 1);
List<String> valueList = optionsMap.get(key);
if (valueList == null) {
valueList = new ArrayList<String>(1);
}
valueList.add(urldecode(value));
optionsMap.put(key, valueList);
} else {
throw new IllegalArgumentException(format("The connection string contains an invalid option '%s'. "
+ "'%s' is missing the value delimiter eg '%s=value'", optionsPart, part, part));
}
}
// handle legacy wtimeout settings
if (optionsMap.containsKey("wtimeout") && !optionsMap.containsKey("wtimeoutms")) {
optionsMap.put("wtimeoutms", optionsMap.remove("wtimeout"));
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Uri option 'wtimeout' has been deprecated, use 'wtimeoutms' instead.");
}
}
// handle legacy slaveok settings
String slaveok = getLastValue(optionsMap, "slaveok");
if (slaveok != null && !optionsMap.containsKey("readpreference")) {
String readPreference = Boolean.TRUE.equals(parseBoolean(slaveok, "slaveok"))
? "secondaryPreferred" : "primary";
optionsMap.put("readpreference", singletonList(readPreference));
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Uri option 'slaveok' has been deprecated, use 'readpreference' instead.");
}
}
// handle legacy j settings
if (optionsMap.containsKey("j") && !optionsMap.containsKey("journal")) {
optionsMap.put("journal", optionsMap.remove("j"));
if (LOGGER.isWarnEnabled()) {
LOGGER.warn("Uri option 'j' has been deprecated, use 'journal' instead.");
}
}
return optionsMap;
}
@Nullable
private ReadPreference buildReadPreference(@Nullable final String readPreferenceType,
final List<TagSet> tagSetList, final long maxStalenessSeconds) {
if (readPreferenceType != null) {
if (tagSetList.isEmpty() && maxStalenessSeconds == -1) {
return ReadPreference.valueOf(readPreferenceType);
} else if (maxStalenessSeconds == -1) {
return ReadPreference.valueOf(readPreferenceType, tagSetList);
} else {
return ReadPreference.valueOf(readPreferenceType, tagSetList, maxStalenessSeconds, TimeUnit.SECONDS);
}
} else if (!(tagSetList.isEmpty() && maxStalenessSeconds == -1)) {
throw new IllegalArgumentException("Read preference mode must be specified if "
+ "either read preference tags or max staleness is specified");
}
return null;
}
@SuppressWarnings("deprecation")
@Nullable
private WriteConcern buildWriteConcern(@Nullable final Boolean safe, @Nullable final String w,
@Nullable final Integer wTimeout, @Nullable final Boolean fsync,
@Nullable final Boolean journal) {
WriteConcern retVal = null;
if (w != null || wTimeout != null || fsync != null || journal != null) {
if (w == null) {
retVal = WriteConcern.ACKNOWLEDGED;
} else {
try {
retVal = new WriteConcern(Integer.parseInt(w));
} catch (NumberFormatException e) {
retVal = new WriteConcern(w);
}
}
if (wTimeout != null) {
retVal = retVal.withWTimeout(wTimeout, TimeUnit.MILLISECONDS);
}
if (journal != null) {
retVal = retVal.withJournal(journal);
}
if (fsync != null) {
retVal = retVal.withFsync(fsync);
}
return retVal;
} else if (safe != null) {
if (safe) {
retVal = WriteConcern.ACKNOWLEDGED;
} else {
retVal = WriteConcern.UNACKNOWLEDGED;
}
}
return retVal;
}
private TagSet getTags(final String tagSetString) {
List<Tag> tagList = new ArrayList<Tag>();
if (tagSetString.length() > 0) {
for (final String tag : tagSetString.split(",")) {
String[] tagKeyValuePair = tag.split(":");
if (tagKeyValuePair.length != 2) {
throw new IllegalArgumentException(format("The connection string contains an invalid read preference tag. "
+ "'%s' is not a key value pair", tagSetString));
}
tagList.add(new Tag(tagKeyValuePair[0].trim(), tagKeyValuePair[1].trim()));
}
}
return new TagSet(tagList);
}
private static final Set<String> TRUE_VALUES = new HashSet<String>(asList("true", "yes", "1"));
private static final Set<String> FALSE_VALUES = new HashSet<String>(asList("false", "no", "0"));
@Nullable
private Boolean parseBoolean(final String input, final String key) {
String trimmedInput = input.trim().toLowerCase();
if (TRUE_VALUES.contains(trimmedInput)) {
if (!trimmedInput.equals("true")) {
LOGGER.warn(format("Deprecated boolean value '%s' in the connection string for '%s'. Replace with 'true'",
trimmedInput, key));
}
return true;
} else if (FALSE_VALUES.contains(trimmedInput)) {
if (!trimmedInput.equals("false")) {
LOGGER.warn(format("Deprecated boolean value '%s' in the connection string for '%s'. Replace with'false'",
trimmedInput, key));
}
return false;
} else {
LOGGER.warn(format("Ignoring unrecognized boolean value '%s' in the connection string for '%s'. "
+ "Replace with either 'true' or 'false'", trimmedInput, key));
return null;
}
}
private int parseInteger(final String input, final String key) {
try {
return Integer.parseInt(input);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(format("The connection string contains an invalid value for '%s'. "
+ "'%s' is not a valid integer", key, input));
}
}
private List<String> parseHosts(final List<String> rawHosts) {
if (rawHosts.size() == 0){
throw new IllegalArgumentException("The connection string must contain at least one host");
}
List<String> hosts = new ArrayList<String>();
for (String host : rawHosts) {
if (host.length() == 0) {
throw new IllegalArgumentException(format("The connection string contains an empty host '%s'. ", rawHosts));
} else if (host.endsWith(".sock")) {
host = urldecode(host);
} else if (host.startsWith("[")) {
if (!host.contains("]")) {
throw new IllegalArgumentException(format("The connection string contains an invalid host '%s'. "
+ "IPv6 address literals must be enclosed in '[' and ']' according to RFC 2732", host));
}
int idx = host.indexOf("]:");
if (idx != -1) {
validatePort(host, host.substring(idx + 2));
}
} else {
int colonCount = countOccurrences(host, ":");
if (colonCount > 1) {
throw new IllegalArgumentException(format("The connection string contains an invalid host '%s'. "
+ "Reserved characters such as ':' must be escaped according RFC 2396. "
+ "Any IPv6 address literal must be enclosed in '[' and ']' according to RFC 2732.", host));
} else if (colonCount == 1) {
validatePort(host, host.substring(host.indexOf(":") + 1));
}
}
hosts.add(host);
}
Collections.sort(hosts);
return hosts;
}
private void validatePort(final String host, final String port) {
boolean invalidPort = false;
try {
int portInt = Integer.parseInt(port);
if (portInt <= 0 || portInt > 65535) {
invalidPort = true;
}
} catch (NumberFormatException e) {
invalidPort = true;
}
if (invalidPort) {
throw new IllegalArgumentException(format("The connection string contains an invalid host '%s'. "
+ "The port '%s' is not a valid, it must be an integer between 0 and 65535", host, port));
}
}
private int countOccurrences(final String haystack, final String needle) {
return haystack.length() - haystack.replace(needle, "").length();
}
private String urldecode(final String input) {
return urldecode(input, false);
}
private String urldecode(final String input, final boolean password) {
try {
return URLDecoder.decode(input, UTF_8);
} catch (UnsupportedEncodingException e) {
if (password) {
throw new IllegalArgumentException("The connection string contained unsupported characters in the password.");
} else {
throw new IllegalArgumentException(format("The connection string contained unsupported characters: '%s'."
+ "Decoding produced the following error: %s", input, e.getMessage()));
}
}
}
// ---------------------------------
Gets the username
Returns: the username
/**
* Gets the username
*
* @return the username
*/
@Nullable
public String getUsername() {
return credential != null ? credential.getUserName() : null;
}
Gets the password
Returns: the password
/**
* Gets the password
*
* @return the password
*/
@Nullable
public char[] getPassword() {
return credential != null ? credential.getPassword() : null;
}
Returns true if the connection string requires SRV protocol to resolve the host lists from the configured host.
Returns: true if SRV protocol is required to resolve hosts.
/**
* Returns true if the connection string requires SRV protocol to resolve the host lists from the configured host.
*
* @return true if SRV protocol is required to resolve hosts.
*/
public boolean isSrvProtocol() {
return isSrvProtocol;
}
Gets the list of hosts
Returns: the host list
/**
* Gets the list of hosts
*
* @return the host list
*/
public List<String> getHosts() {
return hosts;
}
Gets the database name
Returns: the database name
/**
* Gets the database name
*
* @return the database name
*/
@Nullable
public String getDatabase() {
return database;
}
Gets the collection name
Returns: the collection name
/**
* Gets the collection name
*
* @return the collection name
*/
@Nullable
public String getCollection() {
return collection;
}
Get the unparsed connection string.
Returns: the connection string deprecated use getConnectionString()
/**
* Get the unparsed connection string.
*
* @return the connection string
* deprecated use {@link #getConnectionString()}
*/
@Deprecated
public String getURI() {
return getConnectionString();
}
Get the unparsed connection string.
Returns: the connection string Since: 3.1
/**
* Get the unparsed connection string.
*
* @return the connection string
* @since 3.1
*/
public String getConnectionString() {
return connectionString;
}
Gets the credentials in an immutable list. The list will be empty if no credentials were specified in the connection string.
Returns: the credentials in an immutable list Deprecated: Prefer getCredential()
/**
* Gets the credentials in an immutable list. The list will be empty if no credentials were specified in the connection string.
*
* @return the credentials in an immutable list
* @deprecated Prefer {@link #getCredential()}
*/
@Deprecated
public List<MongoCredential> getCredentialList() {
return credential != null ? singletonList(credential) : Collections.<MongoCredential>emptyList();
}
Gets the credential or null if no credentials were specified in the connection string.
Returns: the credentials in an immutable list Since: 3.6
/**
* Gets the credential or null if no credentials were specified in the connection string.
*
* @return the credentials in an immutable list
* @since 3.6
*/
@Nullable
public MongoCredential getCredential() {
return credential;
}
Gets the read preference specified in the connection string.
Returns: the read preference
/**
* Gets the read preference specified in the connection string.
* @return the read preference
*/
@Nullable
public ReadPreference getReadPreference() {
return readPreference;
}
Gets the read concern specified in the connection string.
Returns: the read concern
/**
* Gets the read concern specified in the connection string.
* @return the read concern
*/
@Nullable
public ReadConcern getReadConcern() {
return readConcern;
}
Gets the write concern specified in the connection string.
Returns: the write concern
/**
* Gets the write concern specified in the connection string.
* @return the write concern
*/
@Nullable
public WriteConcern getWriteConcern() {
return writeConcern;
}
Returns true if writes should be retried if they fail due to a network error, and false otherwise
Starting with the 3.11.0 release, the default value is true
Returns: the retryWrites value, or true if unset Since: 3.6 @mongodb.server.release 3.6 Deprecated: Prefer getRetryWritesValue()
/**
* Returns true if writes should be retried if they fail due to a network error, and false otherwise
*
* <p>Starting with the 3.11.0 release, the default value is true</p>
*
* @return the retryWrites value, or true if unset
* @since 3.6
* @mongodb.server.release 3.6
* @deprecated Prefer {@link #getRetryWritesValue()}
*/
@Deprecated
public boolean getRetryWrites() {
return retryWrites == null ? true : retryWrites;
}
Gets whether writes should be retried if they fail due to a network error
The name of this method differs from others in this class so as not to conflict with the deprecated (and soon to be removed) getRetryWrites()
method, which returns a primitive boolean
value, which doesn't allow callers to differentiate between a false value and an unset value. Returns: the retryWrites value, or null if unset Since: 3.9 @mongodb.server.release 3.6
/**
* <p>Gets whether writes should be retried if they fail due to a network error</p>
*
* The name of this method differs from others in this class so as not to conflict with the deprecated (and soon to be removed)
* {@link #getRetryWrites()} method, which returns a primitive {@code boolean} value, which doesn't allow callers to differentiate
* between a false value and an unset value.
*
* @return the retryWrites value, or null if unset
* @since 3.9
* @mongodb.server.release 3.6
*/
public Boolean getRetryWritesValue() {
return retryWrites;
}
Gets whether reads should be retried if they fail due to a network error
Returns: the retryWrites value Since: 3.11 @mongodb.server.release 3.6
/**
* <p>Gets whether reads should be retried if they fail due to a network error</p>
*
* @return the retryWrites value
* @since 3.11
* @mongodb.server.release 3.6
*/
public Boolean getRetryReads() {
return retryReads;
}
Gets the minimum connection pool size specified in the connection string.
Returns: the minimum connection pool size
/**
* Gets the minimum connection pool size specified in the connection string.
* @return the minimum connection pool size
*/
@Nullable
public Integer getMinConnectionPoolSize() {
return minConnectionPoolSize;
}
Gets the maximum connection pool size specified in the connection string.
Returns: the maximum connection pool size
/**
* Gets the maximum connection pool size specified in the connection string.
* @return the maximum connection pool size
*/
@Nullable
public Integer getMaxConnectionPoolSize() {
return maxConnectionPoolSize;
}
Gets the multiplier for the number of threads allowed to block waiting for a connection specified in the connection string.
Returns: the multiplier for the number of threads allowed to block waiting for a connection Deprecated: in the next major release, wait queue size limitations will be removed
/**
* Gets the multiplier for the number of threads allowed to block waiting for a connection specified in the connection string.
* @return the multiplier for the number of threads allowed to block waiting for a connection
* @deprecated in the next major release, wait queue size limitations will be removed
*/
@Deprecated
@Nullable
public Integer getThreadsAllowedToBlockForConnectionMultiplier() {
return threadsAllowedToBlockForConnectionMultiplier;
}
Gets the maximum wait time of a thread waiting for a connection specified in the connection string.
Returns: the maximum wait time of a thread waiting for a connection
/**
* Gets the maximum wait time of a thread waiting for a connection specified in the connection string.
* @return the maximum wait time of a thread waiting for a connection
*/
@Nullable
public Integer getMaxWaitTime() {
return maxWaitTime;
}
Gets the maximum connection idle time specified in the connection string.
Returns: the maximum connection idle time
/**
* Gets the maximum connection idle time specified in the connection string.
* @return the maximum connection idle time
*/
@Nullable
public Integer getMaxConnectionIdleTime() {
return maxConnectionIdleTime;
}
Gets the maximum connection life time specified in the connection string.
Returns: the maximum connection life time
/**
* Gets the maximum connection life time specified in the connection string.
* @return the maximum connection life time
*/
@Nullable
public Integer getMaxConnectionLifeTime() {
return maxConnectionLifeTime;
}
Gets the socket connect timeout specified in the connection string.
Returns: the socket connect timeout
/**
* Gets the socket connect timeout specified in the connection string.
* @return the socket connect timeout
*/
@Nullable
public Integer getConnectTimeout() {
return connectTimeout;
}
Gets the socket timeout specified in the connection string.
Returns: the socket timeout
/**
* Gets the socket timeout specified in the connection string.
* @return the socket timeout
*/
@Nullable
public Integer getSocketTimeout() {
return socketTimeout;
}
Gets the SSL enabled value specified in the connection string.
Returns: the SSL enabled value
/**
* Gets the SSL enabled value specified in the connection string.
* @return the SSL enabled value
*/
@Nullable
public Boolean getSslEnabled() {
return sslEnabled;
}
Gets the stream type value specified in the connection string.
Returns: the stream type value Since: 3.3 Deprecated: Use Builder.streamFactoryFactory(StreamFactoryFactory)
to configure the stream type programmatically
/**
* Gets the stream type value specified in the connection string.
* @return the stream type value
* @since 3.3
* @deprecated Use {@link MongoClientSettings.Builder#streamFactoryFactory(StreamFactoryFactory)} to configure the stream type
* programmatically
*/
@Deprecated
@Nullable
public String getStreamType() {
return streamType;
}
Gets the SSL invalidHostnameAllowed value specified in the connection string.
Returns: the SSL invalidHostnameAllowed value Since: 3.3
/**
* Gets the SSL invalidHostnameAllowed value specified in the connection string.
*
* @return the SSL invalidHostnameAllowed value
* @since 3.3
*/
@Nullable
public Boolean getSslInvalidHostnameAllowed() {
return sslInvalidHostnameAllowed;
}
Gets the required replica set name specified in the connection string.
Returns: the required replica set name
/**
* Gets the required replica set name specified in the connection string.
* @return the required replica set name
*/
@Nullable
public String getRequiredReplicaSetName() {
return requiredReplicaSetName;
}
Returns: the server selection timeout (in milliseconds), or null if unset Since: 3.3
/**
*
* @return the server selection timeout (in milliseconds), or null if unset
* @since 3.3
*/
@Nullable
public Integer getServerSelectionTimeout() {
return serverSelectionTimeout;
}
Returns: the local threshold (in milliseconds), or null if unset
since 3.3
/**
*
* @return the local threshold (in milliseconds), or null if unset
* since 3.3
*/
@Nullable
public Integer getLocalThreshold() {
return localThreshold;
}
Returns: the heartbeat frequency (in milliseconds), or null if unset
since 3.3
/**
*
* @return the heartbeat frequency (in milliseconds), or null if unset
* since 3.3
*/
@Nullable
public Integer getHeartbeatFrequency() {
return heartbeatFrequency;
}
Gets the logical name of the application. The application name may be used by the client to identify the application to the server,
for use in server logs, slow query logs, and profile collection.
Default is null.
Returns: the application name, which may be null Since: 3.4 @mongodb.server.release 3.4
/**
* Gets the logical name of the application. The application name may be used by the client to identify the application to the server,
* for use in server logs, slow query logs, and profile collection.
*
* <p>Default is null.</p>
*
* @return the application name, which may be null
* @since 3.4
* @mongodb.server.release 3.4
*/
@Nullable
public String getApplicationName() {
return applicationName;
}
Gets the list of compressors.
Returns: the non-null list of compressors Since: 3.6
/**
* Gets the list of compressors.
*
* @return the non-null list of compressors
* @since 3.6
*/
public List<MongoCompressor> getCompressorList() {
return compressorList;
}
Gets the UUID representation.
Default is null.
Returns: the UUID representation, which may be null if it was unspecified Since: 3.12
/**
* Gets the UUID representation.
*
* <p>Default is null.</p>
*
* @return the UUID representation, which may be null if it was unspecified
* @since 3.12
*/
@Nullable
public UuidRepresentation getUuidRepresentation() {
return uuidRepresentation;
}
@Override
public String toString() {
return connectionString;
}
@Override
public boolean equals(final Object o) {
if (this == o) {
return true;
}
if (!(o instanceof ConnectionString)) {
return false;
}
ConnectionString that = (ConnectionString) o;
if (collection != null ? !collection.equals(that.collection) : that.collection != null) {
return false;
}
if (connectTimeout != null ? !connectTimeout.equals(that.connectTimeout) : that.connectTimeout != null) {
return false;
}
if (credential != null ? !credential.equals(that.credential) : that.credential != null) {
return false;
}
if (database != null ? !database.equals(that.database) : that.database != null) {
return false;
}
if (!hosts.equals(that.hosts)) {
return false;
}
if (maxConnectionIdleTime != null ? !maxConnectionIdleTime.equals(that.maxConnectionIdleTime)
: that.maxConnectionIdleTime != null) {
return false;
}
if (maxConnectionLifeTime != null ? !maxConnectionLifeTime.equals(that.maxConnectionLifeTime)
: that.maxConnectionLifeTime != null) {
return false;
}
if (maxConnectionPoolSize != null ? !maxConnectionPoolSize.equals(that.maxConnectionPoolSize)
: that.maxConnectionPoolSize != null) {
return false;
}
if (maxWaitTime != null ? !maxWaitTime.equals(that.maxWaitTime) : that.maxWaitTime != null) {
return false;
}
if (minConnectionPoolSize != null ? !minConnectionPoolSize.equals(that.minConnectionPoolSize)
: that.minConnectionPoolSize != null) {
return false;
}
if (readPreference != null ? !readPreference.equals(that.readPreference) : that.readPreference != null) {
return false;
}
if (requiredReplicaSetName != null ? !requiredReplicaSetName.equals(that.requiredReplicaSetName)
: that.requiredReplicaSetName != null) {
return false;
}
if (socketTimeout != null ? !socketTimeout.equals(that.socketTimeout) : that.socketTimeout != null) {
return false;
}
if (sslEnabled != null ? !sslEnabled.equals(that.sslEnabled) : that.sslEnabled != null) {
return false;
}
if (threadsAllowedToBlockForConnectionMultiplier != null
? !threadsAllowedToBlockForConnectionMultiplier.equals(that.threadsAllowedToBlockForConnectionMultiplier)
: that.threadsAllowedToBlockForConnectionMultiplier != null) {
return false;
}
if (writeConcern != null ? !writeConcern.equals(that.writeConcern) : that.writeConcern != null) {
return false;
}
if (applicationName != null ? !applicationName.equals(that.applicationName) : that.applicationName != null) {
return false;
}
if (!compressorList.equals(that.compressorList)) {
return false;
}
return true;
}
@Override
public int hashCode() {
int result = credential != null ? credential.hashCode() : 0;
result = 31 * result + hosts.hashCode();
result = 31 * result + (database != null ? database.hashCode() : 0);
result = 31 * result + (collection != null ? collection.hashCode() : 0);
result = 31 * result + (readPreference != null ? readPreference.hashCode() : 0);
result = 31 * result + (writeConcern != null ? writeConcern.hashCode() : 0);
result = 31 * result + (minConnectionPoolSize != null ? minConnectionPoolSize.hashCode() : 0);
result = 31 * result + (maxConnectionPoolSize != null ? maxConnectionPoolSize.hashCode() : 0);
result = 31 * result + (threadsAllowedToBlockForConnectionMultiplier != null
? threadsAllowedToBlockForConnectionMultiplier.hashCode()
: 0);
result = 31 * result + (maxWaitTime != null ? maxWaitTime.hashCode() : 0);
result = 31 * result + (maxConnectionIdleTime != null ? maxConnectionIdleTime.hashCode() : 0);
result = 31 * result + (maxConnectionLifeTime != null ? maxConnectionLifeTime.hashCode() : 0);
result = 31 * result + (connectTimeout != null ? connectTimeout.hashCode() : 0);
result = 31 * result + (socketTimeout != null ? socketTimeout.hashCode() : 0);
result = 31 * result + (sslEnabled != null ? sslEnabled.hashCode() : 0);
result = 31 * result + (requiredReplicaSetName != null ? requiredReplicaSetName.hashCode() : 0);
result = 31 * result + (applicationName != null ? applicationName.hashCode() : 0);
result = 31 * result + compressorList.hashCode();
return result;
}
}