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

com.clickhouse.jdbc.internal.ClickHouseJdbcUrlParser Maven / Gradle / Ivy

The newest version!
package com.clickhouse.jdbc.internal;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.sql.SQLException;
import java.util.Locale;
import java.util.Properties;

import com.clickhouse.client.ClickHouseChecker;
import com.clickhouse.client.ClickHouseCredentials;
import com.clickhouse.client.ClickHouseFormat;
import com.clickhouse.client.ClickHouseNode;
import com.clickhouse.client.ClickHouseProtocol;
import com.clickhouse.client.ClickHouseUtils;
import com.clickhouse.client.config.ClickHouseClientOption;
import com.clickhouse.client.config.ClickHouseDefaults;
import com.clickhouse.client.logging.Logger;
import com.clickhouse.client.logging.LoggerFactory;
import com.clickhouse.jdbc.JdbcConfig;
import com.clickhouse.jdbc.SqlExceptionUtils;

public class ClickHouseJdbcUrlParser {
    private static final Logger log = LoggerFactory.getLogger(ClickHouseJdbcUrlParser.class);

    public static class ConnectionInfo {
        private final URI uri;
        private final ClickHouseNode server;
        private final JdbcConfig jdbcConf;
        private final Properties props;

        protected ConnectionInfo(URI uri, ClickHouseNode server, Properties props) throws URISyntaxException {
            this.uri = new URI("jdbc:clickhouse:" + server.getProtocol().name().toLowerCase(Locale.ROOT), null,
                    server.getHost(), server.getPort(), "/" + server.getDatabase().orElse(""),
                    removeCredentialsFromQuery(uri.getRawQuery()), null);
            this.server = server;
            this.jdbcConf = new JdbcConfig(props);
            this.props = props;
        }

        public URI getUri() {
            return uri;
        }

        public ClickHouseNode getServer() {
            return server;
        }

        public JdbcConfig getJdbcConfig() {
            return jdbcConf;
        }

        public Properties getProperties() {
            return props;
        }
    }

    // URL pattern:
    // jdbc:(clickhouse|ch)[:(grpc|http|tcp)]://host[:port][/db][?param1=value1¶m2=value2]
    public static final String JDBC_PREFIX = "jdbc:";
    public static final String JDBC_CLICKHOUSE_PREFIX = JDBC_PREFIX + "clickhouse:";
    public static final String JDBC_ABBREVIATION_PREFIX = JDBC_PREFIX + "ch:";

    private static String decode(String str) {
        try {
            return URLDecoder.decode(str, StandardCharsets.UTF_8.name());
        } catch (UnsupportedEncodingException e) {
            // don't print the content here as it may contain password
            log.warn("Failed to decode given string, fallback to the original string");
            return str;
        }
    }

    private static ClickHouseNode parseNode(URI uri, Properties defaults) {
        ClickHouseProtocol protocol = ClickHouseProtocol.fromUriScheme(uri.getScheme());
        if (protocol == null || protocol == ClickHouseProtocol.ANY) {
            throw new IllegalArgumentException("Unsupported scheme: " + uri.getScheme());
        }

        String host = uri.getHost();
        if (host == null || host.isEmpty()) {
            throw new IllegalArgumentException("Host is missed or wrong");
        }

        int port = uri.getPort();
        if (port == -1) {
            port = protocol.getDefaultPort();
        }

        String userName = defaults.getProperty(ClickHouseDefaults.USER.getKey());
        String password = defaults.getProperty(ClickHouseDefaults.PASSWORD.getKey());
        String userInfo = uri.getRawUserInfo();
        if (userInfo != null && !userInfo.isEmpty()) {
            int index = userInfo.indexOf(':');
            if (userName == null) {
                userName = index == 0 ? userName : decode(index > 0 ? userInfo.substring(0, index) : userInfo);
            }
            if (password == null) {
                password = index < 0 ? password : decode(userInfo.substring(index + 1));
            }
        }
        if (userName == null || userName.isEmpty()) {
            userName = (String) ClickHouseDefaults.USER.getEffectiveDefaultValue();
        }
        if (password == null) {
            password = (String) ClickHouseDefaults.PASSWORD.getEffectiveDefaultValue();
        }

        final String dbKey = ClickHouseDefaults.DATABASE.getKey();

        String path = uri.getPath();
        String database;
        if (path == null || path.isEmpty() || path.equals("/")) {
            database = defaults.getProperty(dbKey);
        } else if (path.charAt(0) == '/') {
            database = defaults.getProperty(dbKey, path.substring(1));
        } else {
            throw new IllegalArgumentException("wrong database name path: '" + path + "'");
        }
        if (database == null || database.isEmpty()) {
            database = (String) ClickHouseDefaults.DATABASE.getEffectiveDefaultValue();
        }

        defaults.setProperty(dbKey, database);

        return ClickHouseNode.builder().host(host).port(protocol, port).database(database)
                .credentials(ClickHouseCredentials.fromUserAndPassword(userName, password)).build();
    }

