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

io.r2dbc.postgresql.PostgresqlConnectionConfiguration Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2017 the original author or authors.
 *
 * 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
 *
 *      https://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 io.r2dbc.postgresql;

import io.netty.handler.ssl.IdentityCipherSuiteFilter;
import io.netty.handler.ssl.OpenSsl;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
import io.r2dbc.postgresql.client.ConnectionSettings;
import io.r2dbc.postgresql.client.DefaultHostnameVerifier;
import io.r2dbc.postgresql.client.MultiHostConfiguration;
import io.r2dbc.postgresql.client.SSLConfig;
import io.r2dbc.postgresql.client.SSLMode;
import io.r2dbc.postgresql.client.SingleHostConfiguration;
import io.r2dbc.postgresql.codec.Codec;
import io.r2dbc.postgresql.codec.Codecs;
import io.r2dbc.postgresql.codec.Json;
import io.r2dbc.postgresql.extension.CodecRegistrar;
import io.r2dbc.postgresql.extension.Extension;
import io.r2dbc.postgresql.message.backend.ErrorResponse;
import io.r2dbc.postgresql.message.backend.NoticeResponse;
import io.r2dbc.postgresql.util.Assert;
import io.r2dbc.postgresql.util.LogLevel;
import org.reactivestreams.Publisher;
import reactor.core.publisher.Mono;
import reactor.netty.resources.LoopResources;
import reactor.util.annotation.Nullable;

import javax.net.ssl.HostnameVerifier;
import javax.net.ssl.SNIHostName;
import javax.net.ssl.SNIServerName;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLException;
import javax.net.ssl.SSLParameters;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.URL;
import java.time.Duration;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.TimeZone;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.function.ToIntFunction;

import static io.r2dbc.postgresql.message.frontend.Execute.NO_LIMIT;

/**
 * Connection configuration information for connecting to a PostgreSQL database.
 */
public final class PostgresqlConnectionConfiguration {

    /**
     * Default PostgreSQL port.
     */
    public static final int DEFAULT_PORT = 5432;

    private final String applicationName;

    private final boolean autodetectExtensions;

    private final boolean compatibilityMode;

    @Nullable
    private final Duration connectTimeout;

    private final String database;

    private final LogLevel errorResponseLogLevel;

    private final List extensions;

    private final ToIntFunction fetchSize;

    private final boolean forceBinary;

    @Nullable
    private final Duration lockWaitTimeout;

    @Nullable
    private final LoopResources loopResources;

    @Nullable
    private final MultiHostConfiguration multiHostConfiguration;

    private final LogLevel noticeLogLevel;

    private final Map options;

    @Nullable
    private final Publisher password;

    private final boolean preferAttachedBuffers;

    private final int preparedStatementCacheQueries;

    @Nullable
    private final SingleHostConfiguration singleHostConfiguration;

    @Nullable
    private final Duration statementTimeout;

    private final SSLConfig sslConfig;

    private final boolean tcpKeepAlive;

    private final boolean tcpNoDelay;

    private final TimeZone timeZone;

    private final Publisher username;

    private PostgresqlConnectionConfiguration(String applicationName, boolean autodetectExtensions, @Nullable boolean compatibilityMode, @Nullable Duration connectTimeout, @Nullable String database
        , LogLevel errorResponseLogLevel, List extensions, ToIntFunction fetchSize, boolean forceBinary, @Nullable Duration lockWaitTimeout,
                                              @Nullable LoopResources loopResources, @Nullable MultiHostConfiguration multiHostConfiguration, LogLevel noticeLogLevel,
                                              @Nullable Map options, @Nullable Publisher password, boolean preferAttachedBuffers, int preparedStatementCacheQueries,
                                              @Nullable String schema, @Nullable SingleHostConfiguration singleHostConfiguration, SSLConfig sslConfig, @Nullable Duration statementTimeout,
                                              boolean tcpKeepAlive, boolean tcpNoDelay, TimeZone timeZone, Publisher username) {
        this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null");
        this.autodetectExtensions = autodetectExtensions;
        this.compatibilityMode = compatibilityMode;
        this.connectTimeout = connectTimeout;
        this.errorResponseLogLevel = errorResponseLogLevel;
        this.extensions = Assert.requireNonNull(extensions, "extensions must not be null");
        this.database = database;
        this.fetchSize = fetchSize;
        this.forceBinary = forceBinary;
        this.loopResources = loopResources;
        this.multiHostConfiguration = multiHostConfiguration;
        this.noticeLogLevel = noticeLogLevel;
        this.options = options == null ? new LinkedHashMap<>() : new LinkedHashMap<>(options);
        this.statementTimeout = statementTimeout;
        this.lockWaitTimeout = lockWaitTimeout;

        if (this.statementTimeout != null) {
            this.options.put("statement_timeout", Long.toString(statementTimeout.toMillis()));
        }

        if (this.lockWaitTimeout != null) {
            this.options.put("lock_timeout", Long.toString(lockWaitTimeout.toMillis()));
        }

        if (schema != null && !schema.isEmpty()) {
            this.options.put("search_path", schema);
        }

        this.password = password;
        this.preferAttachedBuffers = preferAttachedBuffers;
        this.preparedStatementCacheQueries = preparedStatementCacheQueries;
        this.singleHostConfiguration = singleHostConfiguration;
        this.sslConfig = sslConfig;
        this.tcpKeepAlive = tcpKeepAlive;
        this.tcpNoDelay = tcpNoDelay;
        this.timeZone = timeZone;
        this.username = Assert.requireNonNull(username, "username must not be null");
    }

