io.mats3.test.TestH2DataSource Maven / Gradle / Ivy
package io.mats3.test;
import java.io.PrintWriter;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.SQLFeatureNotSupportedException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import javax.sql.ConnectionPoolDataSource;
import javax.sql.DataSource;
import javax.sql.PooledConnection;
import javax.sql.XAConnection;
import javax.sql.XADataSource;
import org.h2.jdbcx.JdbcDataSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* A wrapped H2 DataBase DataSource which has a couple of extra methods which simplifies testing, in particular the
* {@link #cleanDatabase()}, and {@link #createDataTable()} method with associated convenience methods for storing and
* getting simple values.
*/
public class TestH2DataSource implements XADataSource, DataSource, ConnectionPoolDataSource {
private static final Logger log = LoggerFactory.getLogger(TestH2DataSource.class);
/**
* System property ("-D" jvm argument) that if set will change the method {@link #createStandard()} from returning
* an in-memory H2 DataSource, to instead return a DataSource using the URL from the value, with the special case
* that if the value is "{@link #SYSPROP_VALUE_FILE_BASED file}", it will be
* "jdbc:h2:./matsTestH2DB;AUTO_SERVER=TRUE"
.
*
* Value is {@code "mats.test.activemq"}
*/
public static final String SYSPROP_MATS_TEST_H2 = "mats.test.h2";
/**
* If the value of {@link #SYSPROP_MATS_TEST_H2} is this value, the {@link #createStandard()} will use the URL
* {@link #FILE_BASED_TEST_H2_DATABASE_URL}, which is "jdbc:h2:./matsTestH2DB;AUTO_SERVER=TRUE"
.
*
* Value is {@code "file"}
*/
public static final String SYSPROP_VALUE_FILE_BASED = "file";
public static final String FILE_BASED_TEST_H2_DATABASE_URL = "jdbc:h2:./matsTestH2DB;AUTO_SERVER=TRUE";
public static final String IN_MEMORY_TEST_H2_DATABASE_URL = "jdbc:h2:mem:matsTestH2DB_uniqueRnd_$random$;DB_CLOSE_DELAY=-1";
/**
* Creates an in-memory {@link TestH2DataSource} as specified by the URL {@link #IN_MEMORY_TEST_H2_DATABASE_URL},
* which is "jdbc:h2:mem:matsTestH2DB_[randomness];DB_CLOSE_DELAY=-1"
, unless the System
* Property {@link #SYSPROP_MATS_TEST_H2} ("mats.test.h2"
) is directly set to a different URL to use,
* with the special case that if it is {@link #SYSPROP_VALUE_FILE_BASED} ("file"
), in which case
* {@link #FILE_BASED_TEST_H2_DATABASE_URL} ("jdbc:h2:./matsTestH2DB;AUTO_SERVER=TRUE
) is used as URL.
*
* Notice that the method {@link #cleanDatabase()} is invoked when creating the DataSource, which is relevant
* when using the file-based variant.
*
* @return the created {@link TestH2DataSource}.
*/
public static TestH2DataSource createStandard() {
String sysprop_matsTestH2 = System.getProperty(SYSPROP_MATS_TEST_H2);
// ?: Was it set?
if (sysprop_matsTestH2 == null) {
// -> No, not set - so return normal in-memory database
return createInMemoryRandom();
}
// E-> The System Property was set
// ?: Was it the special "file" value?
if (sysprop_matsTestH2.equalsIgnoreCase(SYSPROP_VALUE_FILE_BASED)) {
// -> Yes, special "file" value
return createFileBased();
}
// E-> No, not special value, so treat it as a URL directly
return create(sysprop_matsTestH2);
}
/**
* Creates a {@link TestH2DataSource} using the URL {@link #FILE_BASED_TEST_H2_DATABASE_URL}, which is
* "jdbc:h2:./matsTestH2DB;AUTO_SERVER=TRUE"
.
*
* @return the created {@link TestH2DataSource}.
*/
public static TestH2DataSource createFileBased() {
return create(FILE_BASED_TEST_H2_DATABASE_URL);
}
/**
* Creates a unique (random) {@link TestH2DataSource} using the URL {@link #IN_MEMORY_TEST_H2_DATABASE_URL}, which
* is "jdbc:h2:mem:matsTestH2DB_[randomness];DB_CLOSE_DELAY=-1"
.
*
* @return the created {@link TestH2DataSource}.
*/
public static TestH2DataSource createInMemoryRandom() {
return create(IN_MEMORY_TEST_H2_DATABASE_URL
.replace("$random$", Long.toString(Math.abs(ThreadLocalRandom.current().nextLong()), 36)));
}
/**
* Creates a {@link TestH2DataSource} using the supplied URL.
*
* @return the created {@link TestH2DataSource}.
*/
public static TestH2DataSource create(String url) {
log.info("Creating TestH2DataSource with URL [" + url + "].");
TestH2DataSource dataSource = new TestH2DataSource();
dataSource.setURL(url);
dataSource.cleanDatabase();
return dataSource;
}
private final JdbcDataSource _wrappedH2JdbcDataSource;
private TestH2DataSource() {
_wrappedH2JdbcDataSource = new JdbcDataSource();
}
/**
* Cleans the test database: Runs SQL "DROP ALL OBJECTS DELETE FILES"
.
*/
public void cleanDatabase() {
cleanDatabase(false);
}
/**
* Cleans the test database: Runs SQL "DROP ALL OBJECTS DELETE FILES"
, and optionally invokes
* {@link #createDataTable()}.
*
* @param createDataTable
* whether to invoke {@link #createDataTable()} afterwards.
*/
public void cleanDatabase(boolean createDataTable) {
String dropSql = "DROP ALL OBJECTS DELETE FILES";
try (Connection con = this.getConnection();
Statement stmt = con.createStatement()) {
stmt.execute(dropSql);
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems cleaning database by running '" + dropSql + "'.", e);
}
if (createDataTable) {
createDataTable();
}
}
/**
* Creates a test "datatable", runs SQL "CREATE TABLE datatable ( data VARCHAR )"
, using a SQL
* Connection from this
DataSource.
*/
public void createDataTable() {
try {
try (Connection dbCon = this.getConnection()) {
createDataTable(dbCon);
}
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems creating the SQL 'datatable'.", e);
}
}
/**
* Creates a test "datatable", runs SQL "CREATE TABLE datatable ( data VARCHAR )"
, using the provided
* SQL Connection.
*/
public static void createDataTable(Connection connection) {
try {
try (Statement stmt = connection.createStatement();) {
stmt.execute("CREATE TABLE datatable (data VARCHAR NOT NULL, CONSTRAINT UC_data UNIQUE (data))");
}
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems creating the SQL 'datatable'.", e);
}
}
/**
* Inserts the provided 'data' into the SQL Table 'datatable', using a SQL Connection from this
* DataSource.
*
* @param data
* the data to insert.
*/
public void insertDataIntoDataTable(String data) {
try (Connection con = this.getConnection()) {
insertDataIntoDataTable(con, data);
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems getting SQL Connection.", e);
}
}
/**
* Inserts the provided 'data' into the SQL Table 'datatable', using the provided SQL Connection.
*
* @param sqlConnection
* the SQL Connection to use to insert data.
* @param data
* the data to insert.
*/
public static void insertDataIntoDataTable(Connection sqlConnection, String data) {
// :: Populate the SQL table with a piece of data
try {
PreparedStatement pStmt = sqlConnection.prepareStatement("INSERT INTO datatable VALUES (?)");
pStmt.setString(1, data);
pStmt.execute();
pStmt.close();
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems fetching column 'data' from SQL Table 'datatable'.", e);
}
}
/**
* @return all rows in the 'datatable' (probably created by {@link #createDataTable()}, using a SQL Connection from
* this
DataSource.
*/
public List getDataFromDataTable() {
try (Connection con = this.getConnection()) {
return getDataFromDataTable(con);
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems getting SQL Connection.", e);
}
}
/**
* @param sqlConnection
* the SQL Connection to use to fetch data.
* @return all rows in the 'datatable' (probably created by {@link #createDataTable()}, using the provided SQL
* Connection - the SQL is "SELECT data FROM datatable ORDER BY data"
.
*/
public static List getDataFromDataTable(Connection sqlConnection) {
try {
Statement stmt = sqlConnection.createStatement();
List ret = new ArrayList<>();
try (ResultSet rs = stmt.executeQuery("SELECT data FROM datatable ORDER BY data")) {
while (rs.next()) {
ret.add(rs.getString(1));
}
}
stmt.close();
return ret;
}
catch (SQLException e) {
throw new TestH2DataSourceException("Got problems fetching column 'data' from SQL Table 'datatable'.", e);
}
}
/**
* A {@link RuntimeException} for use in database access methods and tests.
*/
public static class TestH2DataSourceException extends RuntimeException {
public TestH2DataSourceException(String message, Throwable cause) {
super(message, cause);
}
}
/**
* Closes the database IF it is a random in-memory URL (as created by {@link #createInMemoryRandom()}, note:
* this method will be picked up by Spring as a destroy-method if the instance is made available as a Bean.
*/
public void close() {
final String thisUrl = getUrl();
if (thisUrl.contains(":mem:matsTestH2DB")) {
// :: Due to the fact that there is evidently some Thread.sleep(200) involved somewhere, we do this async
// in a separate Thread. This is OK since we use separate DataSources with random URLs for each DS instance.
log.info("Shutting down in-mem random TestH2DataSource with url [" + thisUrl
+ "] (performed in separate Thread to not hold back next tests).");
Thread shutdownH2Thread = new Thread(() -> {
try {
Connection con = getConnection();
Statement stmt = con.createStatement();
stmt.execute("SHUTDOWN");
stmt.close();
con.close();
}
catch (SQLException e) {
throw new AssertionError("Problems shutting down H2", e);
}
log.info("Shutdown of TestH2DataSource [" + thisUrl + "] finished (from previous tests).");
}, "ShutdownThread of TestH2DataSource [" + thisUrl + "]");
// If the VM shuts down, H2 will also shut down, probably due to a shutdown hook thread.
// We've sometimes gotten this exception during test runs:
// Caused by: org.h2.jdbc.JdbcSQLNonTransientConnectionException: Database is already closed (to disable
// automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-210]
// So, /either/ daemon this thread, OR add that prop to the URL. Both would probably be unwise.
shutdownH2Thread.setDaemon(true);
shutdownH2Thread.start();
}
}
// =================================================================================================
// ======= Implementation of the non-standard methods of H2's JdbcDataSource
// =================================================================================================
/**
* Get the current URL.
*
* @return the URL
*/
public String getURL() {
return _wrappedH2JdbcDataSource.getURL();
}
/**
* Get the current URL. This method does the same as getURL, but this methods signature conforms the JavaBean naming
* convention.
*
* @return the URL
*/
public String getUrl() {
return _wrappedH2JdbcDataSource.getUrl();
}
/**
* Set the current URL.
*
* @param url
* the new URL
*/
public void setURL(String url) {
_wrappedH2JdbcDataSource.setURL(url);
}
/**
* Set the current URL. This method does the same as setURL, but this methods signature conforms the JavaBean naming
* convention.
*
* @param url
* the new URL
*/
public void setUrl(String url) {
_wrappedH2JdbcDataSource.setUrl(url);
}
/**
* Set the current password.
*
* @param password
* the new password.
*/
public void setPassword(String password) {
_wrappedH2JdbcDataSource.setPassword(password);
}
/**
* Set the current password in the form of a char array.
*
* @param password
* the new password in the form of a char array.
*/
public void setPasswordChars(char[] password) {
_wrappedH2JdbcDataSource.setPasswordChars(password);
}
/**
* Get the current password.
*
* @return the password
*/
public String getPassword() {
return _wrappedH2JdbcDataSource.getPassword();
}
/**
* Get the current user name.
*
* @return the user name
*/
public String getUser() {
return _wrappedH2JdbcDataSource.getUser();
}
/**
* Set the current user name.
*
* @param user
* the new user name
*/
public void setUser(String user) {
_wrappedH2JdbcDataSource.setUser(user);
}
/**
* Get the current description.
*
* @return the description
*/
public String getDescription() {
return _wrappedH2JdbcDataSource.getDescription();
}
/**
* Set the description.
*
* @param description
* the new description
*/
public void setDescription(String description) {
_wrappedH2JdbcDataSource.setDescription(description);
}
// =================================================================================================
// ======= Implementation of the interfaces XADataSource, DataSource, ConnectionPoolDataSource
// =================================================================================================
@Override
public Connection getConnection() throws SQLException {
return _wrappedH2JdbcDataSource.getConnection();
}
@Override
public Connection getConnection(String username, String password) throws SQLException {
return _wrappedH2JdbcDataSource.getConnection(username, password);
}
@Override
@SuppressWarnings("unchecked")
public T unwrap(Class iface) throws SQLException {
if (iface == null) {
throw new SQLException("iface is null");
}
if (_wrappedH2JdbcDataSource.isWrapperFor(iface)) {
return _wrappedH2JdbcDataSource.unwrap(iface);
}
if (iface.isAssignableFrom(getClass())) {
return (T) this;
}
throw new SQLException("this.isWrapperFor([" + iface + "]) == false, this:[" + this + "]");
}
@Override
public boolean isWrapperFor(Class> iface) throws SQLException {
if (iface == null) {
return false;
}
if (iface.isAssignableFrom(getClass())) {
return true;
}
return _wrappedH2JdbcDataSource.isWrapperFor(iface);
}
@Override
public PooledConnection getPooledConnection() throws SQLException {
return _wrappedH2JdbcDataSource.getPooledConnection();
}
@Override
public PooledConnection getPooledConnection(String user, String password) throws SQLException {
return _wrappedH2JdbcDataSource.getPooledConnection(user, password);
}
@Override
public XAConnection getXAConnection() throws SQLException {
return _wrappedH2JdbcDataSource.getXAConnection();
}
@Override
public XAConnection getXAConnection(String user, String password) throws SQLException {
return _wrappedH2JdbcDataSource.getXAConnection(user, password);
}
@Override
public PrintWriter getLogWriter() throws SQLException {
return _wrappedH2JdbcDataSource.getLogWriter();
}
@Override
public void setLogWriter(PrintWriter out) throws SQLException {
_wrappedH2JdbcDataSource.setLogWriter(out);
}
@Override
public void setLoginTimeout(int seconds) throws SQLException {
_wrappedH2JdbcDataSource.setLoginTimeout(seconds);
}
@Override
public int getLoginTimeout() throws SQLException {
return _wrappedH2JdbcDataSource.getLoginTimeout();
}
@Override
public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
return _wrappedH2JdbcDataSource.getParentLogger();
}
}