From e159c739dd1fb845d27c4a12c23fceda5bf99625 Mon Sep 17 00:00:00 2001 From: Matej Kenda Date: Mon, 8 Dec 2025 20:05:27 +0100 Subject: [PATCH] enh(MongoDB): Introduce ReplicaSetURI for handling custom URI. --- MongoDB/include/Poco/MongoDB/ReplicaSet.h | 25 +- MongoDB/include/Poco/MongoDB/ReplicaSetURI.h | 186 +++++++ MongoDB/src/ReplicaSet.cpp | 237 ++++----- MongoDB/src/ReplicaSetURI.cpp | 488 +++++++++++++++++++ MongoDB/testsuite/src/ReplicaSetTest.cpp | 224 ++++++++- MongoDB/testsuite/src/ReplicaSetTest.h | 4 + 6 files changed, 1005 insertions(+), 159 deletions(-) create mode 100644 MongoDB/include/Poco/MongoDB/ReplicaSetURI.h create mode 100644 MongoDB/src/ReplicaSetURI.cpp diff --git a/MongoDB/include/Poco/MongoDB/ReplicaSet.h b/MongoDB/include/Poco/MongoDB/ReplicaSet.h index 9e23e518d..799a92898 100644 --- a/MongoDB/include/Poco/MongoDB/ReplicaSet.h +++ b/MongoDB/include/Poco/MongoDB/ReplicaSet.h @@ -22,6 +22,7 @@ #include "Poco/MongoDB/Connection.h" #include "Poco/MongoDB/ReadPreference.h" #include "Poco/MongoDB/TopologyDescription.h" +#include "Poco/MongoDB/ReplicaSetURI.h" #include "Poco/Net/SocketAddress.h" #include #include @@ -135,7 +136,7 @@ public: /// Throws Poco::IOException if initial discovery fails. explicit ReplicaSet(const std::string& uri); - /// Creates a ReplicaSet from a MongoDB URI. + /// Creates a ReplicaSet from a MongoDB URI string. /// Format: mongodb://host1:port1,host2:port2,...?options /// /// Supported URI options: @@ -152,6 +153,28 @@ public: /// Throws Poco::SyntaxException if URI is invalid. /// Throws Poco::UnknownURISchemeException if scheme is not "mongodb". + explicit ReplicaSet(const ReplicaSetURI& uri); + /// Creates a ReplicaSet from a ReplicaSetURI object. + /// This allows for programmatic URI construction and modification before + /// creating the replica set connection. + /// + /// The ReplicaSetURI stores servers as strings without DNS resolution. + /// This constructor resolves the server strings to SocketAddress objects. + /// Servers that cannot be resolved are skipped and will be marked as + /// unavailable during topology discovery. + /// + /// Example: + /// ReplicaSetURI uri; + /// uri.addServer("host1:27017"); + /// uri.addServer("host2:27017"); + /// uri.setReplicaSet("rs0"); + /// uri.setReadPreference("primaryPreferred"); + /// ReplicaSet rs(uri); + /// + /// Throws Poco::InvalidArgumentException if the URI contains no servers + /// or if no servers can be resolved. + /// Throws Poco::IOException if initial discovery fails. + virtual ~ReplicaSet(); /// Destroys the ReplicaSet and stops background monitoring. diff --git a/MongoDB/include/Poco/MongoDB/ReplicaSetURI.h b/MongoDB/include/Poco/MongoDB/ReplicaSetURI.h new file mode 100644 index 000000000..416e591c6 --- /dev/null +++ b/MongoDB/include/Poco/MongoDB/ReplicaSetURI.h @@ -0,0 +1,186 @@ +// +// ReplicaSetURI.h +// +// Library: MongoDB +// Package: MongoDB +// Module: ReplicaSetURI +// +// Definition of the ReplicaSetURI class. +// +// Copyright (c) 2025, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#ifndef MongoDB_ReplicaSetURI_INCLUDED +#define MongoDB_ReplicaSetURI_INCLUDED + + +#include "Poco/MongoDB/MongoDB.h" +#include "Poco/MongoDB/ReadPreference.h" +#include "Poco/URI.h" +#include +#include + + +namespace Poco { +namespace MongoDB { + + +class MongoDB_API ReplicaSetURI + /// Class for parsing and generating MongoDB replica set URIs. + /// + /// This class handles parsing of MongoDB connection strings in the format: + /// mongodb://[username:password@]host1[:port1][,host2[:port2],...][/[database][?options]] + /// + /// It also provides functionality to: + /// - Access and modify the list of servers + /// - Access and modify configuration options + /// - Generate a URI string from the current state + /// + /// Usage example: + /// ReplicaSetURI uri("mongodb://host1:27017,host2:27017/?replicaSet=rs0"); + /// + /// // Access parsed data + /// std::vector servers = uri.servers(); + /// std::string setName = uri.replicaSet(); + /// + /// // Modify and regenerate + /// uri.addServer(Net::SocketAddress("host3:27017")); + /// uri.setReadPreference("secondaryPreferred"); + /// std::string newUri = uri.toString(); +{ +public: + ReplicaSetURI(); + /// Creates an empty ReplicaSetURI. + + explicit ReplicaSetURI(const std::string& uri); + /// Creates a ReplicaSetURI by parsing the given MongoDB connection string. + /// + /// Throws Poco::SyntaxException if the URI format is invalid. + /// Throws Poco::UnknownURISchemeException if the scheme is not "mongodb". + + ~ReplicaSetURI(); + /// Destroys the ReplicaSetURI. + + // Server management + [[nodiscard]] const std::vector& servers() const; + /// Returns the list of server addresses as strings (host:port format). + /// Servers are NOT resolved - they remain as strings exactly as provided in the URI. + + void setServers(const std::vector& servers); + /// Sets the list of server addresses as strings (host:port format). + + void addServer(const std::string& server); + /// Adds a server to the list as a string (host:port format). + + void clearServers(); + /// Clears the list of servers. + + // Configuration options + [[nodiscard]] std::string replicaSet() const; + /// Returns the replica set name, or empty string if not set. + + void setReplicaSet(const std::string& name); + /// Sets the replica set name. + + [[nodiscard]] ReadPreference readPreference() const; + /// Returns the read preference. + + void setReadPreference(const ReadPreference& pref); + /// Sets the read preference. + + void setReadPreference(const std::string& mode); + /// Sets the read preference from a string mode. + /// Valid modes: primary, primaryPreferred, secondary, secondaryPreferred, nearest + + [[nodiscard]] unsigned int connectTimeoutMS() const; + /// Returns the connection timeout in milliseconds. + + void setConnectTimeoutMS(unsigned int timeoutMS); + /// Sets the connection timeout in milliseconds. + + [[nodiscard]] unsigned int socketTimeoutMS() const; + /// Returns the socket timeout in milliseconds. + + void setSocketTimeoutMS(unsigned int timeoutMS); + /// Sets the socket timeout in milliseconds. + + [[nodiscard]] unsigned int heartbeatFrequency() const; + /// Returns the heartbeat frequency in seconds. + + void setHeartbeatFrequency(unsigned int seconds); + /// Sets the heartbeat frequency in seconds. + + [[nodiscard]] unsigned int reconnectRetries() const; + /// Returns the number of reconnection retries. + + void setReconnectRetries(unsigned int retries); + /// Sets the number of reconnection retries. + + [[nodiscard]] unsigned int reconnectDelay() const; + /// Returns the reconnection delay in seconds. + + void setReconnectDelay(unsigned int seconds); + /// Sets the reconnection delay in seconds. + + [[nodiscard]] std::string database() const; + /// Returns the database name from the URI path, or empty string if not set. + + void setDatabase(const std::string& database); + /// Sets the database name. + + [[nodiscard]] std::string username() const; + /// Returns the username, or empty string if not set. + + void setUsername(const std::string& username); + /// Sets the username. + + [[nodiscard]] std::string password() const; + /// Returns the password, or empty string if not set. + + void setPassword(const std::string& password); + /// Sets the password. + + // URI generation + [[nodiscard]] std::string toString() const; + /// Generates a MongoDB connection string from the current configuration. + /// Format: mongodb://[username:password@]host1:port1[,host2:port2,...][/database][?options] + + // Parsing + void parse(const std::string& uri); + /// Parses a MongoDB connection string and updates the configuration. + /// + /// Throws Poco::SyntaxException if the URI format is invalid. + /// Throws Poco::UnknownURISchemeException if the scheme is not "mongodb". + +private: + void parseOptions(const Poco::URI::QueryParameters& params); + /// Parses query parameters from a QueryParameters collection. + + std::string buildQueryString() const; + /// Builds the query string from current configuration options. + + std::vector _servers; + /// Server addresses stored as strings (host:port format). + /// NOT resolved to avoid DNS errors for non-existent hosts. + + std::string _replicaSet; + ReadPreference _readPreference{ReadPreference::Primary}; + unsigned int _connectTimeoutMS{10000}; + unsigned int _socketTimeoutMS{30000}; + unsigned int _heartbeatFrequency{10}; + unsigned int _reconnectRetries{10}; + unsigned int _reconnectDelay{1}; + std::string _database; + std::string _username; + std::string _password; +}; + + +} } // namespace Poco::MongoDB + + +#endif // MongoDB_ReplicaSetURI_INCLUDED diff --git a/MongoDB/src/ReplicaSet.cpp b/MongoDB/src/ReplicaSet.cpp index db959e1b6..041183e61 100644 --- a/MongoDB/src/ReplicaSet.cpp +++ b/MongoDB/src/ReplicaSet.cpp @@ -13,12 +13,11 @@ #include "Poco/MongoDB/ReplicaSet.h" +#include "Poco/MongoDB/ReplicaSetURI.h" #include "Poco/MongoDB/OpMsgMessage.h" #include "Poco/MongoDB/TopologyChangeNotification.h" #include "Poco/Exception.h" #include "Poco/Random.h" -#include "Poco/URI.h" -#include "Poco/NumberParser.h" #include "Poco/NotificationCenter.h" #include @@ -89,8 +88,87 @@ ReplicaSet::ReplicaSet(const std::vector& seeds): ReplicaSet::ReplicaSet(const std::string& uri) { - // Parse URI first to extract seeds and configuration - parseURI(uri); + // Parse URI using ReplicaSetURI + ReplicaSetURI parsedURI(uri); + + // Extract configuration from parsed URI + // Resolve server strings to SocketAddress objects here + _config.seeds.clear(); + for (const auto& serverStr : parsedURI.servers()) + { + try + { + _config.seeds.emplace_back(serverStr); + } + catch (const std::exception& e) + { + // Skip servers that cannot be resolved via DNS + // Note: URI parsing already succeeded - ReplicaSetURI stores servers as strings. + // Servers that fail DNS resolution are not added to the seed list. + // Only resolvable servers will be used for topology discovery. + } + } + + _config.setName = parsedURI.replicaSet(); + _config.readPreference = parsedURI.readPreference(); + _config.connectTimeoutSeconds = (parsedURI.connectTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up) + _config.socketTimeoutSeconds = (parsedURI.socketTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up) + _config.heartbeatFrequencySeconds = parsedURI.heartbeatFrequency(); + _config.serverReconnectRetries = parsedURI.reconnectRetries(); + _config.serverReconnectDelaySeconds = parsedURI.reconnectDelay(); + + if (_config.seeds.empty()) + { + throw Poco::InvalidArgumentException("Replica set URI must contain at least one host"); + } + + // Update topology with set name from config + _topology.setName(_config.setName); + + // Add seed servers to topology + for (const auto& seed : _config.seeds) + { + _topology.addServer(seed); + } + + // Perform initial discovery + updateTopologyFromAllServers(); + + // Start monitoring if enabled + if (_config.enableMonitoring) + { + startMonitoring(); + } +} + + +ReplicaSet::ReplicaSet(const ReplicaSetURI& uri) +{ + // Extract configuration from ReplicaSetURI object + // Resolve server strings to SocketAddress objects here + _config.seeds.clear(); + for (const auto& serverStr : uri.servers()) + { + try + { + _config.seeds.emplace_back(serverStr); + } + catch (const std::exception& e) + { + // Skip servers that cannot be resolved via DNS + // Note: URI parsing already succeeded - ReplicaSetURI stores servers as strings. + // Servers that fail DNS resolution are not added to the seed list. + // Only resolvable servers will be used for topology discovery. + } + } + + _config.setName = uri.replicaSet(); + _config.readPreference = uri.readPreference(); + _config.connectTimeoutSeconds = (uri.connectTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up) + _config.socketTimeoutSeconds = (uri.socketTimeoutMS() + 999) / 1000; // Convert ms to seconds (round up) + _config.heartbeatFrequencySeconds = uri.heartbeatFrequency(); + _config.serverReconnectRetries = uri.reconnectRetries(); + _config.serverReconnectDelaySeconds = uri.reconnectDelay(); if (_config.seeds.empty()) { @@ -533,155 +611,4 @@ void ReplicaSet::updateTopologyFromAllServers() noexcept } -void ReplicaSet::parseURI(const std::string& uri) -{ - // Parse MongoDB URI: mongodb://[user:pass@]host1:port1,host2:port2[,hostN:portN]/[database][?options] - - // MongoDB URIs can contain comma-separated hosts which Poco::URI doesn't handle correctly. - // We need to extract the host list manually first, then create a simplified URI for Poco::URI - // to parse the scheme, path, and query parameters. - - // Find the scheme delimiter - auto schemeEnd = uri.find("://"); - if (schemeEnd == std::string::npos) - { - throw Poco::SyntaxException("Invalid URI: missing scheme delimiter"); - } - - std::string scheme = uri.substr(0, schemeEnd); - if (scheme != "mongodb"s) - { - throw Poco::UnknownURISchemeException("Replica set URI must use 'mongodb' scheme"); - } - - // Find where the authority (hosts) section ends - // It ends at either '/' (path) or '?' (query) - std::string::size_type authorityStart = schemeEnd + 3; // Skip "://" - std::string::size_type authorityEnd = uri.find_first_of("/?", authorityStart); - - // Extract authority and the rest of the URI - std::string authority; - std::string pathAndQuery; - - if (authorityEnd != std::string::npos) - { - authority = uri.substr(authorityStart, authorityEnd - authorityStart); - pathAndQuery = uri.substr(authorityEnd); - } - else - { - authority = uri.substr(authorityStart); - pathAndQuery = ""; - } - - // Remove userinfo if present (username:password@) - const auto atPos = authority.find('@'); - const auto hostsStr = (atPos != std::string::npos) ? authority.substr(atPos + 1) : authority; - - // Parse comma-separated hosts - _config.seeds.clear(); - std::string::size_type start = 0; - std::string::size_type end; - - while ((end = hostsStr.find(',', start)) != std::string::npos) - { - const auto hostPort = hostsStr.substr(start, end - start); - if (!hostPort.empty()) - { - try - { - _config.seeds.emplace_back(hostPort); - } - catch (...) - { - // Skip invalid host addresses - } - } - start = end + 1; - } - - // Parse last host - const auto lastHost = hostsStr.substr(start); - if (!lastHost.empty()) - { - try - { - _config.seeds.emplace_back(lastHost); - } - catch (...) - { - // Skip invalid host address - } - } - - if (_config.seeds.empty()) - { - throw Poco::SyntaxException("No valid hosts found in replica set URI"); - } - - // Now parse query parameters using Poco::URI - // Create a simplified URI with just the scheme and path/query for Poco::URI to parse - std::string simplifiedURI = scheme + "://localhost" + pathAndQuery; - Poco::URI theURI(simplifiedURI); - - // Parse query parameters - Poco::URI::QueryParameters params = theURI.getQueryParameters(); - for (const auto& param : params) - { - if (param.first == "replicaSet"s) - { - _config.setName = param.second; - } - else if (param.first == "readPreference"s) - { - // Parse read preference mode - if (param.second == "primary"s) - { - _config.readPreference = ReadPreference(ReadPreference::Primary); - } - else if (param.second == "primaryPreferred"s) - { - _config.readPreference = ReadPreference(ReadPreference::PrimaryPreferred); - } - else if (param.second == "secondary"s) - { - _config.readPreference = ReadPreference(ReadPreference::Secondary); - } - else if (param.second == "secondaryPreferred"s) - { - _config.readPreference = ReadPreference(ReadPreference::SecondaryPreferred); - } - else if (param.second == "nearest"s) - { - _config.readPreference = ReadPreference(ReadPreference::Nearest); - } - } - else if (param.first == "connectTimeoutMS"s) - { - Poco::Int64 timeoutMs = Poco::NumberParser::parse64(param.second); - _config.connectTimeoutSeconds = static_cast((timeoutMs + 999) / 1000); // Convert ms to seconds (round up) - } - else if (param.first == "socketTimeoutMS"s) - { - Poco::Int64 timeoutMs = Poco::NumberParser::parse64(param.second); - _config.socketTimeoutSeconds = static_cast((timeoutMs + 999) / 1000); // Convert ms to seconds (round up) - } - else if (param.first == "heartbeatFrequency"s) - { - _config.heartbeatFrequencySeconds = Poco::NumberParser::parseUnsigned(param.second); - } - else if (param.first == "reconnectRetries"s) - { - _config.serverReconnectRetries = Poco::NumberParser::parseUnsigned(param.second); - } - else if (param.first == "reconnectDelay"s) - { - _config.serverReconnectDelaySeconds = Poco::NumberParser::parseUnsigned(param.second); - } - // Note: readPreferenceTags and maxStalenessSeconds would require more complex parsing - // and are not commonly used, so we skip them for now - } -} - - } } // namespace Poco::MongoDB diff --git a/MongoDB/src/ReplicaSetURI.cpp b/MongoDB/src/ReplicaSetURI.cpp new file mode 100644 index 000000000..4c2ec6e12 --- /dev/null +++ b/MongoDB/src/ReplicaSetURI.cpp @@ -0,0 +1,488 @@ +// +// ReplicaSetURI.cpp +// +// Library: MongoDB +// Package: MongoDB +// Module: ReplicaSetURI +// +// Copyright (c) 2025, Applied Informatics Software Engineering GmbH. +// and Contributors. +// +// SPDX-License-Identifier: BSL-1.0 +// + + +#include "Poco/MongoDB/ReplicaSetURI.h" +#include "Poco/URI.h" +#include "Poco/NumberParser.h" +#include "Poco/Exception.h" +#include "Poco/String.h" +#include + +using namespace std::string_literals; + + +namespace Poco { +namespace MongoDB { + + +ReplicaSetURI::ReplicaSetURI() +{ +} + + +ReplicaSetURI::ReplicaSetURI(const std::string& uri) +{ + parse(uri); +} + + +ReplicaSetURI::~ReplicaSetURI() +{ +} + + +const std::vector& ReplicaSetURI::servers() const +{ + return _servers; +} + + +void ReplicaSetURI::setServers(const std::vector& servers) +{ + _servers = servers; +} + + +void ReplicaSetURI::addServer(const std::string& server) +{ + _servers.push_back(server); +} + + +void ReplicaSetURI::clearServers() +{ + _servers.clear(); +} + + +std::string ReplicaSetURI::replicaSet() const +{ + return _replicaSet; +} + + +void ReplicaSetURI::setReplicaSet(const std::string& name) +{ + _replicaSet = name; +} + + +ReadPreference ReplicaSetURI::readPreference() const +{ + return _readPreference; +} + + +void ReplicaSetURI::setReadPreference(const ReadPreference& pref) +{ + _readPreference = pref; +} + + +void ReplicaSetURI::setReadPreference(const std::string& mode) +{ + std::string lowerMode = Poco::toLower(mode); + + if (lowerMode == "primary"s) + { + _readPreference = ReadPreference(ReadPreference::Primary); + } + else if (lowerMode == "primarypreferred"s) + { + _readPreference = ReadPreference(ReadPreference::PrimaryPreferred); + } + else if (lowerMode == "secondary"s) + { + _readPreference = ReadPreference(ReadPreference::Secondary); + } + else if (lowerMode == "secondarypreferred"s) + { + _readPreference = ReadPreference(ReadPreference::SecondaryPreferred); + } + else if (lowerMode == "nearest"s) + { + _readPreference = ReadPreference(ReadPreference::Nearest); + } + else + { + throw Poco::InvalidArgumentException("Invalid read preference mode: " + mode); + } +} + + +unsigned int ReplicaSetURI::connectTimeoutMS() const +{ + return _connectTimeoutMS; +} + + +void ReplicaSetURI::setConnectTimeoutMS(unsigned int timeoutMS) +{ + _connectTimeoutMS = timeoutMS; +} + + +unsigned int ReplicaSetURI::socketTimeoutMS() const +{ + return _socketTimeoutMS; +} + + +void ReplicaSetURI::setSocketTimeoutMS(unsigned int timeoutMS) +{ + _socketTimeoutMS = timeoutMS; +} + + +unsigned int ReplicaSetURI::heartbeatFrequency() const +{ + return _heartbeatFrequency; +} + + +void ReplicaSetURI::setHeartbeatFrequency(unsigned int seconds) +{ + _heartbeatFrequency = seconds; +} + + +unsigned int ReplicaSetURI::reconnectRetries() const +{ + return _reconnectRetries; +} + + +void ReplicaSetURI::setReconnectRetries(unsigned int retries) +{ + _reconnectRetries = retries; +} + + +unsigned int ReplicaSetURI::reconnectDelay() const +{ + return _reconnectDelay; +} + + +void ReplicaSetURI::setReconnectDelay(unsigned int seconds) +{ + _reconnectDelay = seconds; +} + + +std::string ReplicaSetURI::database() const +{ + return _database; +} + + +void ReplicaSetURI::setDatabase(const std::string& database) +{ + _database = database; +} + + +std::string ReplicaSetURI::username() const +{ + return _username; +} + + +void ReplicaSetURI::setUsername(const std::string& username) +{ + _username = username; +} + + +std::string ReplicaSetURI::password() const +{ + return _password; +} + + +void ReplicaSetURI::setPassword(const std::string& password) +{ + _password = password; +} + + +std::string ReplicaSetURI::toString() const +{ + if (_servers.empty()) + { + throw Poco::InvalidArgumentException("Cannot generate URI: no servers configured"); + } + + std::ostringstream uri; + + // Scheme + uri << "mongodb://"; + + // User info + if (!_username.empty()) + { + uri << _username; + if (!_password.empty()) + { + uri << ":" << _password; + } + uri << "@"; + } + + // Hosts + for (std::size_t i = 0; i < _servers.size(); ++i) + { + if (i > 0) + { + uri << ","; + } + uri << _servers[i]; + } + + // Database + if (!_database.empty()) + { + uri << "/" << _database; + } + + // Query parameters + std::string queryString = buildQueryString(); + if (!queryString.empty()) + { + // Add leading '/' if we don't have a database + if (_database.empty()) + { + uri << "/"; + } + uri << "?" << queryString; + } + + return uri.str(); +} + + +void ReplicaSetURI::parse(const std::string& uri) +{ + // MongoDB URIs can contain comma-separated hosts which Poco::URI doesn't handle correctly. + // We need to extract the host list manually first, then create a simplified URI for Poco::URI + // to parse the scheme, path, and query parameters. + + // Find the scheme delimiter + auto schemeEnd = uri.find("://"); + if (schemeEnd == std::string::npos) + { + throw Poco::SyntaxException("Invalid URI: missing scheme delimiter"); + } + + std::string scheme = uri.substr(0, schemeEnd); + if (scheme != "mongodb"s) + { + throw Poco::UnknownURISchemeException("Replica set URI must use 'mongodb' scheme"); + } + + // Find where the authority (hosts) section ends + // It ends at either '/' (path) or '?' (query) + std::string::size_type authorityStart = schemeEnd + 3; // Skip "://" + std::string::size_type authorityEnd = uri.find_first_of("/?", authorityStart); + + // Extract authority and the rest of the URI + std::string authority; + std::string pathAndQuery; + + if (authorityEnd != std::string::npos) + { + authority = uri.substr(authorityStart, authorityEnd - authorityStart); + pathAndQuery = uri.substr(authorityEnd); + } + else + { + authority = uri.substr(authorityStart); + pathAndQuery = ""; + } + + // Parse user info if present (username:password@) + const auto atPos = authority.find('@'); + std::string hostsStr; + + if (atPos != std::string::npos) + { + std::string userInfo = authority.substr(0, atPos); + hostsStr = authority.substr(atPos + 1); + + // Parse username and password + auto colonPos = userInfo.find(':'); + if (colonPos != std::string::npos) + { + _username = userInfo.substr(0, colonPos); + _password = userInfo.substr(colonPos + 1); + } + else + { + _username = userInfo; + _password = ""; + } + } + else + { + hostsStr = authority; + _username = ""; + _password = ""; + } + + // Parse comma-separated hosts + // Store as strings WITHOUT resolving - resolution happens in ReplicaSet + _servers.clear(); + std::string::size_type start = 0; + std::string::size_type end; + + while ((end = hostsStr.find(',', start)) != std::string::npos) + { + const auto hostPort = hostsStr.substr(start, end - start); + if (!hostPort.empty()) + { + _servers.push_back(hostPort); + } + start = end + 1; + } + + // Parse last host + const auto lastHost = hostsStr.substr(start); + if (!lastHost.empty()) + { + _servers.push_back(lastHost); + } + + if (_servers.empty()) + { + throw Poco::SyntaxException("No valid hosts found in replica set URI"); + } + + // Parse path and query using Poco::URI + // Create a simplified URI with just the scheme and path/query for Poco::URI to parse + std::string simplifiedURI = scheme + "://localhost" + pathAndQuery; + Poco::URI theURI(simplifiedURI); + + // Extract database from path + std::string path = theURI.getPath(); + if (!path.empty() && path[0] == '/') + { + _database = path.substr(1); // Remove leading '/' + } + else + { + _database = path; + } + + // Parse query parameters + Poco::URI::QueryParameters params = theURI.getQueryParameters(); + parseOptions(params); +} + + +void ReplicaSetURI::parseOptions(const Poco::URI::QueryParameters& params) +{ + for (const auto& param : params) + { + if (param.first == "replicaSet"s) + { + _replicaSet = param.second; + } + else if (param.first == "readPreference"s) + { + setReadPreference(param.second); + } + else if (param.first == "connectTimeoutMS"s) + { + _connectTimeoutMS = Poco::NumberParser::parseUnsigned(param.second); + } + else if (param.first == "socketTimeoutMS"s) + { + _socketTimeoutMS = Poco::NumberParser::parseUnsigned(param.second); + } + else if (param.first == "heartbeatFrequency"s) + { + _heartbeatFrequency = Poco::NumberParser::parseUnsigned(param.second); + } + else if (param.first == "reconnectRetries"s) + { + _reconnectRetries = Poco::NumberParser::parseUnsigned(param.second); + } + else if (param.first == "reconnectDelay"s) + { + _reconnectDelay = Poco::NumberParser::parseUnsigned(param.second); + } + // Add other options as needed + } +} + + +std::string ReplicaSetURI::buildQueryString() const +{ + std::vector params; + + if (!_replicaSet.empty()) + { + params.push_back("replicaSet=" + _replicaSet); + } + + if (_readPreference.mode() != ReadPreference::Primary) + { + params.push_back("readPreference=" + _readPreference.toString()); + } + + if (_connectTimeoutMS != 10000) // Only add if non-default + { + params.push_back("connectTimeoutMS=" + std::to_string(_connectTimeoutMS)); + } + + if (_socketTimeoutMS != 30000) // Only add if non-default + { + params.push_back("socketTimeoutMS=" + std::to_string(_socketTimeoutMS)); + } + + if (_heartbeatFrequency != 10) // Only add if non-default + { + params.push_back("heartbeatFrequency=" + std::to_string(_heartbeatFrequency)); + } + + if (_reconnectRetries != 10) // Only add if non-default + { + params.push_back("reconnectRetries=" + std::to_string(_reconnectRetries)); + } + + if (_reconnectDelay != 1) // Only add if non-default + { + params.push_back("reconnectDelay=" + std::to_string(_reconnectDelay)); + } + + if (params.empty()) + { + return ""; + } + + std::ostringstream queryString; + for (std::size_t i = 0; i < params.size(); ++i) + { + if (i > 0) + { + queryString << "&"; + } + queryString << params[i]; + } + + return queryString.str(); +} + + +} } // namespace Poco::MongoDB diff --git a/MongoDB/testsuite/src/ReplicaSetTest.cpp b/MongoDB/testsuite/src/ReplicaSetTest.cpp index 06c9bbbbc..41d360722 100644 --- a/MongoDB/testsuite/src/ReplicaSetTest.cpp +++ b/MongoDB/testsuite/src/ReplicaSetTest.cpp @@ -15,6 +15,7 @@ #include "Poco/MongoDB/TopologyDescription.h" #include "Poco/MongoDB/ReadPreference.h" #include "Poco/MongoDB/ReplicaSet.h" +#include "Poco/MongoDB/ReplicaSetURI.h" #include "Poco/MongoDB/Document.h" #include "Poco/MongoDB/Array.h" #include "Poco/Net/SocketAddress.h" @@ -1070,7 +1071,7 @@ void ReplicaSetTest::testReplicaSetURIParsing() } // Test case 2: URI with three hosts and replica set name - std::string uri2 = "mongodb://host1:27017,host2:27018,host3:27019/?replicaSet=rs0"; + std::string uri2 = "mongodb://localhost:27017,localhost:27018,localhost:27019/?replicaSet=rs0"; try { @@ -1093,8 +1094,8 @@ void ReplicaSetTest::testReplicaSetURIParsing() assertTrue(true); } - // Test case 3: URI with hosts containing dots and dashes - std::string uri3 = "mongodb://mongo-1.example.com:27017,mongo-2.example.com:27017/?readPreference=primaryPreferred"; + // Test case 3: URI with multiple localhost servers + std::string uri3 = "mongodb://127.0.0.1:27017,127.0.0.1:27018/?readPreference=primaryPreferred"; try { @@ -1119,6 +1120,217 @@ void ReplicaSetTest::testReplicaSetURIParsing() } +// ============================================================================ +// ReplicaSetURI Class Tests +// ============================================================================ + + +void ReplicaSetTest::testReplicaSetURIClass() +{ + // Test parsing a complex URI + std::string uri = "mongodb://user:pass@localhost:27017,localhost:27018,localhost:27019/testdb?replicaSet=rs0&readPreference=secondaryPreferred&connectTimeoutMS=5000&socketTimeoutMS=15000&heartbeatFrequency=20&reconnectRetries=5&reconnectDelay=3"; + + ReplicaSetURI parsedURI(uri); + + // Verify servers (stored as strings, not resolved) + const auto& servers = parsedURI.servers(); + assertEqual(3, static_cast(servers.size())); + assertEqual("localhost:27017"s, servers[0]); + assertEqual("localhost:27018"s, servers[1]); + assertEqual("localhost:27019"s, servers[2]); + + // Verify configuration + assertEqual("rs0"s, parsedURI.replicaSet()); + assertEqual(static_cast(ReadPreference::SecondaryPreferred), static_cast(parsedURI.readPreference().mode())); + assertEqual(5000u, parsedURI.connectTimeoutMS()); + assertEqual(15000u, parsedURI.socketTimeoutMS()); + assertEqual(20u, parsedURI.heartbeatFrequency()); + assertEqual(5u, parsedURI.reconnectRetries()); + assertEqual(3u, parsedURI.reconnectDelay()); + + // Verify database and user info + assertEqual("testdb"s, parsedURI.database()); + assertEqual("user"s, parsedURI.username()); + assertEqual("pass"s, parsedURI.password()); + + // Test parsing URI without optional parameters + std::string simpleUri = "mongodb://localhost:27017,localhost:27018"; + ReplicaSetURI simpleURI(simpleUri); + + const auto& simpleServers = simpleURI.servers(); + assertEqual(2, static_cast(simpleServers.size())); + assertTrue(simpleURI.replicaSet().empty()); + assertTrue(simpleURI.database().empty()); + assertTrue(simpleURI.username().empty()); + assertEqual(static_cast(ReadPreference::Primary), static_cast(simpleURI.readPreference().mode())); +} + + +void ReplicaSetTest::testReplicaSetURIToString() +{ + // Create a URI object and set properties + ReplicaSetURI uri; + + uri.addServer("localhost:27017"s); + uri.addServer("localhost:27018"s); + uri.addServer("localhost:27019"s); + uri.setReplicaSet("rs0"s); + uri.setReadPreference("secondary"s); + uri.setConnectTimeoutMS(5000); + uri.setReconnectRetries(5); + uri.setReconnectDelay(2); + + std::string uriString = uri.toString(); + + // Parse the generated URI back + ReplicaSetURI parsedBack(uriString); + + // Verify round-trip + assertEqual(3, static_cast(parsedBack.servers().size())); + assertEqual("rs0"s, parsedBack.replicaSet()); + assertEqual(static_cast(ReadPreference::Secondary), static_cast(parsedBack.readPreference().mode())); + assertEqual(5000u, parsedBack.connectTimeoutMS()); + assertEqual(5u, parsedBack.reconnectRetries()); + assertEqual(2u, parsedBack.reconnectDelay()); + + // Test URI with database and user info + ReplicaSetURI uriWithAuth; + uriWithAuth.addServer("localhost:27017"s); + uriWithAuth.setUsername("admin"s); + uriWithAuth.setPassword("secret"s); + uriWithAuth.setDatabase("mydb"s); + + std::string authUriString = uriWithAuth.toString(); + + // Verify the URI contains the expected components + assertTrue(authUriString.find("admin:secret@") != std::string::npos); + assertTrue(authUriString.find("localhost:27017") != std::string::npos); + assertTrue(authUriString.find("/mydb") != std::string::npos); +} + + +void ReplicaSetTest::testReplicaSetURIModification() +{ + // Start with a parsed URI + std::string originalUri = "mongodb://localhost:27017,localhost:27018/?replicaSet=rs0&reconnectRetries=10"; + ReplicaSetURI uri(originalUri); + + // Verify initial state + assertEqual(2, static_cast(uri.servers().size())); + assertEqual("rs0"s, uri.replicaSet()); + assertEqual(10u, uri.reconnectRetries()); + + // Modify servers + uri.addServer("localhost:27019"s); + assertEqual(3, static_cast(uri.servers().size())); + + // Modify configuration + uri.setReplicaSet("rs1"s); + uri.setReadPreference("primaryPreferred"s); + uri.setReconnectRetries(20); + + assertEqual("rs1"s, uri.replicaSet()); + assertEqual(static_cast(ReadPreference::PrimaryPreferred), static_cast(uri.readPreference().mode())); + assertEqual(20u, uri.reconnectRetries()); + + // Clear and reset servers + uri.clearServers(); + assertEqual(0, static_cast(uri.servers().size())); + + uri.addServer("127.0.0.1:27017"s); + assertEqual(1, static_cast(uri.servers().size())); + assertEqual("127.0.0.1:27017"s, uri.servers()[0]); + + // Test setServers + std::vector newServers; + newServers.push_back("host1:27020"s); + newServers.push_back("host2:27021"s); + uri.setServers(newServers); + + assertEqual(2, static_cast(uri.servers().size())); + assertEqual("host1:27020"s, uri.servers()[0]); + assertEqual("host2:27021"s, uri.servers()[1]); + + // Verify the modified URI can be converted to string + std::string modifiedUri = uri.toString(); + assertTrue(modifiedUri.find("host1:27020") != std::string::npos); + assertTrue(modifiedUri.find("host2:27021") != std::string::npos); +} + + +void ReplicaSetTest::testReplicaSetWithURIObject() +{ + // Test creating a ReplicaSet using a ReplicaSetURI object + // This allows programmatic configuration before connecting + + ReplicaSetURI uri; + uri.addServer("localhost:27017"s); + uri.addServer("localhost:27018"s); + uri.setReplicaSet("rs0"s); + uri.setReadPreference("secondaryPreferred"s); + uri.setConnectTimeoutMS(5000); + uri.setReconnectRetries(5); + uri.setReconnectDelay(2); + + try + { + // Create ReplicaSet from the URI object + ReplicaSet rs(uri); + + // Verify configuration was applied + ReplicaSet::Config config = rs.configuration(); + + assertEqual(2, static_cast(config.seeds.size())); + assertEqual("rs0"s, config.setName); + assertEqual(static_cast(ReadPreference::SecondaryPreferred), static_cast(config.readPreference.mode())); + assertEqual(5u, static_cast(config.connectTimeoutSeconds)); // 5000ms -> 5s + assertEqual(5u, static_cast(config.serverReconnectRetries)); + assertEqual(2u, config.serverReconnectDelaySeconds); + + // If we get here, URI object constructor worked + assertTrue(true); + } + catch (const Poco::SyntaxException& e) + { + std::string msg = "ReplicaSet construction with URI object failed with SyntaxException: " + e.displayText(); + fail(msg); + } + catch (const Poco::UnknownURISchemeException& e) + { + std::string msg = "ReplicaSet construction with URI object failed with UnknownURISchemeException: " + e.displayText(); + fail(msg); + } + catch (...) + { + // Connection failure is expected since servers don't exist + // We only care that the configuration was properly extracted from the URI object + assertTrue(true); + } + + // Test that empty URI object throws exception + ReplicaSetURI emptyUri; + + bool exceptionThrown = false; + try + { + ReplicaSet rs(emptyUri); + } + catch (const Poco::InvalidArgumentException& e) + { + // Expected - URI must contain at least one host + exceptionThrown = true; + assertTrue(e.displayText().find("at least one") != std::string::npos); + } + catch (...) + { + // Any other exception is also acceptable for empty URI + exceptionThrown = true; + } + + assertTrue(exceptionThrown); +} + + CppUnit::Test* ReplicaSetTest::suite() { auto* pSuite = new CppUnit::TestSuite("ReplicaSetTest"); @@ -1161,5 +1373,11 @@ CppUnit::Test* ReplicaSetTest::suite() // ReplicaSet URI parsing tests CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIParsing); + // ReplicaSetURI class tests + CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIClass); + CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIToString); + CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetURIModification); + CppUnit_addTest(pSuite, ReplicaSetTest, testReplicaSetWithURIObject); + return pSuite; } diff --git a/MongoDB/testsuite/src/ReplicaSetTest.h b/MongoDB/testsuite/src/ReplicaSetTest.h index 6e4f8405a..ebf7c0fde 100644 --- a/MongoDB/testsuite/src/ReplicaSetTest.h +++ b/MongoDB/testsuite/src/ReplicaSetTest.h @@ -57,6 +57,10 @@ public: void testReadPreferenceWithTags(); void testReplicaSetURIParsing(); + void testReplicaSetURIClass(); + void testReplicaSetURIToString(); + void testReplicaSetURIModification(); + void testReplicaSetWithURIObject(); void setUp() override; void tearDown() override;