    /**
     * Returns a new {@link Builder}.
     *
     * @return a new {@link Builder}
     */
    public static Builder builder() {
        return new Builder();
    }

    @Override
    public String toString() {
        return "PostgresqlConnectionConfiguration{" +
            "applicationName='" + this.applicationName + '\'' +
            ", autodetectExtensions='" + this.autodetectExtensions + '\'' +
            ", compatibilityMode=" + this.compatibilityMode +
            ", connectTimeout=" + this.connectTimeout +
            ", errorResponseLogLevel=" + this.errorResponseLogLevel +
            ", database='" + this.database + '\'' +
            ", extensions=" + this.extensions +
            ", fetchSize=" + this.fetchSize +
            ", forceBinary='" + this.forceBinary + '\'' +
            ", lockWaitTimeout='" + this.lockWaitTimeout +
            ", loopResources='" + this.loopResources + '\'' +
            ", multiHostConfiguration='" + this.multiHostConfiguration + '\'' +
            ", noticeLogLevel='" + this.noticeLogLevel + '\'' +
            ", options='" + this.options + '\'' +
            ", password='" + obfuscate(this.password != null ? 4 : 0) + '\'' +
            ", preferAttachedBuffers=" + this.preferAttachedBuffers +
            ", singleHostConfiguration=" + this.singleHostConfiguration +
            ", statementTimeout=" + this.statementTimeout +
            ", tcpKeepAlive=" + this.tcpKeepAlive +
            ", tcpNoDelay=" + this.tcpNoDelay +
            ", timeZone=" + this.timeZone +
            ", username='" + this.username + '\'' +
            '}';
    }

    String getApplicationName() {
        return this.applicationName;
    }

    boolean isCompatibilityMode() {
        return this.compatibilityMode;
    }

    @Nullable
    Duration getConnectTimeout() {
        return this.connectTimeout;
    }

    @Nullable
    String getDatabase() {
        return this.database;
    }

    List getExtensions() {
        return this.extensions;
    }

    ToIntFunction getFetchSize() {
        return this.fetchSize;
    }

    int getFetchSize(String sql) {
        return this.fetchSize.applyAsInt(sql);
    }

    @Nullable
    MultiHostConfiguration getMultiHostConfiguration() {
        return this.multiHostConfiguration;
    }

    MultiHostConfiguration getRequiredMultiHostConfiguration() {

        MultiHostConfiguration config = getMultiHostConfiguration();

        if (config == null) {
            throw new IllegalStateException("MultiHostConfiguration not configured");
        }

        return config;
    }

    Map getOptions() {
        return Collections.unmodifiableMap(this.options);
    }

    Publisher getPassword() {
        return this.password == null ? Mono.empty() : this.password;
    }

    boolean isPreferAttachedBuffers() {
        return this.preferAttachedBuffers;
    }

    int getPreparedStatementCacheQueries() {
        return this.preparedStatementCacheQueries;
    }

    @Nullable
    SingleHostConfiguration getSingleHostConfiguration() {
        return this.singleHostConfiguration;
    }

    SingleHostConfiguration getRequiredSingleHostConfiguration() {

        SingleHostConfiguration config = getSingleHostConfiguration();

        if (config == null) {
            throw new IllegalStateException("SingleHostConfiguration not configured");
        }

        return config;
    }

    Publisher getUsername() {
        return this.username;
    }

    boolean isAutodetectExtensions() {
        return this.autodetectExtensions;
    }

    boolean isForceBinary() {
        return this.forceBinary;
    }

    boolean isTcpKeepAlive() {
        return this.tcpKeepAlive;
    }

    boolean isTcpNoDelay() {
        return this.tcpNoDelay;
    }

    TimeZone getTimeZone() {
        return this.timeZone;
    }

    SSLConfig getSslConfig() {
        return this.sslConfig;
    }

    ConnectionSettings getConnectionSettings() {
        return ConnectionSettings.builder()
            .connectTimeout(getConnectTimeout())
            .errorResponseLogLevel(this.errorResponseLogLevel)
            .noticeLogLevel(this.noticeLogLevel)
            .sslConfig(getSslConfig())
            .startupOptions(this.options)
            .tcpKeepAlive(isTcpKeepAlive())
            .tcpNoDelay(isTcpNoDelay())
            .loopResources(this.loopResources)
            .build();
    }

    private static String obfuscate(int length) {

        StringBuilder builder = new StringBuilder();

        for (int i = 0; i < length; i++) {
            builder.append("*");
        }

        return builder.toString();
    }

