All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.mysql.cj.conf.ConnectionUrlParser Maven / Gradle / Ivy

There is a newer version: 9.1.0
Show newest version
/*
 * Copyright (c) 2016, 2024, Oracle and/or its affiliates.
 *
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License, version 2.0, as published by
 * the Free Software Foundation.
 *
 * This program is designed to work with certain software that is licensed under separate terms, as designated in a particular file or component or in
 * included license documentation. The authors of MySQL hereby grant you an additional permission to link the program and your derivative works with the
 * separately licensed software that they have either included with the program or referenced in the documentation.
 *
 * Without limiting anything contained in the foregoing, this file, which is part of MySQL Connector/J, is also subject to the Universal FOSS Exception,
 * version 1.0, a copy of which can be found at http://oss.oracle.com/licenses/universal-foss-exception.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License, version 2.0, for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc.,
 * 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
 */

package com.mysql.cj.conf;

import static com.mysql.cj.util.StringUtils.isNullOrEmpty;
import static com.mysql.cj.util.StringUtils.safeTrim;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import com.mysql.cj.Messages;
import com.mysql.cj.conf.ConnectionUrl.Type;
import com.mysql.cj.exceptions.ExceptionFactory;
import com.mysql.cj.exceptions.UnsupportedConnectionStringException;
import com.mysql.cj.exceptions.WrongArgumentException;
import com.mysql.cj.util.SearchMode;
import com.mysql.cj.util.StringUtils;

