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

com.eventstore.dbclient.EventStoreDBConnectionString Maven / Gradle / Ivy

package com.eventstore.dbclient;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static java.lang.Integer.parseInt;
import static java.lang.Long.parseLong;

public class EventStoreDBConnectionString {
    private final Logger logger = LoggerFactory.getLogger(EventStoreDBConnectionString.class);
    private int position = 0;
    private int nextPosition = 0;
    private String connectionString;
    private final ConnectionSettingsBuilder settings = EventStoreDBClientSettings.builder();
    private final List notCurrentlySupported = Arrays.asList(
            "throwOnAppendFailure"
    );
    private HashMap LowerToKey = new HashMap(){
        {
            put("dnsdiscover", "dnsDiscover");
            put("maxdiscoverattempts", "maxDiscoverAttempts");
            put("discoveryinterval", "discoveryInterval");
            put("gossiptimeout", "gossipTimeout");
            put("nodepreference", "nodePreference");
            put("tls", "tls");
            put("tlsverifycert", "tlsVerifyCert");
            put("throwonappendfailure", "throwOnAppendFailure");
            put("keepalivetimeout", "keepAliveTimeout");
            put("keepaliveinterval", "keepAliveInterval");
            put("defaultdeadline", "defaultDeadline");
        }
    };

    public static EventStoreDBClientSettings parse(String connectionString) throws ParseError {
        return new EventStoreDBConnectionString().parseConnectionString(connectionString);
    }

    public static EventStoreDBClientSettings parseOrThrow(String connectionString) {
        try {
            return EventStoreDBConnectionString.parse(connectionString);
        } catch (ParseError e) {
            throw new RuntimeException(e);
        }
    }

    private String getRemaining() {
        return this.connectionString.substring(this.position);
    }

    private EventStoreDBClientSettings parseConnectionString(String connectionString) throws ParseError {
        this.connectionString = connectionString.trim().replaceAll("/+$", "");
        return this.parseProtocol();
    }

    private EventStoreDBClientSettings parseProtocol() throws ParseError {
        this.position = nextPosition;
        String expected = "esdb:// or esdb+discover://";
        String pattern = "^(?[^:]+)://";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(this.getRemaining());
        boolean found = m.find();

        if (found && !m.group("protocol").isEmpty()) {
            this.nextPosition += m.end();

            switch (m.group("protocol")) {
                case "esdb": {
                    this.settings.dnsDiscover(false);
                    return this.parseCredentials();
                }
                case "esdb+discover": {
                    this.settings.dnsDiscover(true);
                    return this.parseCredentials();
                }
            }
        }

        throw new ParseError(this.connectionString, this.position, this.nextPosition, expected);
    }

    private EventStoreDBClientSettings parseCredentials() throws ParseError {
        this.position = nextPosition;
        String expected = ":";
        String pattern = "^(?:(?[^:]+:[^@]+)@)";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(this.getRemaining());
        boolean found = m.find();

        // This is optional
        if (!found) {
            return this.parseHosts(true);
        }

        if (!m.group("credentials").isEmpty()) {
            try {
                this.nextPosition += m.end();
                String[] credentials = m.group("credentials").split(":");
                String username = URLDecoder.decode(credentials[0], "utf-8");
                String password = URLDecoder.decode(credentials[1], "utf-8");
                this.settings.defaultCredentials(username, password);
            } catch (UnsupportedEncodingException e) {
                throw new ParseError(this.connectionString, this.position, this.nextPosition, expected);
            }

            return this.parseHosts(true);
        }

        throw new ParseError(this.connectionString, this.position, this.nextPosition, expected);
    }

    private EventStoreDBClientSettings parseHosts(boolean mustMatch) throws ParseError {
        this.position = nextPosition;
        String expected = ":";
        String pattern = "^(?:(?[^$+!?*'(),;\\[\\]{}|\"%~#<>=&/]+)[,/]?)";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(this.getRemaining());
        boolean found = m.find();

        if (!found && mustMatch) {
            throw new ParseError(this.connectionString, this.position, this.nextPosition, expected);
        }

        if (found && !m.group("host").isEmpty()) {
            this.nextPosition += m.end();

            String[] hostParts = m.group("host").split(":");
            String address = hostParts[0];
            String rawPort = hostParts.length > 1 ? hostParts[1] : "2113";

            if (hostParts.length > 2) {
                throw new ParseError(
                        this.connectionString,
                        this.position + (address + ":" + rawPort).length(),
                        this.nextPosition,
                        ", or ?key=value");
            }

            try {
                int port = parseInt(rawPort);
                Endpoint host = new Endpoint(address, port);
                this.settings.addHost(host);
            } catch (NumberFormatException e) {
                throw new ParseError(
                        this.connectionString,
                        this.position + address.length(),
                        this.nextPosition,
                        "port number"
                );
            }

            return parseHosts(false);
        }

        return this.parseSearchParams(true);
    }

    public static Optional parseNodePreference(String value) {
        switch (value.toLowerCase()) {
            case "leader":
                return Optional.of(NodePreference.LEADER);
            case "follower":
                return Optional.of(NodePreference.FOLLOWER);
            case "readonlyreplica":
                return Optional.of(NodePreference.READ_ONLY_REPLICA);
            case "random":
                return Optional.of(NodePreference.RANDOM);
            default:
                return Optional.empty();
        }
    }

    private static final String[] NODE_PREFERENCE_VALUES = new String[] { "leader", "follower", "readonlyreplica","random" };