    /**
     * A builder for {@link PostgresqlConnectionConfiguration} instances.
     * 

* This class is not threadsafe */ public static final class Builder { private String applicationName = "r2dbc-postgresql"; private boolean autodetectExtensions = true; private boolean compatibilityMode = false; @Nullable private Duration connectTimeout; @Nullable private String database; private LogLevel errorResponseLogLevel = LogLevel.DEBUG; private final List extensions = new ArrayList<>(); private ToIntFunction fetchSize = sql -> NO_LIMIT; private boolean forceBinary = false; @Nullable private Duration lockWaitTimeout; @Nullable private MultiHostConfiguration.Builder multiHostConfiguration; private LogLevel noticeLogLevel = LogLevel.DEBUG; private Map options; @Nullable private Publisher password; private boolean preferAttachedBuffers = false; private int preparedStatementCacheQueries = -1; @Nullable private String schema; @Nullable private SingleHostConfiguration.Builder singleHostConfiguration; @Nullable private URL sslCert = null; private HostnameVerifier sslHostnameVerifier = DefaultHostnameVerifier.INSTANCE; @Nullable private URL sslKey = null; private SSLMode sslMode = SSLMode.DISABLE; @Nullable private CharSequence sslPassword = null; @Nullable private URL sslRootCert = null; @Nullable private Duration statementTimeout = null; private Function sslContextBuilderCustomizer = Function.identity(); private Function sslEngineCustomizer = Function.identity(); private Function sslParametersFactory = it -> new SSLParameters(); private boolean sslSni = true; private boolean tcpKeepAlive = false; private boolean tcpNoDelay = true; private TimeZone timeZone = TimeZone.getDefault(); @Nullable private LoopResources loopResources = null; @Nullable private Publisher username; private Builder() { } /** * Configure the application name. Defaults to {@code postgresql-r2dbc}. * * @param applicationName the application name * @return this {@link Builder} * @throws IllegalArgumentException if {@code applicationName} is {@code null} */ public Builder applicationName(String applicationName) { this.applicationName = Assert.requireNonNull(applicationName, "applicationName must not be null"); return this; } /** * Configures whether to use {@link ServiceLoader} to discover and register extensions. Defaults to true. * * @param autodetectExtensions to discover and register extensions * @return this {@link Builder} */ public Builder autodetectExtensions(boolean autodetectExtensions) { this.autodetectExtensions = autodetectExtensions; return this; } /** * Returns a configured {@link PostgresqlConnectionConfiguration}. * * @return a configured {@link PostgresqlConnectionConfiguration} */ public PostgresqlConnectionConfiguration build() { SingleHostConfiguration singleHostConfiguration = this.singleHostConfiguration != null ? this.singleHostConfiguration.build() : null; MultiHostConfiguration multiHostConfiguration = this.multiHostConfiguration != null ? this.multiHostConfiguration.build() : null; if ((singleHostConfiguration == null) == (multiHostConfiguration == null)) { throw new IllegalArgumentException("Connection must be configured for either multi-host or single host connectivity"); } if (this.username == null) { throw new IllegalArgumentException("username must not be null"); } return new PostgresqlConnectionConfiguration(this.applicationName, this.autodetectExtensions, this.compatibilityMode, this.connectTimeout, this.database, this.errorResponseLogLevel, this.extensions, this.fetchSize, this.forceBinary, this.lockWaitTimeout, this.loopResources, multiHostConfiguration, this.noticeLogLevel, this.options, this.password, this.preferAttachedBuffers, this.preparedStatementCacheQueries, this.schema, singleHostConfiguration, this.createSslConfig(this.sslSni), this.statementTimeout, this.tcpKeepAlive, this.tcpNoDelay, this.timeZone, this.username); } /** * Enables protocol compatibility mode for cursored query execution. Cursored query execution applies when configuring a non-zero fetch size. Compatibility mode uses {@code Execute+Sync} * messages instead of {@code Execute+Flush}. The default mode uses optimized fetching which does not work with newer pgpool versions. * * @param compatibilityMode whether to enable compatibility mode * @return this {@link Builder} * @since 0.8.7 */ public Builder compatibilityMode(boolean compatibilityMode) { this.compatibilityMode = compatibilityMode; return this; } /** * Configure the connection timeout. Default unconfigured. * * @param connectTimeout the connection timeout * @return this {@link Builder} */ public Builder connectTimeout(@Nullable Duration connectTimeout) { this.connectTimeout = connectTimeout; return this; } /** * Register a {@link CodecRegistrar} that can contribute extension {@link Codec}s. * Calling this method adds a {@link CodecRegistrar} and does not replace existing {@link Extension}s. * * @param codecRegistrar registrar to contribute codecs * @return this {@link Builder} */ public Builder codecRegistrar(CodecRegistrar codecRegistrar) { return extendWith(codecRegistrar); } /** * Configure the database. * * @param database the database * @return this {@link Builder} */ public Builder database(@Nullable String database) { this.database = database; return this; } /** * Enable SSL usage. This flag is also known as Use Encryption in other drivers. * * @return this {@link Builder} */ public Builder enableSsl() { return sslMode(SSLMode.VERIFY_FULL); } /** * Registers a {@link Extension} to extend driver functionality. * Calling this method adds a {@link Extension} and does not replace existing {@link Extension}s. * * @param extension extension to extend driver functionality * @return this {@link Builder} */ public Builder extendWith(Extension extension) { this.extensions.add(Assert.requireNonNull(extension, "extension must not be null")); return this; } /** * Configure the {@link LogLevel} for {@link ErrorResponse error responses} that are part of a statement execution. * * @param errorResponseLogLevel the log level to use. * @return this {@link Builder} * @since 0.9 */ public Builder errorResponseLogLevel(LogLevel errorResponseLogLevel) { this.errorResponseLogLevel = Assert.requireNonNull(errorResponseLogLevel, "errorResponseLogLevel must not be null"); return this; } /** * Set the default number of rows to return when fetching results from a query. If the value specified is zero, then the hint is ignored and queries request all rows when running a statement. * * @param fetchSize the number of rows to fetch * @return this {@code Builder} * @throws IllegalArgumentException if {@code fetchSize} is {@code negative} * @since 0.9 */ public Builder fetchSize(int fetchSize) { Assert.isTrue(fetchSize >= 0, "fetch size must be greater or equal zero"); return fetchSize(new FixedFetchSize(fetchSize)); } /** * Set a function that maps a SQL query to the number of rows to return when fetching results for that query. * * @param fetchSizeFunction a function that maps the number of rows to fetch * @return this {@code Builder} * @throws IllegalArgumentException if {@code fetchSizeFunction} is {@code null} * @since 0.9 */ public Builder fetchSize(ToIntFunction fetchSizeFunction) { Assert.requireNonNull(fetchSizeFunction, "fetch size function must be non null"); this.fetchSize = fetchSizeFunction; return this; } /** * Force binary results (Binary Transfer). Defaults to false. * * @param forceBinary whether to force binary transfer * @return this {@link Builder} */ public Builder forceBinary(boolean forceBinary) { this.forceBinary = forceBinary; return this; } /** * Configure the host. Calling this method prepares single-node configuration. This method can be only used if the builder was not configured with a multi-host configuration. * * @param host the host * @return this {@link Builder} * @throws IllegalArgumentException if {@code host} is {@code null} * @throws IllegalStateException if {@link #addHost}, {@link #targetServerType(MultiHostConnectionStrategy.TargetServerType)} or {@link #hostRecheckTime(Duration)} was configured earlier * to ensure a consistent configuration state */ public Builder host(String host) { Assert.requireNonNull(host, "host must not be null"); prepareSingleHostConfiguration().host(host); return this; } /** * Add host with default port to the hosts list. Calling this method prepares multi-node configuration. This method can be only used if the builder was not configured with a single-host * configuration. * * @param host the host * @return this {@link Builder} * @throws IllegalArgumentException if {@code host} is {@code null} * @throws IllegalStateException if {@link #port(int)}, {@link #host(String)} or {@link #socket(String)} was configured earlier to ensure a consistent configuration state * @since 1.0 */ public Builder addHost(String host) { Assert.requireNonNull(host, "host must not be null"); prepareMultiHostConfiguration().addHost(host); return this; } /** * Add host to the hosts list. Calling this method prepares multi-node configuration. This method can be only used if the builder was not configured with a single-host configuration. * * @param host the host * @param port the port * @return this {@link Builder} * @throws IllegalArgumentException if {@code host} is {@code null} * @throws IllegalStateException if {@link #port(int)}, {@link #host(String)} or {@link #socket(String)} was configured earlier to ensure a consistent configuration state * @since 1.0 */ public Builder addHost(String host, int port) { Assert.requireNonNull(host, "host must not be null"); prepareMultiHostConfiguration().addHost(host, port); return this; } /** * Controls how long the knowledge about a host state is cached connection factory. The default value is {@code 10 seconds}. Calling this method prepares multi-node configuration. This * method can be only used if the builder was not configured with a single-host configuration. * * @param hostRecheckTime host recheck time * @return this {@link Builder} * @throws IllegalStateException if {@link #port(int)}, {@link #host(String)} or {@link #socket(String)} was configured earlier to ensure a consistent configuration state * @since 1.0 */ public Builder hostRecheckTime(Duration hostRecheckTime) { prepareMultiHostConfiguration().hostRecheckTime(hostRecheckTime); return this; } /** * In default mode (disabled) hosts are connected in the given order. If enabled hosts are chosen randomly from the set of suitable candidates. Calling this method prepares multi-node * configuration. This method can be only used if the builder was not configured with a single-host configuration. * * @param loadBalanceHosts is load balance mode enabled * @return this {@link Builder} * @throws IllegalStateException if {@link #port(int)}, {@link #host(String)} or {@link #socket(String)} was configured earlier to ensure a consistent configuration state * @since 1.0 */ public Builder loadBalanceHosts(boolean loadBalanceHosts) { prepareMultiHostConfiguration().loadBalanceHosts(loadBalanceHosts); return this; } /** * Configure the Lock wait timeout. Default unconfigured. *

* This parameter is applied once after creating a new connection. * If lockTimeout is already set using {@link #options(Map)}, it will be overridden. * Lock Timeout * * @param lockWaitTimeout the lock timeout * @return this {@link Builder} * @since 0.8.9 */ public Builder lockWaitTimeout(Duration lockWaitTimeout) { this.lockWaitTimeout = Assert.requireNonNull(lockWaitTimeout, "Lock wait timeout must not be null"); return this; } /** * Configure {@link LoopResources}. * * @param loopResources the {@link LoopResources} * @return this {@link Builder} * @since 0.8.5 */ public Builder loopResources(LoopResources loopResources) { this.loopResources = Assert.requireNonNull(loopResources, "loopResources must not be null"); return this; } /** * Configure the {@link LogLevel} for {@link NoticeResponse notice responses}. * * @param noticeLogLevel the log level to use. * @return this {@link Builder} * @since 0.9 */ public Builder noticeLogLevel(LogLevel noticeLogLevel) { this.noticeLogLevel = Assert.requireNonNull(noticeLogLevel, "noticeLogLevel must not be null"); return this; } /** * Configure connection initialization parameters. *

* These parameters are applied once after creating a new connection. This is useful for setting up client-specific * runtime parameters * like statement timeouts, time zones etc. * * @param options the options * @return this {@link Builder} * @throws IllegalArgumentException if {@code options} or any key or value of {@code options} is {@code null} */ public Builder options(Map options) { Assert.requireNonNull(options, "options map must not be null"); options.forEach((k, v) -> { Assert.requireNonNull(k, "option keys must not be null"); Assert.requireNonNull(v, "option values must not be null"); }); this.options = options; return this; } /** * Configure the password. * * @param password the password * @return this {@link Builder} */ public Builder password(@Nullable CharSequence password) { this.password = Mono.justOrEmpty(password); return this; } /** * Configure the password publisher. The publisher is used on each authentication attempt. * * @param password the password * @return this {@link Builder} * @since 1.0.3 */ public Builder password(Publisher password) { this.password = Mono.from(password); return this; } /** * Configure the password supplier. The supplier is used on each authentication attempt. * * @param password the password * @return this {@link Builder} * @since 1.0.3 */ public Builder password(Supplier password) { this.password = Mono.fromSupplier(password); return this; } /** * Configure the port. Defaults to {@code 5432}. Calling this method prepares single-node configuration. This method can be only used if the builder was not configured with a multi-host * configuration. * * @param port the port * @return this {@link Builder} * @throws IllegalStateException if {@link #addHost}, {@link #targetServerType(MultiHostConnectionStrategy.TargetServerType)} or {@link #hostRecheckTime(Duration)} was configured earlier to * ensure a consistent configuration state */ public Builder port(int port) { prepareSingleHostConfiguration().port(port); return this; } /** * Configure whether {@link Codecs codecs} should prefer attached data buffers. The default is {@code false}, meaning that codecs will copy data from the input buffer into a {@code byte[]} * or similar data structure that is enabled for garbage collection. Using attached buffers is more efficient but comes with the requirement that decoded values (such as {@link Json}) must * be consumed to release attached buffers to avoid memory leaks. * * @param preferAttachedBuffers the flag whether to prefer attached buffers * @return this {@link Builder} * @since 0.8.5 */ public Builder preferAttachedBuffers(boolean preferAttachedBuffers) { this.preferAttachedBuffers = preferAttachedBuffers; return this; } /** * Configure the preparedStatementCacheQueries. The default is {@code -1}, meaning there's no limit. The value of {@code 0} disables the cache. Any other value specifies the cache size. * * @param preparedStatementCacheQueries the preparedStatementCacheQueries * @return this {@link Builder} * @since 0.8.1 */ public Builder preparedStatementCacheQueries(int preparedStatementCacheQueries) { this.preparedStatementCacheQueries = preparedStatementCacheQueries; return this; } /** * Configure the schema. * * @param schema the schema * @return this {@link Builder} */ public Builder schema(@Nullable String schema) { this.schema = schema; return this; } /** * Configure the unix domain socket to connect to. Calling this method prepares single-node configuration. This method can be only used if the builder was not configured with a multi-host * configuration. * * @param socket the socket path * @return this {@link Builder} * @throws IllegalArgumentException if {@code socket} is {@code null} * @throws IllegalStateException if {@link #addHost}, {@link #targetServerType(MultiHostConnectionStrategy.TargetServerType)} or {@link #hostRecheckTime(Duration)} was configured earlier * to ensure a consistent configuration state */ public Builder socket(String socket) { Assert.requireNonNull(socket, "host must not be null"); prepareSingleHostConfiguration().socket(socket); sslMode(SSLMode.DISABLE); return this; } /** * Configure a {@link SslContextBuilder} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * called with the prepared {@link SslContextBuilder} that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to * build the SSL context. * * @param sslContextBuilderCustomizer customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslContextBuilderCustomizer} is {@code null} */ public Builder sslContextBuilderCustomizer(Function sslContextBuilderCustomizer) { this.sslContextBuilderCustomizer = Assert.requireNonNull(sslContextBuilderCustomizer, "sslContextBuilderCustomizer must not be null"); return this; } /** * Configure a {@link SSLEngine} customizer. The customizer gets applied on each SSL connection attempt to allow for just-in-time configuration updates. The {@link Function} gets * called with a {@link SSLEngine} instance that has all configuration options applied. The customizer may return the same builder or return a new builder instance to be used to * build the SSL context. * * @param sslEngineCustomizer customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslEngineCustomizer} is {@code null} */ public Builder sslEngineCustomizer(Function sslEngineCustomizer) { this.sslEngineCustomizer = Assert.requireNonNull(sslEngineCustomizer, "sslEngineCustomizer must not be null"); return this; } /** * Configure a {@link SSLParameters} provider for a given {@link SocketAddress}. The provider gets applied on each SSL connection attempt to allow for just-in-time configuration updates. * Typically used to configure SSL protocols * * @param sslParametersFactory customizer function * @return this {@link Builder} * @throws IllegalArgumentException if {@code sslParametersFactory} is {@code null} * @since 1.0.5 */ public Builder sslParameters(Function sslParametersFactory) { this.sslParametersFactory = Assert.requireNonNull(sslParametersFactory, "sslParametersFactory must not be null"); return this; } /** * Configure whether to indicate the hostname and port via SNI to the server. Enabled by default. * * @param sslSni whether to indicate the hostname and port via SNI. Sets {@link SSLParameters#setServerNames(List)} on the {@link SSLParameters} instance provided by * {@link #sslParameters(Function)}. * @return this {@link Builder} * @since 1.0.5 */ public Builder sslSni(boolean sslSni) { this.sslSni = sslSni; return this; } /** * Configure ssl cert for client certificate authentication. Can point to either a resource within the classpath or a file. * * @param sslCert an X.509 certificate chain file in PEM format * @return this {@link Builder} */ public Builder sslCert(String sslCert) { return sslCert(requireExistingFilePath(sslCert, "sslCert must not be null and must exist")); } /** * Configure ssl cert for client certificate authentication. * * @param sslCert an X.509 certificate chain file in PEM format * @return this {@link Builder} * @since 0.8.7 */ public Builder sslCert(URL sslCert) { this.sslCert = Assert.requireNonNull(sslCert, "sslCert must not be null"); return this; } /** * Configure ssl HostnameVerifier. * * @param sslHostnameVerifier {@link HostnameVerifier} * @return this {@link Builder} */ public Builder sslHostnameVerifier(HostnameVerifier sslHostnameVerifier) { this.sslHostnameVerifier = Assert.requireNonNull(sslHostnameVerifier, "sslHostnameVerifier must not be null"); return this; } /** * Configure ssl key for client certificate authentication. Can point to either a resource within the classpath or a file. * * @param sslKey a PKCS#8 private key file in PEM format * @return this {@link Builder} */ public Builder sslKey(String sslKey) { return sslKey(requireExistingFilePath(sslKey, "sslKey must not be null and must exist")); } /** * Configure ssl key for client certificate authentication. * * @param sslKey a PKCS#8 private key file in PEM format * @return this {@link Builder} * @since 0.8.7 */ public Builder sslKey(URL sslKey) { this.sslKey = Assert.requireNonNull(sslKey, "sslKey must not be null"); return this; } /** * Configure ssl mode. * * @param sslMode the SSL mode to use. * @return this {@link Builder} */ public Builder sslMode(SSLMode sslMode) { this.sslMode = Assert.requireNonNull(sslMode, "sslMode must be not be null"); return this; } /** * Configure ssl password. * * @param sslPassword the password of the sslKey, or null if it's not password-protected * @return this {@link Builder} */ public Builder sslPassword(@Nullable CharSequence sslPassword) { this.sslPassword = sslPassword; return this; } /** * Configure ssl root cert for server certificate validation. Can point to either a resource within the classpath or a file. * * @param sslRootCert an X.509 certificate chain file in PEM format * @return this {@link Builder} */ public Builder sslRootCert(String sslRootCert) { return sslRootCert(requireExistingFilePath(sslRootCert, "sslRootCert must not be null and must exist")); } /** * Configure ssl root cert for server certificate validation. * * @param sslRootCert an X.509 certificate chain file in PEM format * @return this {@link Builder} * @since 0.8.7 */ public Builder sslRootCert(URL sslRootCert) { this.sslRootCert = Assert.requireNonNull(sslRootCert, "sslRootCert must not be null and must exist"); return this; } /** * Configure the Statement timeout. Default unconfigured. *

* This parameter is applied once after creating a new connection. * If statementTimeout is already set using {@link #options(Map)}, it will be overridden. * Statement Timeout * * @param statementTimeout the statement timeout * @return this {@link Builder} * @since 0.8.9 */ public Builder statementTimeout(Duration statementTimeout) { this.statementTimeout = Assert.requireNonNull(statementTimeout, "Statement timeout"); return this; } /** * Allows opening connections to only servers with required state. * The primary/secondary distinction is currently done by observing if the server allows writes. * The value {@link MultiHostConnectionStrategy.TargetServerType#PREFER_SECONDARY} tries to connect to a secondary if any are available, otherwise allows falls back to a primary. * Default value is {@link MultiHostConnectionStrategy.TargetServerType#ANY}. * * @param targetServerType target server type * @return this {@link Builder} * @throws IllegalArgumentException if {@code targetServerType} is {@code null} * @since 1.0 */ public Builder targetServerType(MultiHostConnectionStrategy.TargetServerType targetServerType) { prepareMultiHostConfiguration().targetServerType(targetServerType); return this; } /** * Configure TCP KeepAlive. * * @param enabled whether to enable TCP KeepAlive * @return this {@link Builder} * @see Socket#setKeepAlive(boolean) * @since 0.8.4 */ public Builder tcpKeepAlive(boolean enabled) { this.tcpKeepAlive = enabled; return this; } /** * Configure TCP NoDelay. * * @param enabled whether to enable TCP NoDelay * @return this {@link Builder} * @see Socket#setTcpNoDelay(boolean) * @since 0.8.4 */ public Builder tcpNoDelay(boolean enabled) { this.tcpNoDelay = enabled; return this; } /** * Configure the session timezone. * * @param timeZone the timeZone identifier * @return this {@link Builder} * @throws IllegalArgumentException if {@code timeZone} is empty or {@code null} * @see TimeZone#getTimeZone(String) * @since 1.0 */ public Builder timeZone(String timeZone) { return timeZone(TimeZone.getTimeZone(Assert.requireNotEmpty(timeZone, "timeZone must not be empty"))); } /** * Configure the session timezone. * * @param timeZone the timeZone identifier * @return this {@link Builder} * @throws IllegalArgumentException if {@code timeZone} is {@code null} * @see TimeZone#getTimeZone(String) * @since 1.0 */ public Builder timeZone(TimeZone timeZone) { this.timeZone = Assert.requireNonNull(timeZone, "timeZone must not be null"); return this; } /** * Configure the username. * * @param username the username * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} */ public Builder username(String username) { this.username = Mono.just(Assert.requireNonNull(username, "username must not be null")); return this; } /** * Configure the username publisher. The publisher is used on each authentication attempt. * * @param username the username * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} */ public Builder username(Publisher username) { this.username = Assert.requireNonNull(username, "username must not be null"); return this; } /** * Configure the username supplier. The supplier is used on each authentication attempt. * * @param username the username * @return this {@link Builder} * @throws IllegalArgumentException if {@code username} is {@code null} */ public Builder username(Supplier username) { this.username = Mono.fromSupplier(Assert.requireNonNull(username, "username must not be null")); return this; } @Override public String toString() { return "Builder{" + "applicationName='" + this.applicationName + '\'' + ", autodetectExtensions='" + this.autodetectExtensions + '\'' + ", compatibilityMode='" + this.compatibilityMode + '\'' + ", connectTimeout='" + this.connectTimeout + '\'' + ", database='" + this.database + '\'' + ", extensions='" + this.extensions + '\'' + ", errorResponseLogLevel='" + this.errorResponseLogLevel + '\'' + ", fetchSize='" + this.fetchSize + '\'' + ", forceBinary='" + this.forceBinary + '\'' + ", lockWaitTimeout='" + this.lockWaitTimeout + '\'' + ", loopResources='" + this.loopResources + '\'' + ", multiHostConfiguration='" + this.multiHostConfiguration + '\'' + ", noticeLogLevel='" + this.noticeLogLevel + '\'' + ", parameters='" + this.options + '\'' + ", password='" + obfuscate(this.password != null ? 4 : 0) + '\'' + ", preparedStatementCacheQueries='" + this.preparedStatementCacheQueries + '\'' + ", schema='" + this.schema + '\'' + ", singleHostConfiguration='" + this.singleHostConfiguration + '\'' + ", sslContextBuilderCustomizer='" + this.sslContextBuilderCustomizer + '\'' + ", sslEngineCustomizer='" + this.sslEngineCustomizer + '\'' + ", sslParametersFactory='" + this.sslParametersFactory + '\'' + ", sslMode='" + this.sslMode + '\'' + ", sslRootCert='" + this.sslRootCert + '\'' + ", sslCert='" + this.sslCert + '\'' + ", sslKey='" + this.sslKey + '\'' + ", sslSni='" + this.sslSni + '\'' + ", statementTimeout='" + this.statementTimeout + '\'' + ", sslHostnameVerifier='" + this.sslHostnameVerifier + '\'' + ", tcpKeepAlive='" + this.tcpKeepAlive + '\'' + ", tcpNoDelay='" + this.tcpNoDelay + '\'' + ", timeZone='" + this.timeZone + '\'' + ", username='" + this.username + '\'' + '}'; } private SSLConfig createSslConfig(boolean sslSni) { if (this.singleHostConfiguration != null && this.singleHostConfiguration.getSocket() != null || this.sslMode == SSLMode.DISABLE) { return SSLConfig.disabled(); } Function sslParametersFunctionToUse = getSslParametersFactory(sslSni, this.sslParametersFactory); return new SSLConfig(this.sslMode, createSslProvider(), this.sslEngineCustomizer, sslParametersFunctionToUse, this.sslHostnameVerifier); } private static Function getSslParametersFactory(boolean sslSni, Function sslParametersFunction) { if (!sslSni) { return sslParametersFunction; } return socket -> { SSLParameters sslParameters = sslParametersFunction.apply(socket); if (socket instanceof InetSocketAddress) { InetSocketAddress inetSocketAddress = (InetSocketAddress) socket; String hostString = inetSocketAddress.getHostString(); if (SSLConfig.isValidSniHostname(hostString)) { appendSniHost(sslParameters, hostString); } } return sslParameters; }; } private static void appendSniHost(SSLParameters sslParameters, String hostString) { List existingServerNames = sslParameters.getServerNames(); List serverNames = existingServerNames == null ? new ArrayList<>() : new ArrayList<>(existingServerNames); serverNames.add(new SNIHostName(hostString)); sslParameters.setServerNames(serverNames); } private Supplier createSslProvider() { SslContextBuilder sslContextBuilder = SslContextBuilder.forClient(); if (this.sslMode.verifyCertificate()) { if (this.sslRootCert != null) { doWithStream(this.sslRootCert, sslContextBuilder::trustManager); } } else { sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE); } sslContextBuilder.sslProvider( OpenSsl.isAvailable() ? io.netty.handler.ssl.SslProvider.OPENSSL : io.netty.handler.ssl.SslProvider.JDK) .ciphers(null, IdentityCipherSuiteFilter.INSTANCE) .applicationProtocolConfig(null); URL sslKey = this.sslKey; URL sslCert = this.sslCert; // Emulate Libpq behavior // Determining the default file location String pathSeparator = System.getProperty("file.separator"); String defaultDir; if (System.getProperty("os.name").toLowerCase().contains("windows")) { // It is Windows defaultDir = System.getenv("APPDATA") + pathSeparator + "postgresql" + pathSeparator; } else { defaultDir = System.getProperty("user.home") + pathSeparator + ".postgresql" + pathSeparator; } if (sslCert == null) { String pathname = defaultDir + "postgresql.crt"; sslCert = resolveUrlFromFile(pathname); } if (sslKey == null) { String pathname = defaultDir + "postgresql.pk8"; sslKey = resolveUrlFromFile(pathname); } URL sslKeyToUse = sslKey; if (sslKey != null && sslCert != null) { String sslPassword = this.sslPassword == null ? null : this.sslPassword.toString(); doWithStream(sslCert, certStream -> { doWithStream(sslKeyToUse, keyStream -> { sslContextBuilder.keyManager(certStream, keyStream, sslPassword); }); }); } return () -> { try { return this.sslContextBuilderCustomizer.apply(sslContextBuilder).build(); } catch (SSLException e) { throw new IllegalStateException("Failed to create SslContext", e); } }; } private SingleHostConfiguration.Builder prepareSingleHostConfiguration() { if (this.multiHostConfiguration != null) { throw new IllegalStateException("Cannot configure single-host properties because the builder is already configured with a multi-host configuration."); } if (this.singleHostConfiguration == null) { this.singleHostConfiguration = SingleHostConfiguration.builder(); } return this.singleHostConfiguration; } private MultiHostConfiguration.Builder prepareMultiHostConfiguration() { if (this.singleHostConfiguration != null) { throw new IllegalStateException("Cannot configure multi-host properties because the builder is already configured with a single-host configuration."); } if (this.multiHostConfiguration == null) { this.multiHostConfiguration = MultiHostConfiguration.builder(); } return this.multiHostConfiguration; } interface StreamConsumer { void doWithStream(InputStream is) throws IOException; } private void doWithStream(URL url, StreamConsumer consumer) { try (InputStream is = url.openStream()) { consumer.doWithStream(is); } catch (IOException e) { throw new IllegalStateException("Error while reading " + url, e); } } private URL requireExistingFilePath(String path, String message) { Assert.requireNonNull(path, message); URL resource = getClass().getClassLoader().getResource(path); if (resource != null) { return resource; } if (!new File(path).exists()) { throw new IllegalArgumentException(message); } return resolveUrlFromFile(path); } private URL resolveUrlFromFile(String pathname) { File file = new File(pathname); if (file.exists()) { try { return file.toURI().toURL(); } catch (MalformedURLException e) { throw new IllegalArgumentException(String.format("Malformed error occurred during creating URL from %s", pathname)); } } return null; } } static class FixedFetchSize implements ToIntFunction { private final int fetchSize; public FixedFetchSize(int fetchSize) { this.fetchSize = fetchSize; } @Override public int applyAsInt(String value) { return this.fetchSize; } @Override public String toString() { return "" + this.fetchSize; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy