/*
* Copyright (c) 2008-2014 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.diagnostics.logging.Logger;
import com.mongodb.diagnostics.logging.Loggers;
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.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;
/**
* <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.</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>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>
* <p>Connection Configuration:</p>
* <ul>
* <li>{@code streamType=nio2|netty}: The stream type to use for connections. If unspecified, nio2 will be used.</li>
* <li>{@code ssl=true|false}: Whether to connect using SSL.</li>
* <li>{@code sslInvalidHostNameAllowed=true|false}: Whether to allow invalid host names for SSL connections.</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>
* </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.</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>
*
* @mongodb.driver.manual reference/connection-string Connection String Format
* @since 3.0.0
*/
public class ConnectionString {
private static final String PREFIX = "mongodb://";
private static final String UTF_8 = "UTF-8";
private static final Logger LOGGER = Loggers.getLogger("uri");
private final MongoCredential credentials;
private final List<String> hosts;
private final String database;
private final String collection;
private final String connectionString;
private ReadPreference readPreference;
private WriteConcern writeConcern;
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;
/**
* Creates a ConnectionString from the given string.
*
* @param connectionString the connection string
* @since 3.0
*/
public ConnectionString(final String connectionString) {
this.connectionString = connectionString;
if (!connectionString.startsWith(PREFIX)) {
throw new IllegalArgumentException(format("The connection string is invalid. "
+ "Connection strings must start with '%s'", PREFIX));
}
String unprocessedConnectionString = connectionString.substring(PREFIX.length());
// Split out the user and host information
String userAndHostInformation = null;
int idx = unprocessedConnectionString.lastIndexOf("/");
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 = null;
String hostIdentifier = null;
String userName = null;
char[] password = null;
idx = userAndHostInformation.lastIndexOf("@");
if (idx > 0) {
userInfo = userAndHostInformation.substring(0, idx);
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
hosts = Collections.unmodifiableList(parseHosts(asList(hostIdentifier.split(","))));
// Process the authDB section
String nsPart = null;
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);
}
} else {
database = null;
collection = null;
}
Map<String, List<String>> optionsMap = parseOptions(unprocessedConnectionString);
translateOptions(optionsMap);
credentials = createCredentials(optionsMap, userName, password);
warnOnUnsupportedOptions(optionsMap);
}
private static final Set<String> GENERAL_OPTIONS_KEYS = new HashSet<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> 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");
GENERAL_OPTIONS_KEYS.add("sockettimeoutms");
GENERAL_OPTIONS_KEYS.add("ssl");
GENERAL_OPTIONS_KEYS.add("streamtype");
GENERAL_OPTIONS_KEYS.add("sslinvalidhostnameallowed");
GENERAL_OPTIONS_KEYS.add("replicaset");
GENERAL_OPTIONS_KEYS.add("readconcernlevel");
GENERAL_OPTIONS_KEYS.add("serverselectiontimeoutms");
GENERAL_OPTIONS_KEYS.add("localthresholdms");
GENERAL_OPTIONS_KEYS.add("heartbeatfrequencyms");
GENERAL_OPTIONS_KEYS.add("appname");
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);
}
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("Unsupported option '%s' in the connection string '%s'.", key, connectionString));
}
}
}
}
private void translateOptions(final Map<String, List<String>> optionsMap) {
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("sslinvalidhostnameallowed") && parseBoolean(value, "sslinvalidhostnameallowed")) {
sslInvalidHostnameAllowed = true;
} else if (key.equals("ssl") && parseBoolean(value, "ssl")) {
sslEnabled = true;
} else if (key.equals("streamtype")) {
streamType = value;
} 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;
}
}
writeConcern = createWriteConcern(optionsMap);
readPreference = createReadPreference(optionsMap);
}
private WriteConcern createWriteConcern(final Map<String, List<String>> optionsMap) {
Boolean safe = null;
String w = null;
Integer wTimeout = 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);
}
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 MongoCredential createCredentials(final Map<String, List<String>> optionsMap, final String userName,
final char[] password) {
AuthenticationMechanism mechanism = null;
String authSource = (database == null) ? "admin" : database;
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) {
switch (mechanism) {
case GSSAPI:
credential = MongoCredential.createGSSAPICredential(userName);
if (gssapiServiceName != null) {
credential = credential.withMechanismProperty("SERVICE_NAME", gssapiServiceName);
}
break;
case PLAIN:
credential = MongoCredential.createPlainCredential(userName, authSource, password);
break;
case MONGODB_CR:
credential = MongoCredential.createMongoCRCredential(userName, authSource, password);
break;
case MONGODB_X509:
credential = MongoCredential.createMongoX509Credential(userName);
break;
case SCRAM_SHA_1:
credential = MongoCredential.createScramSha1Credential(userName, authSource, password);
break;
default:
throw new UnsupportedOperationException(format("The connection string contains an invalid authentication mechanism'. "
+ "'%s' is not a supported authentication mechanism",
mechanism));
}
} else if (userName != null) {
credential = MongoCredential.createCredential(userName, authSource, 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;
}
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
if (optionsMap.containsKey("slaveok") && !optionsMap.containsKey("readpreference")) {
String readPreference = parseBoolean(getLastValue(optionsMap, "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;
}
private ReadPreference buildReadPreference(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")
private WriteConcern buildWriteConcern(final Boolean safe, final String w,
final Integer wTimeout, final Boolean fsync, 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 boolean parseBoolean(final String input, final String key) {
String trimmedInput = input.trim();
boolean isTrue = trimmedInput.length() > 0 && (trimmedInput.equals("1") || trimmedInput.toLowerCase().equals("true")
|| trimmedInput.toLowerCase().equals("yes"));
if ((!input.equals("true") && !input.equals("false")) && LOGGER.isWarnEnabled()) {
LOGGER.warn(format("Deprecated boolean value ('%s') in the connection string for '%s', please update to %s=%s",
input, key, key, isTrue));
}
return isTrue;
}
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")) {
throw new IllegalArgumentException(format("The connection string contains an invalid host '%s'. "
+ "Unix Domain Socket which is not supported by the Java driver", 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
*
* @return the username
*/
public String getUsername() {
return credentials != null ? credentials.getUserName() : null;
}
/**
* Gets the password
*
* @return the password
*/
public char[] getPassword() {
return credentials != null ? credentials.getPassword() : null;
}
/**
* Gets the list of hosts
*
* @return the host list
*/
public List<String> getHosts() {
return hosts;
}
/**
* Gets the database name
*
* @return the database name
*/
public String getDatabase() {
return database;
}
/**
* Gets the collection name
*
* @return the collection name
*/
public String getCollection() {
return collection;
}
/**
* Get the unparsed connection string.
*
* @return the connection string
* deprecated use {@link #getConnectionString()}
*/
@Deprecated
public String getURI() {
return getConnectionString();
}
/**
* 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.
*
* @return the credentials in an immutable list
*/
public List<MongoCredential> getCredentialList() {
return credentials != null ? singletonList(credentials) : Collections.<MongoCredential>emptyList();
}
/**
* Gets the read preference specified in the connection string.
* @return the read preference
*/
public ReadPreference getReadPreference() {
return readPreference;
}
/**
* Gets the read concern specified in the connection string.
* @return the read concern
*/
public ReadConcern getReadConcern() {
return readConcern;
}
/**
* Gets the write concern specified in the connection string.
* @return the write concern
*/
public WriteConcern getWriteConcern() {
return writeConcern;
}
/**
* Gets the minimum connection pool size specified in the connection string.
* @return the minimum connection pool size
*/
public Integer getMinConnectionPoolSize() {
return minConnectionPoolSize;
}
/**
* Gets the maximum connection pool size specified in the connection string.
* @return the maximum connection pool size
*/
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.
* @return the multiplier for the number of threads allowed to block waiting for a connection
*/
public Integer getThreadsAllowedToBlockForConnectionMultiplier() {
return threadsAllowedToBlockForConnectionMultiplier;
}
/**
* 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
*/
public Integer getMaxWaitTime() {
return maxWaitTime;
}
/**
* Gets the maximum connection idle time specified in the connection string.
* @return the maximum connection idle time
*/
public Integer getMaxConnectionIdleTime() {
return maxConnectionIdleTime;
}
/**
* Gets the maximum connection life time specified in the connection string.
* @return the maximum connection life time
*/
public Integer getMaxConnectionLifeTime() {
return maxConnectionLifeTime;
}
/**
* Gets the socket connect timeout specified in the connection string.
* @return the socket connect timeout
*/
public Integer getConnectTimeout() {
return connectTimeout;
}
/**
* Gets the socket timeout specified in the connection string.
* @return the socket timeout
*/
public Integer getSocketTimeout() {
return socketTimeout;
}
/**
* Gets the SSL enabled value specified in the connection string.
* @return the SSL enabled value
*/
public Boolean getSslEnabled() {
return sslEnabled;
}
/**
* Gets the stream type value specified in the connection string.
* @return the stream type value
* @since 3.3
*/
public String getStreamType() {
return streamType;
}
/**
* Gets the SSL invalidHostnameAllowed value specified in the connection string.
*
* @return the SSL invalidHostnameAllowed value
* @since 3.3
*/
public Boolean getSslInvalidHostnameAllowed() {
return sslInvalidHostnameAllowed;
}
/**
* Gets the required replica set name specified in the connection string.
* @return the required replica set name
*/
public String getRequiredReplicaSetName() {
return requiredReplicaSetName;
}
/**
*
* @return the server selection timeout (in milliseconds), or null if unset
* @since 3.3
*/
public Integer getServerSelectionTimeout() {
return serverSelectionTimeout;
}
/**
*
* @return the local threshold (in milliseconds), or null if unset
* since 3.3
*/
public Integer getLocalThreshold() {
return localThreshold;
}
/**
*
* @return the heartbeat frequency (in milliseconds), or null if unset
* since 3.3
*/
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.
*
* <p>Default is null.</p>
*
* @return the application name, which may be null
* @since 3.4
* @mongodb.server.release 3.4
*/
public String getApplicationName() {
return applicationName;
}
@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 (credentials != null ? !credentials.equals(that.credentials) : that.credentials != 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;
}
return true;
}
@Override
public int hashCode() {
int result = credentials != null ? credentials.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);
return result;
}
}