    private EventStoreDBClientSettings parseSearchParams(boolean first) throws ParseError {
        this.position = nextPosition;
        if (this.position == this.connectionString.length()) {
            return this.settings.buildConnectionSettings();
        }
        String expected = first ? "?key=value" : "&key=value";
        String pattern = "^(?:" + (first ?  "\\?" : "&") + "(?[^=]+)=(?[^&?]+))";
        Pattern r = Pattern.compile(pattern);
        Matcher m = r.matcher(this.getRemaining());
        boolean found = m.find();

        if (!found || m.group("key").isEmpty() || m.group("value").isEmpty()) {
            throw new ParseError(this.connectionString, this.position, this.nextPosition, expected);
        }

        this.nextPosition += m.end();

        String rawKey = m.group("key");
        String key = this.LowerToKey.getOrDefault(rawKey.toLowerCase(Locale.ROOT), rawKey);
        String value = m.group("value");
        int keyPosition = this.position + ("&" + key + "=").length();

        if (notCurrentlySupported.contains(key)) {
            logger.warn("{} is not currently supported by this client, and will have no effect.", key);
        }

        switch (key.toLowerCase()) {
            case "nodepreference": {
                Optional preference = parseNodePreference(value);
                if (preference.isPresent()) {
                    this.settings.nodePreference(preference.get());
                } else {
                    throw new ParseError(this.connectionString, keyPosition, this.nextPosition,
                            Arrays.toString(NODE_PREFERENCE_VALUES));
                }
                break;
            }
            case "maxdiscoverattempts": {
                try {
                    int parsedValue = parseInt(value);
                    this.settings.maxDiscoverAttempts(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
                break;
            }
            case "discoveryinterval": {
                try {
                    int parsedValue = parseInt(value);
                    this.settings.discoveryInterval(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
                break;
            }
            case "gossiptimeout": {
                try {
                    int parsedValue = parseInt(value);
                    this.settings.gossipTimeout(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
                break;
            }
            case "dnsdiscover": {
                if (!value.equals("true") && !value.equals("false")) {
                    throw new ParseError(this.connectionString, keyPosition, this.nextPosition, "true or false");
                }
                this.settings.dnsDiscover(value.equals("true"));
                break;
            }
            case "tls": {
                if (!value.equals("true") && !value.equals("false")) {
                    throw new ParseError(this.connectionString, keyPosition, this.nextPosition, "true or false");
                }
                this.settings.tls(value.equals("true"));
                break;
            }
            case "tlsverifycert": {
                if (!value.equals("true") && !value.equals("false")) {
                    throw new ParseError(this.connectionString, keyPosition, this.nextPosition, "true or false");
                }
                this.settings.tlsVerifyCert(value.equals("true"));
                break;
            }
            case "throwonappendfailure": {
                if (!value.equals("true") && !value.equals("false")) {
                    throw new ParseError(this.connectionString, keyPosition, this.nextPosition, "true or false");
                }
                this.settings.throwOnAppendFailure(value.equals("true"));
                break;
            }
            case "keepalivetimeout": {
                try {
                    long parsedValue = parseLong(value);
                    if (parsedValue >= 0 && parsedValue < Consts.DEFAULT_KEEP_ALIVE_TIMEOUT_IN_MS) {
                        logger.warn("Specified keepAliveTimeout of {} is less than recommended {}", parsedValue, Consts.DEFAULT_KEEP_ALIVE_TIMEOUT_IN_MS);
                    }

                    if (parsedValue < -1) {
                        logger.error("Invalid keepAliveTimeout of {}. Please provide a positive integer, or -1 to disable.", parsedValue);

                        throw new ParseError(
                                this.connectionString,
                                keyPosition,
                                this.nextPosition,
                                "positive integer"
                        );
                    }

                    if (parsedValue == -1)
                        parsedValue = Long.MAX_VALUE;

                    this.settings.keepAliveTimeout(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
                break;
            }
            case "keepaliveinterval": {
                try {
                    long parsedValue = parseLong(value);
                    if (parsedValue >= 0 && parsedValue < Consts.DEFAULT_KEEP_ALIVE_INTERVAL_IN_MS) {
                        logger.warn("Specified keepAliveInterval of {} is less than recommended {}", parsedValue, Consts.DEFAULT_KEEP_ALIVE_INTERVAL_IN_MS);
                    }

                    if (parsedValue < -1) {
                        logger.error("Invalid keepAliveInterval of {}. Please provide a positive integer, or -1 to disable.", parsedValue);

                        throw new ParseError(
                                this.connectionString,
                                keyPosition,
                                this.nextPosition,
                                "positive integer"
                        );
                    }

                    if (parsedValue == -1)
                        parsedValue = Long.MAX_VALUE;

                    this.settings.keepAliveInterval(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
                break;
            }
            case "defaultdeadline":
                try {
                    long parsedValue = parseLong(value);

                    if (parsedValue <= 0) {
                        logger.error("Invalid defaultDeadline of {}. Please provide a strictly positive integer", parsedValue);

                        throw new ParseError(
                                this.connectionString,
                                keyPosition,
                                this.nextPosition,
                                "positive integer"
                        );
                    }

                    this.settings.defaultDeadline(parsedValue);
                } catch (NumberFormatException e) {
                    throw new ParseError(
                            this.connectionString,
                            keyPosition,
                            this.nextPosition,
                            "integer"
                    );
                }
            default: {
                logger.warn("Unknown option {}, setting will be ignored.", key);
            }
        }


        return this.parseSearchParams(false);
    }


}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy