eliminated dependency on jdbc core

This commit is contained in:
Keith Donald
2009-05-06 07:36:24 +00:00
parent c0c9501005
commit bc9ad5685d
11 changed files with 470 additions and 135 deletions

View File

@@ -0,0 +1,10 @@
package org.springframework.jdbc.datasource.embedded;
import org.springframework.core.io.support.EncodedResource;
public class CannotReadScriptException extends RuntimeException {
public CannotReadScriptException(EncodedResource resource, Throwable cause) {
super("Cannot read SQL script from " + resource, cause);
}
}

View File

@@ -15,8 +15,8 @@
*/
package org.springframework.jdbc.datasource.embedded;
import org.springframework.dao.DataAccessException;
import org.springframework.jdbc.core.JdbcTemplate;
import java.sql.Connection;
import java.sql.SQLException;
/**
* Strategy for populating a database with data.
@@ -27,8 +27,8 @@ public interface DatabasePopulator {
/**
* Populate the database using the JDBC-based data access template provided.
* @param template the data access template to use to populate the db; already configured and ready to use
* @throws DataAccessException if an unrecoverable data access exception occurs during database population
* @param connection the JDBC connection to use to populate the db; already configured and ready to use
* @throws SQLException if an unrecoverable data access exception occurs during database population
*/
void populate(JdbcTemplate template);
void populate(Connection connection) throws SQLException;
}

View File

