
io.debezium.jdbc.JdbcConnection Maven / Gradle / Ivy
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.jdbc;
import java.lang.reflect.InvocationTargetException;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Objects;
import java.util.Optional;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.function.Supplier;
import org.apache.kafka.connect.errors.ConnectException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import io.debezium.DebeziumException;
import io.debezium.annotation.NotThreadSafe;
import io.debezium.annotation.ThreadSafe;
import io.debezium.config.Configuration;
import io.debezium.config.Field;
import io.debezium.relational.Column;
import io.debezium.relational.ColumnEditor;
import io.debezium.relational.RelationalDatabaseConnectorConfig;
import io.debezium.relational.TableId;
import io.debezium.relational.Tables;
import io.debezium.relational.Tables.ColumnNameFilter;
import io.debezium.relational.Tables.TableFilter;
import io.debezium.util.BoundedConcurrentHashMap;
import io.debezium.util.BoundedConcurrentHashMap.Eviction;
import io.debezium.util.BoundedConcurrentHashMap.EvictionListener;
import io.debezium.util.Collect;
import io.debezium.util.Strings;
/**
* A utility that simplifies using a JDBC connection and executing transactions composed of multiple statements.
*
* @author Randall Hauch
*/
@NotThreadSafe
public class JdbcConnection implements AutoCloseable {
private static final int WAIT_FOR_CLOSE_SECONDS = 10;
private static final char STATEMENT_DELIMITER = ';';
private static final int STATEMENT_CACHE_CAPACITY = 10_000;
private final static Logger LOGGER = LoggerFactory.getLogger(JdbcConnection.class);
private final Map statementCache = new BoundedConcurrentHashMap<>(STATEMENT_CACHE_CAPACITY, 16, Eviction.LIRS,
new EvictionListener() {
@Override
public void onEntryEviction(Map evicted) {
}
@Override
public void onEntryChosenForEviction(PreparedStatement statement) {
cleanupPreparedStatement(statement);
}
});
/**
* Establishes JDBC connections.
*/
@FunctionalInterface
@ThreadSafe
public static interface ConnectionFactory {
/**
* Establish a connection to the database denoted by the given configuration.
*
* @param config the configuration with JDBC connection information
* @return the JDBC connection; may not be null
* @throws SQLException if there is an error connecting to the database
*/
Connection connect(JdbcConfiguration config) throws SQLException;
}
private class ConnectionFactoryDecorator implements ConnectionFactory {
private final ConnectionFactory defaultConnectionFactory;
private final Supplier classLoaderSupplier;
private ConnectionFactory customConnectionFactory;
private ConnectionFactoryDecorator(ConnectionFactory connectionFactory, Supplier classLoaderSupplier) {
this.defaultConnectionFactory = connectionFactory;
this.classLoaderSupplier = classLoaderSupplier;
}
@Override
public Connection connect(JdbcConfiguration config) throws SQLException {
if (Strings.isNullOrEmpty(config.getConnectionFactoryClassName())) {
return defaultConnectionFactory.connect(config);
}
if (customConnectionFactory == null) {
customConnectionFactory = config.getInstance(JdbcConfiguration.CONNECTION_FACTORY_CLASS,
ConnectionFactory.class, classLoaderSupplier);
}
return customConnectionFactory.connect(config);
}
}
/**
* Defines multiple JDBC operations.
*/
@FunctionalInterface
public static interface Operations {
/**
* Apply a series of operations against the given JDBC statement.
*
* @param statement the JDBC statement to use to execute one or more operations
* @throws SQLException if there is an error connecting to the database or executing the statements
*/
void apply(Statement statement) throws SQLException;
}
/**
* Extracts a data of resultset..
*/
@FunctionalInterface
public static interface ResultSetExtractor {
T apply(ResultSet rs) throws SQLException;
}
/**
* Create a {@link ConnectionFactory} that replaces variables in the supplied URL pattern. Variables include:
*
* ${hostname}
* ${port}
* ${dbname}
* ${username}
* ${password}
*
*
* @param urlPattern the URL pattern string; may not be null
* @param variables any custom or overridden configuration variables
* @return the connection factory
*/
public static ConnectionFactory patternBasedFactory(String urlPattern, Field... variables) {
return (config) -> {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Config: {}", propsWithMaskedPassword(config.asProperties()));
}
Properties props = config.asProperties();
Field[] varsWithDefaults = combineVariables(variables,
JdbcConfiguration.HOSTNAME,
JdbcConfiguration.PORT,
JdbcConfiguration.USER,
JdbcConfiguration.PASSWORD,
JdbcConfiguration.DATABASE);
String url = findAndReplace(urlPattern, props, varsWithDefaults);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Props: {}", propsWithMaskedPassword(props));
}
LOGGER.trace("URL: {}", url);
Connection conn = DriverManager.getConnection(url, props);
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Connected to {} with {}", url, propsWithMaskedPassword(props));
}
return conn;
};
}
/**
* Create a {@link ConnectionFactory} that uses the specific JDBC driver class loaded with the given class loader, and obtains the connection URL by replacing the following variables in the URL pattern:
*
* ${hostname}
* ${port}
* ${dbname}
* ${username}
* ${password}
*
*
* This method attempts to instantiate the JDBC driver class and use that instance to connect to the database.
* @param urlPattern the URL pattern string; may not be null
* @param driverClassName the name of the JDBC driver class; may not be null
* @param classloader the ClassLoader that should be used to load the JDBC driver class given by `driverClassName`; may be null if this class' class loader should be used
* @param variables any custom or overridden configuration variables
* @return the connection factory
*/
@SuppressWarnings("unchecked")
public static ConnectionFactory patternBasedFactory(String urlPattern, String driverClassName,
ClassLoader classloader, Field... variables) {
return (config) -> {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Config: {}", propsWithMaskedPassword(config.asProperties()));
}
Properties props = config.asProperties();
Field[] varsWithDefaults = combineVariables(variables,
JdbcConfiguration.HOSTNAME,
JdbcConfiguration.PORT,
JdbcConfiguration.USER,
JdbcConfiguration.PASSWORD,
JdbcConfiguration.DATABASE);
String url = findAndReplace(urlPattern, props, varsWithDefaults);
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("Props: {}", propsWithMaskedPassword(props));
}
LOGGER.trace("URL: {}", url);
Connection conn = null;
try {
ClassLoader driverClassLoader = classloader;
if (driverClassLoader == null) {
driverClassLoader = JdbcConnection.class.getClassLoader();
}
Class driverClazz = (Class) Class.forName(driverClassName, true, driverClassLoader);
java.sql.Driver driver = driverClazz.getDeclaredConstructor().newInstance();
conn = driver.connect(url, props);
}
catch (ClassNotFoundException | IllegalAccessException | InstantiationException | NoSuchMethodException | InvocationTargetException e) {
throw new SQLException(e);
}
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Connected to {} with {}", url, propsWithMaskedPassword(props));
}
return conn;
};
}
private static Properties propsWithMaskedPassword(Properties props) {
final Properties filtered = new Properties();
filtered.putAll(props);
if (props.containsKey(JdbcConfiguration.PASSWORD.name())) {
filtered.put(JdbcConfiguration.PASSWORD.name(), "***");
}
return filtered;
}
private static Field[] combineVariables(Field[] overriddenVariables,
Field... defaultVariables) {
Map fields = new HashMap<>();
if (defaultVariables != null) {
for (Field variable : defaultVariables) {
fields.put(variable.name(), variable);
}
}
if (overriddenVariables != null) {
for (Field variable : overriddenVariables) {
fields.put(variable.name(), variable);
}
}
return fields.values().toArray(new Field[fields.size()]);
}
private static String findAndReplace(String url, Properties props, Field... variables) {
for (Field field : variables) {
if (field != null) {
url = findAndReplace(url, field.name(), props, field.defaultValueAsString());
}
}
for (Object key : new HashSet<>(props.keySet())) {
if (key != null) {
url = findAndReplace(url, key.toString(), props, null);
}
}
return url;
}
private static String findAndReplace(String url, String name, Properties props, String defaultValue) {
if (name != null && url.contains("${" + name + "}")) {
{
// Otherwise, we have to remove it from the properties ...
String value = props.getProperty(name);
if (value != null) {
props.remove(name);
}
if (value == null) {
value = defaultValue;
}
if (value != null) {
// And replace the variable ...
url = url.replaceAll("\\$\\{" + name + "\\}", value);
}
}
}
return url;
}
private final Configuration config;
private final ConnectionFactory factory;
private final Operations initialOps;
private volatile Connection conn;
/**
* Create a new instance with the given configuration and connection factory.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
*/
public JdbcConnection(Configuration config, ConnectionFactory connectionFactory) {
this(config, connectionFactory, (Operations) null);
}
/**
* Create a new instance with the given configuration and connection factory.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
*/
public JdbcConnection(Configuration config, ConnectionFactory connectionFactory, Supplier classLoaderSupplier) {
this(config, connectionFactory, null, null, classLoaderSupplier);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
*/
public JdbcConnection(Configuration config, ConnectionFactory connectionFactory, Operations initialOperations) {
this(config, connectionFactory, initialOperations, null);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
* @param adapter the function that can be called to update the configuration with defaults
*/
protected JdbcConnection(Configuration config, ConnectionFactory connectionFactory, Operations initialOperations,
Consumer adapter) {
this(config, connectionFactory, initialOperations, adapter, null);
}
/**
* Create a new instance with the given configuration and connection factory, and specify the operations that should be
* run against each newly-established connection.
*
* @param config the configuration; may not be null
* @param connectionFactory the connection factory; may not be null
* @param initialOperations the initial operations that should be run on each new connection; may be null
* @param adapter the function that can be called to update the configuration with defaults
* @param classLoaderSupplier class loader supplier
*/
protected JdbcConnection(Configuration config, ConnectionFactory connectionFactory, Operations initialOperations,
Consumer adapter, Supplier classLoaderSupplier) {
this.config = adapter == null ? config : config.edit().apply(adapter).build();
this.factory = classLoaderSupplier == null ? connectionFactory : new ConnectionFactoryDecorator(connectionFactory, classLoaderSupplier);
this.initialOps = initialOperations;
this.conn = null;
}
/**
* Obtain the configuration for this connection.
*
* @return the JDBC configuration; never null
*/
public JdbcConfiguration config() {
return JdbcConfiguration.adapt(config);
}
public JdbcConnection setAutoCommit(boolean autoCommit) throws SQLException {
connection().setAutoCommit(autoCommit);
return this;
}
public JdbcConnection commit() throws SQLException {
Connection conn = connection();
if (!conn.getAutoCommit()) {
conn.commit();
}
return this;
}
public synchronized JdbcConnection rollback() throws SQLException {
if (!isConnected()) {
return this;
}
Connection conn = connection();
if (!conn.getAutoCommit()) {
conn.rollback();
}
return this;
}
/**
* Ensure a connection to the database is established.
*
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database
*/
public JdbcConnection connect() throws SQLException {
connection();
return this;
}
/**
* Execute a series of SQL statements as a single transaction.
*
* @param sqlStatements the SQL statements that are to be performed as a single transaction
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection execute(String... sqlStatements) throws SQLException {
return execute(statement -> {
for (String sqlStatement : sqlStatements) {
if (sqlStatement != null) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("executing '{}'", sqlStatement);
}
statement.execute(sqlStatement);
}
}
});
}
/**
* Execute a series of operations as a single transaction.
*
* @param operations the function that will be called with a newly-created {@link Statement}, and that performs
* one or more operations on that statement object
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
*/
public JdbcConnection execute(Operations operations) throws SQLException {
Connection conn = connection();
try (Statement statement = conn.createStatement();) {
operations.apply(statement);
commit();
}
return this;
}
public static interface ResultSetConsumer {
void accept(ResultSet rs) throws SQLException;
}
public static interface ResultSetMapper {
T apply(ResultSet rs) throws SQLException;
}
public static interface BlockingResultSetConsumer {
void accept(ResultSet rs) throws SQLException, InterruptedException;
}
public static interface ParameterResultSetConsumer {
void accept(List> parameters, ResultSet rs) throws SQLException;
}
public static interface MultiResultSetConsumer {
void accept(ResultSet[] rs) throws SQLException;
}
public static interface BlockingMultiResultSetConsumer {
void accept(ResultSet[] rs) throws SQLException, InterruptedException;
}
public static interface StatementPreparer {
void accept(PreparedStatement statement) throws SQLException;
}
@FunctionalInterface
public static interface CallPreparer {
void accept(CallableStatement statement) throws SQLException;
}
/**
* Execute a SQL query.
*
* @param query the SQL query
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection query(String query, ResultSetConsumer resultConsumer) throws SQLException {
return query(query, Connection::createStatement, resultConsumer);
}
/**
* Execute a SQL query and map the result set into an expected type.
* @param type returned by the mapper
*
* @param query the SQL query
* @param mapper the function processing the query results
* @return the result of the mapper calculation
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public T queryAndMap(String query, ResultSetMapper mapper) throws SQLException {
return queryAndMap(query, Connection::createStatement, mapper);
}
/**
* Execute a stored procedure.
*
* @param sql the SQL query; may not be {@code null}
* @param callPreparer a {@link CallPreparer} instance which can be used to set additional parameters; may be null
* @param resultSetConsumer a {@link ResultSetConsumer} instance which can be used to process the results; may be null
* @return this object for chaining methods together
* @throws SQLException if anything unexpected fails
*/
public JdbcConnection call(String sql, CallPreparer callPreparer, ResultSetConsumer resultSetConsumer) throws SQLException {
Connection conn = connection();
try (CallableStatement callableStatement = conn.prepareCall(sql)) {
if (callPreparer != null) {
callPreparer.accept(callableStatement);
}
try (ResultSet rs = callableStatement.executeQuery()) {
if (resultSetConsumer != null) {
resultSetConsumer.accept(rs);
}
}
}
return this;
}
/**
* Execute a SQL query.
*
* @param query the SQL query
* @param statementFactory the function that should be used to create the statement from the connection; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection query(String query, StatementFactory statementFactory, ResultSetConsumer resultConsumer) throws SQLException {
Connection conn = connection();
try (Statement statement = statementFactory.createStatement(conn);) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("running '{}'", query);
}
try (ResultSet resultSet = statement.executeQuery(query);) {
if (resultConsumer != null) {
resultConsumer.accept(resultSet);
}
}
}
return this;
}
/**
* Execute multiple SQL prepared queries where each query is executed with the same set of parameters.
*
* @param multiQuery the array of prepared queries
* @param preparer the function that supplies arguments to the prepared statement; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareQuery(String[] multiQuery, StatementPreparer preparer, BlockingMultiResultSetConsumer resultConsumer)
throws SQLException, InterruptedException {
final StatementPreparer[] preparers = new StatementPreparer[multiQuery.length];
Arrays.fill(preparers, preparer);
return prepareQuery(multiQuery, preparers, resultConsumer);
}
/**
* Execute multiple SQL prepared queries where each query is executed with the same set of parameters.
*
* @param multiQuery the array of prepared queries
* @param preparers the array of functions that supply arguments to the prepared statements; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareQuery(String[] multiQuery, StatementPreparer[] preparers, BlockingMultiResultSetConsumer resultConsumer)
throws SQLException, InterruptedException {
final ResultSet[] resultSets = new ResultSet[multiQuery.length];
final PreparedStatement[] preparedStatements = new PreparedStatement[multiQuery.length];
try {
for (int i = 0; i < multiQuery.length; i++) {
final String query = multiQuery[i];
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("running '{}'", query);
}
final PreparedStatement statement = createPreparedStatement(query);
preparedStatements[i] = statement;
preparers[i].accept(statement);
resultSets[i] = statement.executeQuery();
}
if (resultConsumer != null) {
resultConsumer.accept(resultSets);
}
}
finally {
for (ResultSet rs : resultSets) {
if (rs != null) {
try {
rs.close();
}
catch (Exception ei) {
}
}
}
}
return this;
}
/**
* Execute a SQL query and map the result set into an expected type.
* @param type returned by the mapper
*
* @param query the SQL query
* @param statementFactory the function that should be used to create the statement from the connection; may not be null
* @param mapper the function processing the query results
* @return the result of the mapper calculation
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public T queryAndMap(String query, StatementFactory statementFactory, ResultSetMapper mapper) throws SQLException {
Objects.requireNonNull(mapper, "Mapper must be provided");
Connection conn = connection();
try (Statement statement = statementFactory.createStatement(conn);) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("running '{}'", query);
}
try (ResultSet resultSet = statement.executeQuery(query);) {
return mapper.apply(resultSet);
}
}
}
public JdbcConnection queryWithBlockingConsumer(String query, StatementFactory statementFactory, BlockingResultSetConsumer resultConsumer)
throws SQLException, InterruptedException {
Connection conn = connection();
try (Statement statement = statementFactory.createStatement(conn);) {
if (LOGGER.isTraceEnabled()) {
LOGGER.trace("running '{}'", query);
}
try (ResultSet resultSet = statement.executeQuery(query);) {
if (resultConsumer != null) {
resultConsumer.accept(resultSet);
}
}
}
return this;
}
/**
* A function to create a statement from a connection.
* @author Randall Hauch
*/
@FunctionalInterface
public interface StatementFactory {
/**
* Use the given connection to create a statement.
* @param connection the JDBC connection; never null
* @return the statement
* @throws SQLException if there are problems creating a statement
*/
Statement createStatement(Connection connection) throws SQLException;
}
/**
* Execute a SQL prepared query.
*
* @param preparedQueryString the prepared query string
* @param preparer the function that supplied arguments to the prepared statement; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareQueryWithBlockingConsumer(String preparedQueryString, StatementPreparer preparer, BlockingResultSetConsumer resultConsumer)
throws SQLException, InterruptedException {
final PreparedStatement statement = createPreparedStatement(preparedQueryString);
preparer.accept(statement);
try (ResultSet resultSet = statement.executeQuery();) {
if (resultConsumer != null) {
resultConsumer.accept(resultSet);
}
}
return this;
}
/**
* Execute a SQL prepared query.
*
* @param preparedQueryString the prepared query string
* @param preparer the function that supplied arguments to the prepared statement; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareQuery(String preparedQueryString, StatementPreparer preparer, ResultSetConsumer resultConsumer)
throws SQLException {
final PreparedStatement statement = createPreparedStatement(preparedQueryString);
preparer.accept(statement);
try (ResultSet resultSet = statement.executeQuery();) {
if (resultConsumer != null) {
resultConsumer.accept(resultSet);
}
}
return this;
}
/**
* Execute a SQL prepared query and map the result set into an expected type..
* @param type returned by the mapper
*
* @param preparedQueryString the prepared query string
* @param preparer the function that supplied arguments to the prepared statement; may not be null
* @param mapper the function processing the query results
* @return the result of the mapper calculation
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public T prepareQueryAndMap(String preparedQueryString, StatementPreparer preparer, ResultSetMapper mapper)
throws SQLException {
Objects.requireNonNull(mapper, "Mapper must be provided");
final PreparedStatement statement = createPreparedStatement(preparedQueryString);
preparer.accept(statement);
try (ResultSet resultSet = statement.executeQuery();) {
return mapper.apply(resultSet);
}
}
/**
* Execute a SQL update via a prepared statement.
*
* @param stmt the statement string
* @param preparer the function that supplied arguments to the prepared stmt; may be null
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareUpdate(String stmt, StatementPreparer preparer) throws SQLException {
final PreparedStatement statement = createPreparedStatement(stmt);
if (preparer != null) {
preparer.accept(statement);
}
statement.execute();
return this;
}
/**
* Execute a SQL prepared query.
*
* @param preparedQueryString the prepared query string
* @param parameters the list of values for parameters in the query; may not be null
* @param resultConsumer the consumer of the query results
* @return this object for chaining methods together
* @throws SQLException if there is an error connecting to the database or executing the statements
* @see #execute(Operations)
*/
public JdbcConnection prepareQuery(String preparedQueryString, List> parameters,
ParameterResultSetConsumer resultConsumer)
throws SQLException {
final PreparedStatement statement = createPreparedStatement(preparedQueryString);
int index = 1;
for (final Object parameter : parameters) {
statement.setObject(index++, parameter);
}
try (ResultSet resultSet = statement.executeQuery()) {
if (resultConsumer != null) {
resultConsumer.accept(parameters, resultSet);
}
}
return this;
}
public void print(ResultSet resultSet) {
// CHECKSTYLE:OFF
print(resultSet, System.out::println);
// CHECKSTYLE:ON
}
public void print(ResultSet resultSet, Consumer lines) {
try {
ResultSetMetaData rsmd = resultSet.getMetaData();
int columnCount = rsmd.getColumnCount();
int[] columnSizes = findMaxLength(resultSet);
lines.accept(delimiter(columnCount, columnSizes));
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
sb.append(" | ");
}
sb.append(Strings.setLength(rsmd.getColumnLabel(i), columnSizes[i], ' '));
}
lines.accept(sb.toString());
sb.setLength(0);
lines.accept(delimiter(columnCount, columnSizes));
while (resultSet.next()) {
sb.setLength(0);
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
sb.append(" | ");
}
sb.append(Strings.setLength(resultSet.getString(i), columnSizes[i], ' '));
}
lines.accept(sb.toString());
sb.setLength(0);
}
lines.accept(delimiter(columnCount, columnSizes));
}
catch (SQLException e) {
throw new RuntimeException(e);
}
}
private String delimiter(int columnCount, int[] columnSizes) {
StringBuilder sb = new StringBuilder();
for (int i = 1; i <= columnCount; i++) {
if (i > 1) {
sb.append("---");
}
sb.append(Strings.createString('-', columnSizes[i]));
}
return sb.toString();
}
private int[] findMaxLength(ResultSet resultSet) throws SQLException {
ResultSetMetaData rsmd = resultSet.getMetaData();
int columnCount = rsmd.getColumnCount();
int[] columnSizes = new int[columnCount + 1];
for (int i = 1; i <= columnCount; i++) {
columnSizes[i] = Math.max(columnSizes[i], rsmd.getColumnLabel(i).length());
}
while (resultSet.next()) {
for (int i = 1; i <= columnCount; i++) {
String value = resultSet.getString(i);
if (value != null) {
columnSizes[i] = Math.max(columnSizes[i], value.length());
}
}
}
resultSet.beforeFirst();
return columnSizes;
}
public synchronized boolean isConnected() throws SQLException {
if (conn == null) {
return false;
}
return !conn.isClosed();
}
public synchronized Connection connection() throws SQLException {
return connection(true);
}
public synchronized Connection connection(boolean executeOnConnect) throws SQLException {
if (!isConnected()) {
conn = factory.connect(JdbcConfiguration.adapt(config));
if (!isConnected()) {
throw new SQLException("Unable to obtain a JDBC connection");
}
// Always run the initial operations on this new connection
if (initialOps != null) {
execute(initialOps);
}
final String statements = config.getString(JdbcConfiguration.ON_CONNECT_STATEMENTS);
if (statements != null && executeOnConnect) {
final List splitStatements = parseSqlStatementString(statements);
execute(splitStatements.toArray(new String[splitStatements.size()]));
}
}
return conn;
}
protected List parseSqlStatementString(final String statements) {
final List splitStatements = new ArrayList<>();
final char[] statementsChars = statements.toCharArray();
StringBuilder activeStatement = new StringBuilder();
for (int i = 0; i < statementsChars.length; i++) {
if (statementsChars[i] == STATEMENT_DELIMITER) {
if (i == statementsChars.length - 1) {
// last character so it is the delimiter
}
else if (statementsChars[i + 1] == STATEMENT_DELIMITER) {
// two semicolons in a row - escaped semicolon
activeStatement.append(STATEMENT_DELIMITER);
i++;
}
else {
// semicolon as a delimiter
final String trimmedStatement = activeStatement.toString().trim();
if (!trimmedStatement.isEmpty()) {
splitStatements.add(trimmedStatement);
}
activeStatement = new StringBuilder();
}
}
else {
activeStatement.append(statementsChars[i]);
}
}
final String trimmedStatement = activeStatement.toString().trim();
if (!trimmedStatement.isEmpty()) {
splitStatements.add(trimmedStatement);
}
return splitStatements;
}
/**
* Close the connection and release any resources.
*/
@Override
public synchronized void close() throws SQLException {
if (conn != null) {
try {
statementCache.values().forEach(this::cleanupPreparedStatement);
statementCache.clear();
LOGGER.trace("Closing database connection");
doClose();
}
finally {
conn = null;
}
}
}
private void doClose() throws SQLException {
ExecutorService executor = Executors.newSingleThreadExecutor();
// attempting to close the connection gracefully
Future