/**
 * This class parses a connection string using the general URI structure defined in RFC 3986. Instead of using a URI instance to ensure the correct syntax of
 * the connection string, this implementation uses regular expressions which is faster but also less strict in terms of validations. This actually works better
 * because database URLs don't exactly stick to the RFC 3986 rules.
 * 

* scheme://authority/path?query#fragment *

* This results in splitting the connection string URL and processing its internal parts: *

*
scheme
*
The protocol and subprotocol identification. Usually "jdbc:mysql:" or "mysqlx:".
*
authority
*
Contains information about the user credentials and/or the host and port information. Unlike its definition in the RFC 3986 specification, there can be * multiple authority sections separated by a single comma (,) in a connection string. It is also possible to use an alternative syntax for the user and/or host * identification, that also allows setting per host connection properties, in the form of * "[user[:password]@]address=(key1=value)[(key2=value)]...[,address=(key3=value)[(key4=value)]...]...".
*
path
*
Corresponds to the database identification.
*
query
*
The connection properties, written as "propertyName1[=[propertyValue1]][&propertyName2[=[propertyValue2]]]..."
*
fragment
*
The fragment section is ignored in Connector/J connection strings.
*
*/ public class ConnectionUrlParser implements DatabaseUrlContainer { private static final String DUMMY_SCHEMA = "cj://"; private static final String USER_PASS_SEPARATOR = ":"; private static final String USER_HOST_SEPARATOR = "@"; private static final String HOSTS_SEPARATOR = ","; private static final String KEY_VALUE_HOST_INFO_OPENING_MARKER = "("; private static final String KEY_VALUE_HOST_INFO_CLOSING_MARKER = ")"; private static final String HOSTS_LIST_OPENING_MARKERS = "[("; private static final String HOSTS_LIST_CLOSING_MARKERS = "])"; private static final String ADDRESS_EQUALS_HOST_INFO_PREFIX = "ADDRESS="; private static final Pattern CONNECTION_STRING_PTRN = Pattern.compile("(?[\\w\\+:%]+)\\s*" // scheme: required; alphanumeric, plus, colon or percent + "(?://(?[^/?#]*))?\\s*" // authority: optional; starts with "//" followed by any char except "/", "?" and "#" + "(?:/(?!\\s*/)(?[^?#]*))?" // path: optional; starts with "/" but not followed by "/", and then followed by by any char except "?" and "#" + "(?:\\?(?!\\s*\\?)(?[^#]*))?" // query: optional; starts with "?" but not followed by "?", and then followed by by any char except "#" + "(?:\\s*#(?.*))?"); // fragment: optional; starts with "#", and then followed by anything private static final Pattern SCHEME_PTRN = Pattern.compile("(?[\\w\\+:%]+).*"); private static final Pattern HOST_LIST_PTRN = Pattern.compile("^\\[(?.*)\\]$"); private static final Pattern GENERIC_HOST_PTRN = Pattern.compile("^(?.*?)(?::(?[^:]*))?$"); private static final Pattern KEY_VALUE_HOST_PTRN = Pattern.compile("[,\\s]*(?[\\w\\.\\-\\s%]*)(?:=(?[^,]*))?"); private static final Pattern ADDRESS_EQUALS_HOST_PTRN = Pattern.compile("\\s*\\(\\s*(?[\\w\\.\\-%]+)?\\s*(?:=(?[^)]*))?\\)\\s*"); private static final Pattern PROPERTIES_PTRN = Pattern.compile("[&\\s]*(?[\\w\\.\\-\\s%]*)(?:=(?[^&]*))?"); private final String baseConnectionString; private String scheme; private String authority; private String path; private String query; private List parsedHosts = null; private Map parsedProperties = null; /** * Static factory method for constructing instances of this class. * * @param connString * The connection string to parse. * @return an instance of {@link ConnectionUrlParser} */ public static ConnectionUrlParser parseConnectionString(String connString) { return new ConnectionUrlParser(connString); } /** * Constructs a connection string parser for the given connection string. * * @param connString * the connection string to parse */ private ConnectionUrlParser(String connString) { if (connString == null) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0")); } if (!isConnectionStringSupported(connString)) { throw ExceptionFactory.createException(UnsupportedConnectionStringException.class, Messages.getString("ConnectionString.17", new String[] { connString })); } this.baseConnectionString = connString; parseConnectionString(); } /** * Checks if the scheme part of given connection string matches one of the {@link Type}s supported by Connector/J. * Throws {@link WrongArgumentException} if connString is null. * * @param connString * connection string * @return true if supported */ public static boolean isConnectionStringSupported(String connString) { if (connString == null) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.0")); } Matcher matcher = SCHEME_PTRN.matcher(connString); return matcher.matches() && Type.isSupported(decodeSkippingPlusSign(matcher.group("scheme"))); } /** * Splits the connection string in its main sections. */ private void parseConnectionString() { String connString = this.baseConnectionString; Matcher matcher = CONNECTION_STRING_PTRN.matcher(connString); if (!matcher.matches()) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.1")); } this.scheme = decodeSkippingPlusSign(matcher.group("scheme")); this.authority = matcher.group("authority"); // Don't decode just yet. this.path = matcher.group("path") == null ? null : decode(matcher.group("path")).trim(); this.query = matcher.group("query"); // Don't decode just yet. } /** * Parses the authority section (user and/or host identification) of the connection string URI. */ private void parseAuthoritySection() { if (isNullOrEmpty(this.authority)) { // Add an empty, default, host. this.parsedHosts.add(new HostInfo()); return; } List authoritySegments = StringUtils.split(this.authority, HOSTS_SEPARATOR, HOSTS_LIST_OPENING_MARKERS, HOSTS_LIST_CLOSING_MARKERS, true, SearchMode.__MRK_WS); for (String hi : authoritySegments) { parseAuthoritySegment(hi); } } /** * Parses the given sub authority segment, which can take one of the following syntaxes: *
    *
  • _user_:_password_@_host_:_port_ *
  • _user_:_password_@(key1=value1,key2=value2,...) *
  • _user_:_password_@address=(key1=value1)(key2=value2)... *
  • _user_:_password_@[_any_of_the_above_1_,_any_of_the_above_2_,...] *
* Most of the above placeholders can be omitted, representing a null, empty, or default value. * The placeholder _host_, can be a host name, IPv4 or IPv6. This parser doesn't check IP syntax. IPv6 addresses are enclosed by square brackets ([::1]). * The placeholder _any_of_the_above_?_ can be any of the above except for the user information part (_user_:_password_@). * When the symbol ":" is not used, it means an null/empty password or a default (HostInfo.NO_PORT) port, respectively. * When the symbol "@" is not used, it means that the authority part doesn't contain user information (depending on the scheme type can still be provided * via key=value pairs). * * @param authSegment * the string containing the authority segment */ private void parseAuthoritySegment(String authSegment) { /* * Start by splitting the user and host information parts from the authority segment and process the user information, if any. */ Pair userHostInfoSplit = splitByUserInfoAndHostInfo(authSegment); String userInfo = safeTrim(userHostInfoSplit.left); String user = null; String password = null; if (!isNullOrEmpty(userInfo)) { Pair userInfoPair = parseUserInfo(userInfo); user = decode(safeTrim(userInfoPair.left)); password = decode(safeTrim(userInfoPair.right)); } String hostInfo = safeTrim(userHostInfoSplit.right); /* * Handle an authority part without host information. */ HostInfo hi = buildHostInfoForEmptyHost(user, password, hostInfo); if (hi != null) { this.parsedHosts.add(hi); return; } /* * Try using a java.net.URI instance to parse the host information. This helps dealing with the IPv6 syntax. */ hi = buildHostInfoResortingToUriParser(user, password, authSegment); if (hi != null) { this.parsedHosts.add(hi); return; } /* * Using a URI didn't work, now check if the host part is composed by a sub list of hosts and process them, one by one if so. */ List hiList = buildHostInfoResortingToSubHostsListParser(user, password, hostInfo); if (hiList != null) { this.parsedHosts.addAll(hiList); return; } /* * The hosts list syntax didn't work, now check if the host information is written in the alternate syntax "(Key1=value1,key2=value2)". */ hi = buildHostInfoResortingToKeyValueSyntaxParser(user, password, hostInfo); if (hi != null) { this.parsedHosts.add(hi); return; } /* * Key/value syntax didn't work either, now check if the host information is written in the alternate syntax "address=(...)". * This parser needs to run after the key/value one because a key named "address" could invalidate it. */ hi = buildHostInfoResortingToAddressEqualsSyntaxParser(user, password, hostInfo); if (hi != null) { this.parsedHosts.add(hi); return; } /* * Alternate syntax also failed, let's wind up the corner cases the URI couldn't handle. */ hi = buildHostInfoResortingToGenericSyntaxParser(user, password, hostInfo); if (hi != null) { this.parsedHosts.add(hi); return; } /* * Failed parsing the authority segment. */ throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.2", new Object[] { authSegment })); } /** * Builds an {@link HostInfo} instance for empty host authority segments. * * @param user * the user to include in the final {@link HostInfo} * @param password * the password to include in the final {@link HostInfo} * @param hostInfo * the string containing the host information part * @return the {@link HostInfo} instance containing the parsed information or null if the host part is not empty */ private HostInfo buildHostInfoForEmptyHost(String user, String password, String hostInfo) { if (isNullOrEmpty(hostInfo)) { return new HostInfo(this, null, HostInfo.NO_PORT, user, password); } return null; } /** * Parses the host information resorting to a URI object. This process handles most single-host well formed addresses. * * @param user * the user to include in the final {@link HostInfo} * @param password * the password to include in the final {@link HostInfo} * @param hostInfo * the string containing the host information part * * @return the {@link HostInfo} instance containing the parsed information or null if unable to parse the host information */ private HostInfo buildHostInfoResortingToUriParser(String user, String password, String hostInfo) { String host = null; int port = HostInfo.NO_PORT; try { URI uri = URI.create(DUMMY_SCHEMA + hostInfo); if (uri.getHost() != null) { host = decode(uri.getHost()); } if (uri.getPort() != -1) { // getPort() returns -1 if the port is undefined. port = uri.getPort(); } if (uri.getUserInfo() != null) { // Can't have another one. The user information should have been handled already. return null; } } catch (IllegalArgumentException e) { // The URI failed to parse the host information. return null; } if (host != null || port != HostInfo.NO_PORT) { // The host info parsing succeeded. return new HostInfo(this, host, port, user, password); } return null; } /** * Parses the host information using the alternate sub hosts lists syntax "[host1, host2, ...]". * * @param user * the user to include in all the resulting {@link HostInfo} * @param password * the password to include in all the resulting {@link HostInfo} * @param hostInfo * the string containing the host information part * @return a list with all {@link HostInfo} instances containing the parsed information or null if unable to parse the host information */ private List buildHostInfoResortingToSubHostsListParser(String user, String password, String hostInfo) { Matcher matcher = HOST_LIST_PTRN.matcher(hostInfo); if (matcher.matches()) { String hosts = matcher.group("hosts"); List hostsList = StringUtils.split(hosts, HOSTS_SEPARATOR, HOSTS_LIST_OPENING_MARKERS, HOSTS_LIST_CLOSING_MARKERS, true, SearchMode.__MRK_WS); // One single element could, in fact, be an IPv6 stripped from its delimiters. boolean maybeIPv6 = hostsList.size() == 1 && hostsList.get(0).matches("(?i)^[\\dabcdef:]+$"); List hostInfoList = new ArrayList<>(); for (String h : hostsList) { HostInfo hi; if ((hi = buildHostInfoForEmptyHost(user, password, h)) != null) { hostInfoList.add(hi); } else if ((hi = buildHostInfoResortingToUriParser(user, password, h)) != null || maybeIPv6 && (hi = buildHostInfoResortingToUriParser(user, password, "[" + h + "]")) != null) { hostInfoList.add(hi); } else if ((hi = buildHostInfoResortingToKeyValueSyntaxParser(user, password, h)) != null) { hostInfoList.add(hi); } else if ((hi = buildHostInfoResortingToAddressEqualsSyntaxParser(user, password, h)) != null) { hostInfoList.add(hi); } else if ((hi = buildHostInfoResortingToGenericSyntaxParser(user, password, h)) != null) { hostInfoList.add(hi); } else { return null; } } return hostInfoList; } return null; } /** * Parses the host information using the alternate syntax "(key1=value1, key2=value2, ...)". * * @param user * the user to include in the resulting {@link HostInfo} * @param password * the password to include in the resulting {@link HostInfo} * @param hostInfo * the string containing the host information part * @return the {@link HostInfo} instance containing the parsed information or null if unable to parse the host information */ private HostInfo buildHostInfoResortingToKeyValueSyntaxParser(String user, String password, String hostInfo) { if (!hostInfo.startsWith(KEY_VALUE_HOST_INFO_OPENING_MARKER) || !hostInfo.endsWith(KEY_VALUE_HOST_INFO_CLOSING_MARKER)) { // This pattern won't work. return null; } hostInfo = hostInfo.substring(KEY_VALUE_HOST_INFO_OPENING_MARKER.length(), hostInfo.length() - KEY_VALUE_HOST_INFO_CLOSING_MARKER.length()); return new HostInfo(this, null, HostInfo.NO_PORT, user, password, processKeyValuePattern(KEY_VALUE_HOST_PTRN, hostInfo)); } /** * Parses the host information using the alternate syntax "address=(key1=value1)(key2=value2)...". * * @param user * the user to include in the resulting {@link HostInfo} * @param password * the password to include in the resulting {@link HostInfo} * @param hostInfo * the string containing the host information part * @return the {@link HostInfo} instance containing the parsed information or null if unable to parse the host information */ private HostInfo buildHostInfoResortingToAddressEqualsSyntaxParser(String user, String password, String hostInfo) { int p = StringUtils.indexOfIgnoreCase(hostInfo, ADDRESS_EQUALS_HOST_INFO_PREFIX); if (p != 0) { // This pattern won't work. return null; } hostInfo = hostInfo.substring(p + ADDRESS_EQUALS_HOST_INFO_PREFIX.length()).trim(); return new HostInfo(this, null, HostInfo.NO_PORT, user, password, processKeyValuePattern(ADDRESS_EQUALS_HOST_PTRN, hostInfo)); } /** * Parses the host information using the generic syntax "host:port". * * @param user * the user to include in the resulting {@link HostInfo} * @param password * the password to include in the resulting {@link HostInfo} * @param hostInfo * the string containing the host information part * @return the {@link HostInfo} instance containing the parsed information or null if unable to parse the host information */ private HostInfo buildHostInfoResortingToGenericSyntaxParser(String user, String password, String hostInfo) { if (splitByUserInfoAndHostInfo(hostInfo).left != null) { // This host information is invalid if contains another user information part. return null; } Pair hostPortPair = parseHostPortPair(hostInfo); String host = decode(safeTrim(hostPortPair.left)); Integer port = hostPortPair.right; return new HostInfo(this, isNullOrEmpty(host) ? null : host, port, user, password); } /** * Splits the given authority segment in the user information part and the host part. * * @param authSegment * the string containing the authority segment, i.e., the user and host information parts * @return * a {@link Pair} containing the user information in the left side and the host information in the right */ private Pair splitByUserInfoAndHostInfo(String authSegment) { String userInfoPart = null; String hostInfoPart = authSegment; int p = authSegment.indexOf(USER_HOST_SEPARATOR); if (p >= 0) { userInfoPart = authSegment.substring(0, p); hostInfoPart = authSegment.substring(p + USER_HOST_SEPARATOR.length()); } return new Pair<>(userInfoPart, hostInfoPart); } /** * Parses the given user information which is formed by the parts [user][:password]. * * @param userInfo * the string containing the user information * @return a {@link Pair} containing the user and password information or null if the user information can't be parsed */ public static Pair parseUserInfo(String userInfo) { if (isNullOrEmpty(userInfo)) { return null; } String[] userInfoParts = userInfo.split(USER_PASS_SEPARATOR, 2); String userName = userInfoParts[0]; String password = userInfoParts.length > 1 ? userInfoParts[1] : null; return new Pair<>(userName, password); } /** * Parses a host:port pair and returns the two elements in a {@link Pair} * * @param hostInfo * the host:pair to parse * @return a {@link Pair} containing the host and port information or null if the host information can't be parsed */ public static Pair parseHostPortPair(String hostInfo) { if (isNullOrEmpty(hostInfo)) { return null; } Matcher matcher = GENERIC_HOST_PTRN.matcher(hostInfo); if (matcher.matches()) { String host = matcher.group("host"); String portAsString = decode(safeTrim(matcher.group("port"))); Integer portAsInteger = HostInfo.NO_PORT; if (!isNullOrEmpty(portAsString)) { try { portAsInteger = Integer.parseInt(portAsString); } catch (NumberFormatException e) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.3", new Object[] { hostInfo }), e); } } return new Pair<>(host, portAsInteger); } throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.3", new Object[] { hostInfo })); } /** * Parses the connection properties section and stores the extracted key/value pairs into a local map. */ private void parseQuerySection() { if (isNullOrEmpty(this.query)) { this.parsedProperties = new HashMap<>(); return; } this.parsedProperties = processKeyValuePattern(PROPERTIES_PTRN, this.query); } /** * Takes a two-matching-groups (respectively named "key" and "value") pattern which is successively tested against the given string and produces a key/value * map with the matched values. The given pattern must ensure that there are no leftovers between successive tests, i.e., the end of the previous match must * coincide with the beginning of the next. * * @param pattern * the regular expression pattern to match against to * @param input * the input string * @return a key/value map containing the matched values */ private Map processKeyValuePattern(Pattern pattern, String input) { Matcher matcher = pattern.matcher(input); int p = 0; Map kvMap = new HashMap<>(); while (matcher.find()) { if (matcher.start() != p) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[] { input.substring(p) })); } String key = decode(safeTrim(matcher.group("key"))); String value = decode(safeTrim(matcher.group("value"))); if (!isNullOrEmpty(key)) { kvMap.put(key, value); } else if (!isNullOrEmpty(value)) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[] { input.substring(p) })); } p = matcher.end(); } if (p != input.length()) { throw ExceptionFactory.createException(WrongArgumentException.class, Messages.getString("ConnectionString.4", new Object[] { input.substring(p) })); } return kvMap; } /** * URL-decode the given string. * * @param text * the string to decode * @return * the decoded string */ private static String decode(String text) { if (isNullOrEmpty(text)) { return text; } try { return URLDecoder.decode(text, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { // Won't happen. } return ""; } /** * URL-decode the given string skipping all occurrences of the plus sign. * * @param text * the string to decode * @return * the decoded string */ private static String decodeSkippingPlusSign(String text) { if (isNullOrEmpty(text)) { return text; } text = text.replace("+", "%2B"); // Percent encode for "+" is "%2B". try { return URLDecoder.decode(text, StandardCharsets.UTF_8.name()); } catch (UnsupportedEncodingException e) { // Won't happen. } return ""; } /** * Returns the original database URL that produced this connection string parser. * * @return the original database URL */ @Override public String getDatabaseUrl() { return this.baseConnectionString; } /** * Returns the scheme section. * * @return the scheme section */ public String getScheme() { return this.scheme; } /** * Returns the authority section. * * @return the authority section */ public String getAuthority() { return this.authority; } /** * Returns the path section. * * @return the path section */ public String getPath() { return this.path; } /** * Returns the query section. * * @return the query section */ public String getQuery() { return this.query; } /** * Returns the hosts information. * * @return the hosts information */ public List getHosts() { if (this.parsedHosts == null) { this.parsedHosts = new ArrayList<>(); parseAuthoritySection(); } return this.parsedHosts; } /** * Returns the properties map contained in this connection string. * * @return the properties map */ public Map getProperties() { if (this.parsedProperties == null) { parseQuerySection(); } return Collections.unmodifiableMap(this.parsedProperties); } /** * Returns a string representation of this object. * * @return a string representation of this object */ @Override public String toString() { StringBuilder asStr = new StringBuilder(super.toString()); asStr.append(String.format(" :: {scheme: \"%s\", authority: \"%s\", path: \"%s\", query: \"%s\", parsedHosts: %s, parsedProperties: %s}", this.scheme, this.authority, this.path, this.query, this.parsedHosts, this.parsedProperties)); return asStr.toString(); } /** * This class is a simple container for two elements. * * @param * left part type * @param * right part type */ public static class Pair { public final T left; public final U right; public Pair(T left, U right) { this.left = left; this.right = right; } @Override public String toString() { StringBuilder asStr = new StringBuilder(super.toString()); asStr.append(String.format(" :: { left: %s, right: %s }", this.left, this.right)); return asStr.toString(); } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy