added DataSourceFactory strategy; promoted EmbeddedDatabaseConfigurer strategy to public API; added ability to add any number of SQL scripts for db population

This commit is contained in:
Keith Donald 2009-04-22 19:31:46 +00:00
parent d34a2c5d02
commit ec463a32ba
13 changed files with 215 additions and 113 deletions

View File

@ -0,0 +1,48 @@
/*
* Copyright 2002-2009 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
*
* http://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 org.springframework.jdbc.datasource.embedded;
/**
* Allows DataSource connection properties to be configured in a DataSource implementation independent manner.
* @author Keith Donald
* @see DataSourceFactory
*/
public interface ConnectionProperties {
/**
* Set the JDBC driver to use to connect to the database.
* @param driverClass the jdbc driver class
*/
void setDriverClass(Class<?> driverClass);
/**
* Sets the JDBC connection URL of the database.
* @param url the connection url
*/
void setUrl(String url);
/**
* Sets the username to use to connect to the database.
* @param username the username
*/
void setUsername(String username);
/**
* Sets the password to use to connect to the database.
* @param password the password
*/
void setPassword(String password);
}

View File

@ -0,0 +1,39 @@
/*
* Copyright 2002-2009 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
*
* http://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 org.springframework.jdbc.datasource.embedded;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
/**
* A factory for a kind of DataSource, such as a {@link SimpleDriverDataSource} or connection pool such as Apache DBCP or c3p0.
* Call {@link #getConnectionProperties()} to configure normalized DataSource properties before calling {@link #getDataSource()} to actually get the configured DataSource instance.
* @author Keith Donald
*/
public interface DataSourceFactory {
/**
* Allows properties of the DataSource to be configured.
*/
ConnectionProperties getConnectionProperties();
/**
* Returns the DataSource with the connection properties applied.
*/
DataSource getDataSource();
}

View File

@ -20,7 +20,7 @@ import org.springframework.jdbc.core.JdbcTemplate;
/**
* Strategy for populating a database with data.
*
* @author Keith Donald
* @see ResourceDatabasePopulator
*/
public interface DatabasePopulator {

View File

@ -21,11 +21,12 @@ import javax.sql.DataSource;
* A handle to an EmbeddedDatabase instance.
* Is a {@link DataSource}.
* Adds a shutdown operation so the embedded database instance can be shutdown.
* @author Keith Donald
*/
public interface EmbeddedDatabase extends DataSource {
/**
* Shutdown this embedded database.
*/
public void shutdown();
void shutdown();
}

View File

@ -28,9 +28,6 @@ import org.springframework.core.io.ResourceLoader;
* EmbeddedDatabase db = builder.schema("schema.sql").testData("test-data.sql").build();
* db.shutdown();
* </pre>
*
* TODO - should we replace schema/testdata with more general 'script' method?
*
* @author Keith Donald
*/
public class EmbeddedDatabaseBuilder {
@ -71,24 +68,12 @@ public class EmbeddedDatabaseBuilder {
}
/**
* Sets the location of the schema SQL to run to create the database structure.
* Defaults to classpath:schema.sql if not called.
* Adds a SQL script to execute to populate the database.
* @param sqlResource the sql resource location
* @return this, for fluent call chaining
*/
public EmbeddedDatabaseBuilder schema(String sqlResource) {
databasePopulator.setSchemaLocation(resourceLoader.getResource(sqlResource));
return this;
}
/**
* Sets the location of the schema SQL to run to create the database structure.
* Defaults to classpath:test-data.sql if not called
* @param sqlResource the sql resource location
* @return this, for fluent call chaining
*/
public EmbeddedDatabaseBuilder testData(String sqlResource) {
databasePopulator.setTestDataLocation(resourceLoader.getResource(sqlResource));
public EmbeddedDatabaseBuilder script(String sqlResource) {
databasePopulator.addScript(resourceLoader.getResource(sqlResource));
return this;
}
@ -120,11 +105,11 @@ public class EmbeddedDatabaseBuilder {
/**
* Factory method that builds a default EmbeddedDatabase instance.
* The default is HSQL with a schema created from classpath:schema.sql and test-data loaded from classpatH:test-data.sql.
* The default is HSQL with a schema created from classpath:schema.sql and test-data loaded from classpath:test-data.sql.
* @return an embedded database
*/
public static EmbeddedDatabase buildDefault() {
return new EmbeddedDatabaseBuilder().build();
return new EmbeddedDatabaseBuilder().script("schema.sql").script("test-data.sql").build();
}
private EmbeddedDatabaseBuilder(ResourceLoader loader) {

View File

@ -17,29 +17,24 @@ package org.springframework.jdbc.datasource.embedded;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
/**
* Encapsulates the configuration required to create, connect to, and shutdown a specific type of embedded database such as HSQLdb or H2.
* Create a subclass for each database type we wish to support.
*
* TODO - introduce ConfigurableDataSource strategy for configuring connection properties?
*
* @see EmbeddedDatabaseConfigurerFactory
* Create a implementation for each database type we wish to support.
* @author Keith Donald
*/
abstract class EmbeddedDatabaseConfigurer {
public interface EmbeddedDatabaseConfigurer {
/**
* Configure the properties required to create and connect to the embedded database instance.
* @param dataSource the data source to configure
* @param databaseName the name of the test database
*/
public abstract void configureConnectionProperties(SimpleDriverDataSource dataSource, String databaseName);
void configureConnectionProperties(ConnectionProperties properties, String databaseName);
/**
* Shutdown the embedded database instance that backs dataSource.
* @param dataSource the data source
*/
public abstract void shutdown(DataSource dataSource);
void shutdown(DataSource dataSource);
}

View File

@ -15,9 +15,16 @@
*/
package org.springframework.jdbc.datasource.embedded;
import org.springframework.util.Assert;
/**
* Package private factory for mapping well-known embedded database types to database configurer strategies.
* @author Keith Donald
*/
class EmbeddedDatabaseConfigurerFactory {
public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) throws IllegalStateException {
Assert.notNull(type, "The EmbeddedDatabaseType is required");
try {
if (type == EmbeddedDatabaseType.HSQL) {
return HsqlEmbeddedDatabaseConfigurer.getInstance();

View File

@ -25,7 +25,6 @@ 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.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
@ -36,10 +35,13 @@ import org.springframework.util.Assert;
* When the database is returned, callers are guaranteed that the database schema and test data will have already been loaded.
*
* Can be configured.
* Call {@link #setDatabaseName(String)} to change the name of the test database.
* Call {@link #setDatabaseType(EmbeddedDatabaseType)} to set the test database type.
* 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 #getDatabase()} to the {@link EmbeddedDatabase} instance.
* 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 {
@ -47,19 +49,22 @@ public class EmbeddedDatabaseFactory {
private String databaseName;
private EmbeddedDatabaseConfigurer dataSourceConfigurer;
private DataSourceFactory dataSourceFactory;
private EmbeddedDatabaseConfigurer databaseConfigurer;
private DatabasePopulator databasePopulator;
private EmbeddedDatabase database;
/**
* Creates a new EmbeddedDatabaseFactory that uses the default {@link ResourceDatabasePopulator}.
* Creates a default {@link EmbeddedDatabaseFactory}.
* Calling {@link #getDatabase()} will create a embedded HSQL database of name 'testdb'.
*/
public EmbeddedDatabaseFactory() {
setDatabaseName("testdb");
setDatabaseType(EmbeddedDatabaseType.HSQL);
setDatabasePopulator(new ResourceDatabasePopulator());
setDataSourceFactory(new SimpleDriverDataSourceFactory());
}
/**
@ -73,25 +78,44 @@ public class EmbeddedDatabaseFactory {
}
/**
* Sets the type of test database to use.
* 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
*/
public void setDatabaseType(EmbeddedDatabaseType type) {
Assert.notNull(type, "The TestDatabaseType is required");
dataSourceConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(type);
setDatabaseConfigurer(EmbeddedDatabaseConfigurerFactory.getConfigurer(type));
}
/**
* Sets the helper that will be invoked to populate the test database with data.
* Defaults a {@link ResourceDatabasePopulator}.
* @param populator
* 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
*/
public void setDatabasePopulator(DatabasePopulator populator) {
Assert.notNull(populator, "The TestDatabasePopulator is required");
databasePopulator = populator;
}
/**
* 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) {
Assert.notNull(dataSourceFactory, "The DataSourceFactory is required");
this.dataSourceFactory = dataSourceFactory;
}
// other public methods
/**
@ -106,9 +130,8 @@ public class EmbeddedDatabaseFactory {
// internal helper methods
// encapsulates the steps involved in initializing the data source: creating it, and populating it
private void initDatabase() {
// create the in-memory database source first
// create the embedded database source first
database = new EmbeddedDataSourceProxy(createDataSource());
if (logger.isInfoEnabled()) {
logger.info("Created embedded database '" + databaseName + "'");
@ -120,9 +143,8 @@ public class EmbeddedDatabaseFactory {
}
private DataSource createDataSource() {
SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
dataSourceConfigurer.configureConnectionProperties(dataSource, databaseName);
return dataSource;
databaseConfigurer.configureConnectionProperties(dataSourceFactory.getConnectionProperties(), databaseName);
return dataSourceFactory.getDataSource();
}
private void populateDatabase() {
@ -183,7 +205,7 @@ public class EmbeddedDatabaseFactory {
private void shutdownDatabase() {
if (database != null) {
dataSourceConfigurer.shutdown(database);
databaseConfigurer.shutdown(database);
database = null;
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.jdbc.datasource.embedded;
/**
* A supported embedded database type.
* @author Keith Donald
* @see HsqlEmbeddedDatabaseConfigurer
*/
public enum EmbeddedDatabaseType {
HSQL;

View File

@ -18,10 +18,9 @@ package org.springframework.jdbc.datasource.embedded;
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
import org.springframework.util.ClassUtils;
class HsqlEmbeddedDatabaseConfigurer extends EmbeddedDatabaseConfigurer {
public class HsqlEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigurer {
private static HsqlEmbeddedDatabaseConfigurer INSTANCE;
@ -33,11 +32,11 @@ class HsqlEmbeddedDatabaseConfigurer extends EmbeddedDatabaseConfigurer {
return INSTANCE;
}
public void configureConnectionProperties(SimpleDriverDataSource dataSource, String databaseName) {
dataSource.setDriverClass(org.hsqldb.jdbcDriver.class);
dataSource.setUrl("jdbc:hsqldb:mem:" + databaseName);
dataSource.setUsername("sa");
dataSource.setPassword("");
public void configureConnectionProperties(ConnectionProperties properties, String databaseName) {
properties.setDriverClass(org.hsqldb.jdbcDriver.class);
properties.setUrl("jdbc:hsqldb:mem:" + databaseName);
properties.setUsername("sa");
properties.setPassword("");
}
public void shutdown(DataSource dataSource) {

View File

@ -17,12 +17,12 @@ package org.springframework.jdbc.datasource.embedded;
import java.io.IOException;
import java.io.LineNumberReader;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.EncodedResource;
import org.springframework.dao.DataAccessException;
@ -43,26 +43,16 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
private static final Log logger = LogFactory.getLog(ResourceDatabasePopulator.class);
private Resource schemaLocation = new ClassPathResource("schema.sql");
private Resource testDataLocation = new ClassPathResource("test-data.sql");
private String sqlScriptEncoding;
private List<Resource> scripts = new ArrayList<Resource>();
/**
* Sets the location of .sql file containing the database schema to create.
* @param schemaLocation the path to the db schema definition
* Add a script to execute to populate the database.
* @param script the path to a SQL script
*/
public void setSchemaLocation(Resource schemaLocation) {
this.schemaLocation = schemaLocation;
}
/**
* Sets the location of the .sql file containing the test data to populate.
* @param testDataLocation the path to the db test data file
*/
public void setTestDataLocation(Resource testDataLocation) {
this.testDataLocation = testDataLocation;
public void addScript(Resource script) {
scripts.add(script);
}
/**
@ -73,18 +63,9 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
}
public void populate(JdbcTemplate template) {
createDatabaseSchema(template);
insertTestData(template);
}
// create the application's database schema (tables, indexes, etc.)
private void createDatabaseSchema(JdbcTemplate template) {
executeSqlScript(template, new EncodedResource(schemaLocation, sqlScriptEncoding), false);
}
// populate the tables with test data
private void insertTestData(JdbcTemplate template) {
executeSqlScript(template, new EncodedResource(testDataLocation, sqlScriptEncoding), false);
for (Resource script : scripts) {
executeSqlScript(template, new EncodedResource(script, sqlScriptEncoding), false);
}
}
// From SimpleJdbcTestUtils - TODO address duplication
@ -94,16 +75,13 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* <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 simpleJdbcTemplate the SimpleJdbcTemplate with which to perform JDBC operations
* @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.
* @throws DataAccessException if there is an error executing a statement
* and continueOnError was <code>false</code>
*/
static void executeSqlScript(JdbcTemplate jdbcTemplate,
EncodedResource resource, boolean continueOnError) throws DataAccessException {
static void executeSqlScript(JdbcTemplate template, EncodedResource resource, boolean continueOnError) {
if (logger.isInfoEnabled()) {
logger.info("Executing SQL script from " + resource);
}
@ -119,7 +97,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
splitSqlScript(script, delimiter, statements);
for (String statement : statements) {
try {
int rowsAffected = jdbcTemplate.update(statement);
int rowsAffected = template.update(statement);
if (logger.isDebugEnabled()) {
logger.debug(rowsAffected + " rows affected by SQL: " + statement);
}
@ -145,7 +123,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
}
}
// From JdbcTestUtils - TODO address duplication
// 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.
@ -153,7 +131,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* @return <code>String</code> containing the script lines
* @throws IOException
*/
static String readScript(LineNumberReader lineNumberReader) throws IOException {
private static String readScript(LineNumberReader lineNumberReader) throws IOException {
String currentStatement = lineNumberReader.readLine();
StringBuilder scriptBuilder = new StringBuilder();
while (currentStatement != null) {
@ -173,7 +151,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* @param script the SQL script
* @param delim charecter delimiting each statement - typically a ';' character
*/
static boolean containsSqlScriptDelimiters(String script, char delim) {
private static boolean containsSqlScriptDelimiters(String script, char delim) {
boolean inLiteral = false;
char[] content = script.toCharArray();
for (int i = 0; i < script.length(); i++) {
@ -194,7 +172,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator {
* @param delim charecter delimiting each statement - typically a ';' character
* @param statements the List that will contain the individual statements
*/
static void splitSqlScript(String script, char delim, List<String> statements) {
private static void splitSqlScript(String script, char delim, List<String> statements) {
StringBuilder sb = new StringBuilder();
boolean inLiteral = false;
char[] content = script.toCharArray();

View File

@ -0,0 +1,38 @@
package org.springframework.jdbc.datasource.embedded;
import javax.sql.DataSource;
import org.springframework.jdbc.datasource.SimpleDriverDataSource;
public class SimpleDriverDataSourceFactory implements DataSourceFactory {
private SimpleDriverDataSource dataSource = new SimpleDriverDataSource();
public ConnectionProperties getConnectionProperties() {
return new ConnectionProperties() {
public void setDriverClass(Class<?> driverClass) {
dataSource.setDriverClass(driverClass);
}
public void setUrl(String url) {
dataSource.setUrl(url);
}
public void setUsername(String username) {
dataSource.setUsername(username);
}
public void setPassword(String password) {
dataSource.setPassword(password);
}
};
}
public DataSource getDataSource() {
return dataSource;
}
}

View File

@ -22,31 +22,20 @@ public class EmbeddedDatabaseBuilderTests {
@Test
public void testBuild() {
EmbeddedDatabaseBuilder builder = EmbeddedDatabaseBuilder.relativeTo(getClass());
EmbeddedDatabase db = builder.schema("db-schema.sql").testData("db-test-data.sql").build();
EmbeddedDatabase db = builder.script("db-schema.sql").script("db-test-data.sql").build();
JdbcTemplate template = new JdbcTemplate(db);
assertEquals("Keith", template.queryForObject("select NAME from T_TEST", String.class));
db.shutdown();
}
@Test
public void testBuildNoSuchSchema() {
public void testBuildNoSuchScript() {
try {
new EmbeddedDatabaseBuilder().schema("bogus.sql").build();
fail("Should have failed");
} catch (DataAccessException e) {
}
}
@Test
public void testBuildNoSuchTestdata() {
try {
new EmbeddedDatabaseBuilder().testData("bogus.sql").build();
new EmbeddedDatabaseBuilder().script("bogus.sql").build();
fail("Should have failed");
} catch (DataAccessException e) {
}
}
}
}