blob: 65d9ac90e9b6296f3d37f76364229d0337d8e67d [file] [log] [blame]
/**
********************************************************************************
* Copyright (c) 2020 Eclipse APP4MC contributors.
*
* This program and the accompanying materials are made
* available under the terms of the Eclipse Public License 2.0
* which is available at https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
********************************************************************************
*/
package org.eclipse.app4mc.atdb;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ParameterMetaData;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.Stream.Builder;
public class DBConnection<T extends DBConnection<T>> implements AutoCloseable {
private static final String TABLE_EXISTS_QUERY = "SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?;";
private final Connection connection;
private final Map<String, PreparedStatement> prepStmts;
private final Collection<Statement> stmts;
private final Set<PreparedStatement> currentBatches;
private boolean isInBatchMode = false;
/**
* Creates a connection to the given db file in read-only mode.
*
* @param dbFile The path to the db file.
* @throws SQLException
*/
public DBConnection(final String dbFile) throws SQLException {
this(dbFile, false);
}
/**
* Creates a connection to the given db file.
*
* @param dbFile The path to the db file.
* @param writeAccess Specify <code>TRUE</code> to open the db connection with write access. <code>FALSE</code> for read-only mode.
* @throws SQLException
*/
public DBConnection(final String dbFile, final boolean writeAccess) throws SQLException {
try {
Class.forName("org.sqlite.JDBC"); // init JDBC driver
} catch (ClassNotFoundException e) {
// fail silently. DriverManager will throw an SQL exception complaining about a missing driver
}
final String dbFileUrl = "jdbc:sqlite:" + dbFile;
if (!writeAccess) {
final Properties properties = new Properties();
properties.put("open_mode", "1"); //$NON-NLS-1$ //$NON-NLS-2$
this.connection = DriverManager.getConnection(dbFileUrl, properties);
} else {
this.connection = DriverManager.getConnection(dbFileUrl);
try (final Statement statement = this.connection.createStatement()) {
statement.setQueryTimeout(30); // set timeout to 30 sec.
statement.executeUpdate("PRAGMA foreign_keys = ON;"); // enable foreign key checks
}
}
this.prepStmts = new LinkedHashMap<>();
this.stmts = new LinkedHashSet<>();
this.currentBatches = new LinkedHashSet<>();
}
/**
* Closes all statements created by this connection. Then closes the connection to the data base itself.
*/
@Override
public void close() throws SQLException {
for(final PreparedStatement ps:this.prepStmts.values()) {
ps.close();
}
this.prepStmts.clear();
this.connection.close();
}
/**
* Executes the statement (changing the contents of the db) given in the query.
*
* @param query The statement that performs the changes in the db.
* @return <code>TRUE</code> if the statement was executed by the db, <code>FALSE</code> otherwise.
* @throws SQLException
*/
public void executeUpdate(final String query) throws SQLException {
try (final Statement statement = this.connection.createStatement()) {
statement.setQueryTimeout(30); // set timeout to 30 sec.
statement.executeUpdate(query);
}
}
/**
* Same as {@link java.util.function.Consumer}, but may throw exception of type E.
* @param <T>
* @param <E>
*/
@FunctionalInterface
public static interface ThrowingConsumer<T, E extends Exception> {
void accept(T t) throws E;
}
/**
* Executes a read-only query and consumes the result set via the given consumer function.
*
* @param query The read-only query to execute.
* @param consumer The function that consumes the result set.
* @throws SQLException
*/
public void queryAndConsumeResult(final String query, final ThrowingConsumer<ResultSet, SQLException> consumer) throws SQLException {
try (final Statement tmpStatement = this.connection.createStatement(ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY)) {
final ResultSet resultSet = tmpStatement.executeQuery(query);
consumer.accept(resultSet);
}
}
/**
* Same as {@link java.util.function.Function}, but may throw exception of type E.
* @param <T>
* @param <R>
* @param <E>
*/
@FunctionalInterface
public static interface ThrowingFunction<T, R, E extends Exception> {
R apply(T t) throws E;
}
/**
* Subclasses can use this method to send a prepared statement query to the data base, map each result row via the rowMapper parameter, and
* return the result for further processing.
*
* @param <R> Target type for the row mapper and returned stream.
* @param query The prepared statement query
* @param rowMapper The result row mapping function
* @return Stream of mapped row results.
* @throws SQLException
*/
protected <R> Stream<R> queryAndMapToStream(final PreparedStatement query, final ThrowingFunction<ResultSet, R, SQLException> rowMapper)
throws SQLException {
try (final ResultSet resultSet = query.executeQuery()) {
final Builder<R> result = Stream.builder();
while (resultSet.next()) {
result.accept(rowMapper.apply(resultSet));
}
return result.build();
}
}
/**
* Returns a prepared statement for the the given query. Will return an existing one if the query was already created as a prepared statement.
* Since the prepared statement is internally cached, this DBConnection will also close it, when the connection is closed. Thus, the user must not
* close the prepared statement.
*
* @param query The query to put into the prepared statement.
* @return The prepared statement for the given query.
* @throws SQLException
*/
public PreparedStatement getPreparedStatementFor(final String query) throws SQLException {
if (!this.prepStmts.containsKey(query)) {
this.prepStmts.put(query, this.connection.prepareStatement(query));
}
return this.prepStmts.get(query);
}
/**
* Returns a prepared query for the given query string. This query is only allowed read access to the data base. Will return an existing one if the query
* was already created as a prepared query. Since the prepared query is internally cached, this DBConnection will also close it, when the connection is
* closed. Thus, the user must not close the prepared query.
*
* @param query The query string to put into the prepared query.
* @return The prepared query for the given query string.
* @throws SQLException
*/
public PreparedStatement getPrepareQueryFor(final String query) throws SQLException {
if (!this.prepStmts.containsKey(query)) {
this.prepStmts.put(query, this.connection.prepareStatement(query, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY));
}
return this.prepStmts.get(query);
}
/**
* Creates and returns a new statement. This DBConection will also close the statement, when the connection is closed. Thus, the user must not close the
* statement.
*
* @return A newly created statement.
* @throws SQLException
*/
public Statement createStatement() throws SQLException {
final Statement stmt = this.connection.createStatement();
this.stmts.add(stmt);
return stmt;
}
/**
* Executes the given statement batches (calls {@link Statement#executeBatch()} on each provided statement).
*
* @param statements Batches of statements to be executed.
* @throws SQLException
*/
public void executeBatchStatements(final Statement...statements) throws SQLException {
executeBatchStatements(Arrays.asList(statements));
}
/**
* Executes the given statement batches (calls {@link Statement#executeBatch()} on each provided statement).
*
* @param statements Batches of statements to be executed.
* @throws SQLException
*/
public void executeBatchStatements(final Collection<? extends Statement> statements) throws SQLException {
final boolean oldAutoCommit = this.connection.getAutoCommit();
if (oldAutoCommit) this.connection.setAutoCommit(false);
try {
for(final Statement stmt:statements) {
stmt.executeBatch();
}
} finally {
this.connection.setAutoCommit(oldAutoCommit);
}
}
/**
* Checks whether a table with the given tableName exists in the data base.
*
* @param tableName The name of the table to check for existence.
* @return <code>TRUE</code> if a table with the specified name exists, <code>FALSE</code> otherwise.
* @throws SQLException
*/
public boolean tableExists(final String tableName) throws SQLException {
final PreparedStatement prepStmt = getPrepareQueryFor(TABLE_EXISTS_QUERY);
prepStmt.setString(1, tableName);
try (final ResultSet resultSet = prepStmt.executeQuery()) {
return resultSet.next();
}
}
/**
* Can be used to execute updating statements (the ones that change the data base) in batches. These batches are only built up by calling
* {@link #executeBatchablePrepStmt(String, List)} (from a subclass, since it is protected).
*
* @param batchConsumer The consumer function in which all updating statements can be called to be batch executed.
* @throws SQLException
*/
public void executeBatchUpdate(final ThrowingConsumer<T, SQLException> batchConsumer) throws SQLException {
this.isInBatchMode = true;
// cast is safe because of recursive generic T
@SuppressWarnings("unchecked")
final T self = (T)this;
batchConsumer.accept(self);
executeBatchStatements(this.currentBatches);
this.currentBatches.clear();
this.isInBatchMode = false;
}
/**
* Subclasses can call this method to update the data base via a prepared statement with parameters.
*
* @param prepStmt The prepared statement used to update the data base.
* @param parameters The parameters for the prepared statement.
* @throws SQLException
*/
protected void executeBatchablePrepStmt(final String prepStmt, final List<?> parameters) throws SQLException {
final PreparedStatement stmt = getPreparedStatementFor(prepStmt);
final ParameterMetaData pmd = stmt.getParameterMetaData();
if (pmd.getParameterCount() == parameters.size()) {
for(int i = 0; i < parameters.size(); i++) {
stmt.setObject(i+1, parameters.get(i));
}
}
if (this.isInBatchMode) {
stmt.addBatch();
this.currentBatches.add(stmt);
} else {
stmt.executeUpdate();
}
}
}