@@ -23,24 +23,20 @@ import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.Assert;
/**
* Returns a {@link EmbeddedDatabase} instance pre-populated with test data.
* When the database is returned, callers are guaranteed that the database schema and test data will have already been loaded.
* Returns a {@link EmbeddedDatabase} instance pre-populated with test data. When the database is returned, callers are
* guaranteed that the database schema and test data will have already been loaded.
* <p>
* Can be configured:<br>
* Call {@link #setDatabaseName(String)} to change the name of the database.<br>
* Call {@link #setDatabaseType(EmbeddedDatabaseType)} to set the database type if you wish to use one of the supported types.<br>
* Call {@link #setDatabaseConfigurer(EmbeddedDatabaseConfigurer)} to set a configuration strategy for your own embedded database type.<br>
* Call {@link #setDatabasePopulator(DatabasePopulator)} to change the algorithm used to populate the database.<br>
* Call {@link #setDataSourceFactory(DataSourceFactory)} to change the type of DataSource used to connect to the database.<br>
* Call {@link #getDatabase()} to get the {@link EmbeddedDatabase} instance.<br>
*
* Can be configured.
* Call {@link #setDatabaseName(String)} to change the name of the database.
* Call {@link #setDatabaseType(EmbeddedDatabaseType)} to set the database type if you wish to use one of the supported types.
* Call {@link #setDatabaseConfigurer(EmbeddedDatabaseConfigurer)} to set a configuration strategy for your own embedded database type.
* Call {@link #setDatabasePopulator(DatabasePopulator)} to change the algorithm used to populate the database.
* Call {@link #setDataSourceFactory(DataSourceFactory)} to change the type of DataSource used to connect to the database.
* Call {@link #getDatabase()} to get the {@link EmbeddedDatabase} instance.
* @author Keith Donald
*/
public class EmbeddedDatabaseFactory {
@@ -50,36 +46,34 @@ public class EmbeddedDatabaseFactory {
private String databaseName;
private DataSourceFactory dataSourceFactory;
private EmbeddedDatabaseConfigurer databaseConfigurer;
private DatabasePopulator databasePopulator;
private DataSource dataSource;
/**
* Creates a default {@link EmbeddedDatabaseFactory}.
* Calling {@link #getDatabase()} will create a embedded HSQL database of name 'testdb'.
* Creates a default {@link EmbeddedDatabaseFactory}. Calling {@link #getDatabase()} will create a embedded HSQL
* database of name 'testdb'.
*/
public EmbeddedDatabaseFactory() {
setDatabaseName("testdb");
setDatabaseType(EmbeddedDatabaseType.HSQL);
setDataSourceFactory(new SimpleDriverDataSourceFactory());
}
/**
* Sets the name of the database.
* Defaults to 'testdb'.
* Sets the name of the database. Defaults to 'testdb'.
* @param name of the test database
*/
public void setDatabaseName(String name) {
Assert.notNull(name, "The testDatabaseName is required");
databaseName = name;
}
/**
* Sets the type of embedded database to use.
* Call this when you wish to configure one of the pre-supported types.
* Sets the type of embedded database to use. Call this when you wish to configure one of the pre-supported types.
* Defaults to HSQL.
* @param type the test database type
*/
@@ -88,18 +82,17 @@ public class EmbeddedDatabaseFactory {
}
/**
* Sets the strategy that will be used to configure the embedded database instance.
* Call this when you wish to use an embedded database type not already supported.
* Sets the strategy that will be used to configure the embedded database instance. Call this when you wish to use
* an embedded database type not already supported.
* @param configurer the embedded database configurer
*/
public void setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) {
this.databaseConfigurer = configurer;
}
/**
* Sets the strategy that will be used to populate the embedded database.
* Defaults to null.
* @param populator the database populator
* Sets the strategy that will be used to populate the embedded database. Defaults to null.
* @param populator the database populator
*/
public void setDatabasePopulator(DatabasePopulator populator) {
Assert.notNull(populator, "The DatabasePopulator is required");
@@ -107,8 +100,8 @@ public class EmbeddedDatabaseFactory {
}
/**
* Sets the factory to use to create the DataSource instance that connects to the embedded database.
* Defaults to {@link SimpleDriverDataSourceFactory}.
* Sets the factory to use to create the DataSource instance that connects to the embedded database. Defaults to
* {@link SimpleDriverDataSourceFactory}.
* @param dataSourceFactory the data source factory
*/
public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
@@ -129,7 +122,7 @@ public class EmbeddedDatabaseFactory {
}
// subclassing hooks
protected void initDataSource() {
// create the embedded database source first
if (logger.isInfoEnabled()) {
@@ -146,26 +139,27 @@ public class EmbeddedDatabaseFactory {
protected DataSource getDataSource() {
return dataSource;
}
protected void shutdownDataSource() {
if (dataSource != null) {
databaseConfigurer.shutdown(dataSource);
dataSource = null;
}
}
// internal helper methods
private void populateDatabase() {
TransactionTemplate template = new TransactionTemplate(new DataSourceTransactionManager(dataSource));
template.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
databasePopulator.populate(new JdbcTemplate(dataSource));
}
});
Connection connection = JdbcUtils.getConnection(dataSource);
try {
databasePopulator.populate(connection);
} catch (SQLException e) {
throw new RuntimeException("SQLException occurred populating embedded database", e);
} finally {
JdbcUtils.closeConnection(connection);
}
}
private class EmbeddedDataSourceProxy implements EmbeddedDatabase {
private DataSource dataSource;
@@ -177,8 +171,7 @@ public class EmbeddedDatabaseFactory {
return dataSource.getConnection();
}
public Connection getConnection(String username, String password)
throws SQLException {
public Connection getConnection(String username, String password) throws SQLException {
return dataSource.getConnection(username, password);
}
@@ -205,11 +198,11 @@ public class EmbeddedDatabaseFactory {
public <T> T unwrap(Class<T> iface) throws SQLException {
return dataSource.unwrap(iface);
}
public void shutdown() {
shutdownDataSource();
}
}
}

View File

@@ -15,13 +15,20 @@
*/
package org.springframework.jdbc.datasource.embedded;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.util.ClassUtils;
public class HsqlEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigurer {
private static final Log logger = LogFactory.getLog(HsqlEmbeddedDatabaseConfigurer.class);
private static HsqlEmbeddedDatabaseConfigurer INSTANCE;
public static synchronized HsqlEmbeddedDatabaseConfigurer getInstance() throws ClassNotFoundException {
@@ -40,7 +47,18 @@ public class HsqlEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigure
}
public void shutdown(DataSource dataSource) {
new JdbcTemplate(dataSource).execute("SHUTDOWN");
Connection connection = JdbcUtils.getConnection(dataSource);
Statement stmt = null;
try {
stmt = connection.createStatement();
stmt.execute("SHUTDOWN");
} catch (SQLException e) {
if (logger.isWarnEnabled()) {
logger.warn("Could not shutdown in-memory HSQL database", e);
}
} finally {
JdbcUtils.closeStatement(stmt);
}
}
}

View File

@@ -0,0 +1,59 @@
package org.springframework.jdbc.datasource.embedded;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import javax.sql.DataSource;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.jdbc.CannotGetJdbcConnectionException;
/**
* Helper JDBC utilities used by other classes in this package. There is some duplication here with JdbcUtils in
* jdbc.support package. We may want to consider simply using that. Package private for now.
* @author Keith Donald
*/
final class JdbcUtils {
private static Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class);
private JdbcUtils() {
}
public static Connection getConnection(DataSource dataSource) {
try {
return dataSource.getConnection();
} catch (SQLException ex) {
throw new CannotGetJdbcConnectionException("Could not get JDBC Connection", ex);
}
}
public static void closeConnection(Connection connection) {
if (connection != null) {
try {
connection.close();
} catch (SQLException ex) {
logger.debug("Could not close JDBC Connection", ex);
} catch (Throwable ex) {
// We don't trust the JDBC driver: It might throw RuntimeException or Error.
logger.debug("Unexpected exception on closing JDBC Connection", ex);
}
}
}
public static void closeStatement(Statement stmt) {
if (stmt != null) {
try {
stmt.close();
} catch (SQLException ex) {
logger.debug("Could not close JDBC Statement", ex);
} catch (Throwable ex) {
// We don't trust the JDBC driver: It might throw RuntimeException or Error.
logger.debug("Unexpected exception on closing JDBC Statement", ex);
}
}
}
}

View File

@@ -17,6 +17,9 @@ package org.springframework.jdbc.datasource.embedded;
import java.io.IOException;
import java.io.LineNumberReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
@@ -25,19 +28,15 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.dao.DataAccessException;
import org.springframework.dao.DataAccessResourceFailureException;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.util.StringUtils;
/**
* Populates a database from schema and test-data SQL defined in external resources.
* By default, looks for a schema.sql file and test-data.sql resource in the root of the classpath.
* Populates a database from schema and test-data SQL defined in external resources. By default, looks for a schema.sql
* file and test-data.sql resource in the root of the classpath.
*
* May be configured.
* Call {@link #setSchemaLocation(Resource)} to configure the location of the database schema file.
* Call {@link #setTestDataLocation(Resource)} to configure the location of the test data file.
* Call {@link #setSqlScriptEncoding(String)} to set the encoding for the schema and test data SQL.
* May be configured. Call {@link #setSchemaLocation(Resource)} to configure the location of the database schema file.
* Call {@link #setTestDataLocation(Resource)} to configure the location of the test data file. Call
* {@link #setSqlScriptEncoding(String)} to set the encoding for the schema and test data SQL.
*/
public class ResourceDatabasePopulator implements DatabasePopulator {
@@ -46,7 +45,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
private String sqlScriptEncoding;
private List<Resource> scripts = new ArrayList<Resource>();
/**
* Add a script to execute to populate the database.
* @param script the path to a SQL script
@@ -54,7 +53,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
public void addScript(Resource script) {
scripts.add(script);
}
/**
* Specify the encoding for SQL scripts, if different from the platform encoding.
*/
@@ -62,68 +61,64 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
this.sqlScriptEncoding = sqlScriptEncoding;
}
public void populate(JdbcTemplate template) {
public void populate(Connection connection) throws SQLException {
for (Resource script : scripts) {
executeSqlScript(template, new EncodedResource(script, sqlScriptEncoding), false);
executeSqlScript(connection, new EncodedResource(script, sqlScriptEncoding), false);
}
}
// From SimpleJdbcTestUtils - TODO address duplication
/**
* Execute the given SQL script.
* <p>The script will normally be loaded by classpath. There should be one statement
* per line. Any semicolons will be removed. <b>Do not use this method to execute
* DDL if you expect rollback.</b>
* Execute the given SQL script. <p>The script will normally be loaded by classpath. There should be one statement
* per line. Any semicolons will be removed. <b>Do not use this method to execute DDL if you expect rollback.</b>
* @param template the SimpleJdbcTemplate with which to perform JDBC operations
* @param resource the resource (potentially associated with a specific encoding)
* to load the SQL script from.
* @param continueOnError whether or not to continue without throwing an
* exception in the event of an error.
* @param resource the resource (potentially associated with a specific encoding) to load the SQL script from.
* @param continueOnError whether or not to continue without throwing an exception in the event of an error.
*/
static void executeSqlScript(JdbcTemplate template, EncodedResource resource, boolean continueOnError) {
private void executeSqlScript(Connection connection, EncodedResource resource, boolean continueOnError)
throws SQLException {
if (logger.isInfoEnabled()) {
logger.info("Executing SQL script from " + resource);
}
long startTime = System.currentTimeMillis();
List<String> statements = new LinkedList<String>();
String script;
try {
LineNumberReader lnr = new LineNumberReader(resource.getReader());
String script = readScript(lnr);
char delimiter = ';';
if (!containsSqlScriptDelimiters(script, delimiter)) {
delimiter = '\n';
}
splitSqlScript(script, delimiter, statements);
for (String statement : statements) {
try {
int rowsAffected = template.update(statement);
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " rows affected by SQL: " + statement);
}
script = readScript(resource);
} catch (IOException e) {
throw new CannotReadScriptException(resource, e);
}
char delimiter = ';';
if (!containsSqlScriptDelimiters(script, delimiter)) {
delimiter = '\n';
}
splitSqlScript(script, delimiter, statements);
int lineNumber = 0;
for (String statement : statements) {
lineNumber++;
Statement stmt = null;
try {
stmt = connection.createStatement();
int rowsAffected = stmt.executeUpdate(statement);
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " rows affected by SQL: " + statement);
}
catch (DataAccessException ex) {
if (continueOnError) {
if (logger.isWarnEnabled()) {
logger.warn("SQL: " + statement + " failed", ex);
}
}
else {
throw ex;
} catch (SQLException e) {
if (continueOnError) {
if (logger.isWarnEnabled()) {
logger.warn("Line " + lineNumber + " statement failed: " + statement, e);
}
} else {
throw e;
}
}
long elapsedTime = System.currentTimeMillis() - startTime;
if (logger.isInfoEnabled()) {
logger.info("Done executing SQL script from " + resource + " in " + elapsedTime + " ms.");
} finally {
JdbcUtils.closeStatement(stmt);
}
}
catch (IOException ex) {
throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex);
long elapsedTime = System.currentTimeMillis() - startTime;
if (logger.isInfoEnabled()) {
logger.info("Done executing SQL script from " + resource + " in " + elapsedTime + " ms.");
}
}
// From JdbcTestUtils - TODO address duplication - these do not seem as useful as the one above
/**
* Read a script from the LineNumberReader and build a String containing the lines.
@@ -131,8 +126,9 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* @return <code>String</code> containing the script lines
* @throws IOException
*/
private static String readScript(LineNumberReader lineNumberReader) throws IOException {
String currentStatement = lineNumberReader.readLine();
private static String readScript(EncodedResource resource) throws IOException {
LineNumberReader lnr = new LineNumberReader(resource.getReader());
String currentStatement = lnr.readLine();
StringBuilder scriptBuilder = new StringBuilder();
while (currentStatement != null) {
if (StringUtils.hasText(currentStatement)) {
@@ -141,7 +137,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
}
scriptBuilder.append(currentStatement);
}
currentStatement = lineNumberReader.readLine();
currentStatement = lnr.readLine();
}
return scriptBuilder.toString();
}
@@ -166,8 +162,8 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
}
/**
* Split an SQL script into separate statements delimited with the provided delimiter character. Each
* individual statement will be added to the provided <code>List</code>.
* Split an SQL script into separate statements delimited with the provided delimiter character. Each individual
* statement will be added to the provided <code>List</code>.
* @param script the SQL script
* @param delim charecter delimiting each statement - typically a ';' character
* @param statements the List that will contain the individual statements
@@ -185,8 +181,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
statements.add(sb.toString());
sb = new StringBuilder();
}
}
else {
} else {
sb.append(content[i]);
}
}