    private static Properties parseParams(String query, Properties props) {
        if (query == null || query.isEmpty()) {
            return props;
        }

        String[] queryKeyValues = query.split("&");
        for (String keyValue : queryKeyValues) {
            int index = keyValue.indexOf('=');
            if (index <= 0) {
                log.warn("don't know how to handle parameter pair: %s", keyValue);
                continue;
            }

            props.put(decode(keyValue.substring(0, index)), decode(keyValue.substring(index + 1)));
        }
        return props;
    }

    static String removeCredentialsFromQuery(String query) {
        if (query == null || query.isEmpty()) {
            return null;
        }

        StringBuilder builder = new StringBuilder(query.length());
        int start = 0;
        int index = 0;
        do {
            index = query.indexOf('&', start);
            String kv = query.substring(start, index < 0 ? query.length() : index);
            start += kv.length() + 1;
            int i = kv.indexOf('=');
            if (i > 0) {
                String key = kv.substring(0, i);
                if (!ClickHouseDefaults.USER.getKey().equals(key)
                        && !ClickHouseDefaults.PASSWORD.getKey().equals(key)) {
                    builder.append(kv).append('&');
                }
            }
        } while (index >= 0);

        if (builder.length() > 0) {
            builder.setLength(builder.length() - 1);
            query = builder.toString();
        } else {
            query = null;
        }

        return query;
    }

    static Properties newProperties() {
        Properties props = new Properties();
        props.setProperty(ClickHouseClientOption.ASYNC.getKey(), Boolean.FALSE.toString());
        props.setProperty(ClickHouseClientOption.FORMAT.getKey(), ClickHouseFormat.RowBinaryWithNamesAndTypes.name());
        return props;
    }

    public static ConnectionInfo parse(String jdbcUrl, Properties defaults) throws SQLException {
        if (defaults == null) {
            defaults = new Properties();
        }

        if (ClickHouseChecker.isNullOrBlank(jdbcUrl)) {
            throw SqlExceptionUtils.clientError("Non-blank JDBC URL is required");
        }

        if (jdbcUrl.startsWith(JDBC_CLICKHOUSE_PREFIX)) {
            jdbcUrl = jdbcUrl.substring(JDBC_CLICKHOUSE_PREFIX.length());
        } else if (jdbcUrl.startsWith(JDBC_ABBREVIATION_PREFIX)) {
            jdbcUrl = jdbcUrl.substring(JDBC_ABBREVIATION_PREFIX.length());
        } else {
            throw SqlExceptionUtils.clientError(
                    new URISyntaxException(jdbcUrl, ClickHouseUtils.format("'%s' or '%s' prefix is mandatory",
                            JDBC_CLICKHOUSE_PREFIX, JDBC_ABBREVIATION_PREFIX)));
        }

        int index = jdbcUrl.indexOf("//");
        if (index == -1) {
            throw SqlExceptionUtils
                    .clientError(new URISyntaxException(jdbcUrl, "Missing '//' from the given JDBC URL"));
        } else if (index == 0) {
            jdbcUrl = "http:" + jdbcUrl;
        }

        try {
            URI uri = new URI(jdbcUrl);
            Properties props = newProperties();
            props.putAll(defaults);
            parseParams(uri.getQuery(), props);
            return new ConnectionInfo(uri, parseNode(uri, props), props);
        } catch (URISyntaxException | IllegalArgumentException e) {
            throw SqlExceptionUtils.clientError(e);
        }
    }

    private ClickHouseJdbcUrlParser() {
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy