package accounts.testdb; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.StringWriter; 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.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.DriverManagerDataSource; /** * A factory that creates a data source fit for use in a system test environment. Creates a simple data source that * connects to an in-memory database pre-loaded with test data. * * This factory returns a fully-initialized DataSource implementation. When the DataSource is returned, callers are * guaranteed that the database schema and test data will have been loaded by that time. * * Is a FactoryBean, for exposing the fully-initialized test DataSource as a Spring bean. See {@link #getObject()}. * * Is an InitializingBean, for receiving an initialization callback when deployed as a Spring bean. See * {@link #afterPropertiesSet()}. */ @SuppressWarnings("rawtypes") public class TestDataSourceFactory implements FactoryBean, InitializingBean { private static Log logger = LogFactory.getLog(TestDataSourceFactory.class); // configurable properties private String testDatabaseName; private Resource schemaLocation; private Resource testDataLocation; /** * The object created by this factory. */ private DataSource dataSource; /** * Creates a new TestDataSourceFactory for use in "bean" style. "Bean" style means the default constructor is called * and then properties are set to configure this object. "Bean" style usage is nice when this object is defined as a * Spring bean, as setter-injection can be more descriptive than constructor-injection from the point of view of a * bean definition author. * @see {@link #setTestDatabaseName(String)} * @see {@link #setSchemaLocation(Resource)} * @see {@link #setTestDataLocation(Resource)} */ public TestDataSourceFactory() { } /** * Creates a new TestDataSourceFactory fully-initialized with what it needs to work. Fully-formed constructors are * nice in a programmatic environment, as they result in more concise code and allow for a class to enforce its * required properties. * @param testDatabaseName the name of the test database to create * @param schemaLocation the location of the file containing the schema DDL to export to the database * @param testDataLocation the location of the file containing the test data to load into the database */ public TestDataSourceFactory(String testDatabaseName, Resource schemaLocation, Resource testDataLocation) { setTestDatabaseName(testDatabaseName); setSchemaLocation(schemaLocation); setTestDataLocation(testDataLocation); } /** * Sets the name of the test database to create. * @param testDatabaseName the name of the test database, i.e. "rewards" */ public void setTestDatabaseName(String testDatabaseName) { this.testDatabaseName = testDatabaseName; } /** * Sets the location of the file containing the schema DDL to export to the test database. * @param schemaLocation the location of the database schema DDL */ public void setSchemaLocation(Resource schemaLocation) { this.schemaLocation = schemaLocation; } /** * Sets the location of the file containing the test data to load into the database. * @param testDataLocation the location of the test data file */ public void setTestDataLocation(Resource testDataLocation) { this.testDataLocation = testDataLocation; } // implementing InitializingBean // this method is automatically called by Spring after configuration to perform a dependency check and init public void afterPropertiesSet() { if (testDatabaseName == null) { throw new IllegalArgumentException("The name of the test database to create is required"); } if (schemaLocation == null) { throw new IllegalArgumentException("The path to the database schema DDL is required"); } if (testDataLocation == null) { throw new IllegalArgumentException("The path to the test data set is required"); } initDataSource(); } // implementing FactoryBean // this method is automatically called by Spring to expose the DataSource as a bean public Object getObject() throws Exception { return getDataSource(); } public Class getObjectType() { return DataSource.class; } public boolean isSingleton() { return true; } // other methods /** * Factory method that returns the fully-initialized test data source. Useful when this class is used * programatically instead of deployed as a Spring bean. * @return the data source */ public DataSource getDataSource() { if (dataSource == null) { initDataSource(); } return dataSource; } // static factory methods /** * Static factory method that creates a DataSource that connects to a test database populated with test data. * @param testDatabaseName the name of the test database to create * @param schemaLocation the database schema to export * @param testDataLocation the database test data to load * @return the data source */ public static DataSource createDataSource(String testDatabaseName, Resource schemaLocation, Resource testDataLocation) { return new TestDataSourceFactory(testDatabaseName, schemaLocation, testDataLocation).getDataSource(); } // internal helper methods // encapsulates the steps involved in initializing the data source: creating it, and populating it private void initDataSource() { // create the in-memory database source first this.dataSource = createDataSource(); if (logger.isDebugEnabled()) { logger.debug("Created in-memory test database '" + testDatabaseName + "'"); } // now populate the database by loading the schema and test data populateDataSource(); if (logger.isDebugEnabled()) { logger.debug("Exported schema in " + schemaLocation); } if (logger.isDebugEnabled()) { logger.debug("Loaded test data in " + testDataLocation); } } private DataSource createDataSource() { DriverManagerDataSource dataSource = new DriverManagerDataSource(); // use the HsqlDB JDBC driver dataSource.setDriverClassName("org.hsqldb.jdbcDriver"); // have it create an in-memory database dataSource.setUrl("jdbc:hsqldb:mem:" + testDatabaseName); dataSource.setUsername("sa"); dataSource.setPassword(""); return dataSource; } private void populateDataSource() { TestDatabasePopulator populator = new TestDatabasePopulator(dataSource); populator.populate(); } /** * Populates a in memory data source with test data. */ private class TestDatabasePopulator { private DataSource dataSource; /** * Creates a new test database populator. * @param dataSource the test data source that will be populated. */ public TestDatabasePopulator(DataSource dataSource) { this.dataSource = dataSource; } /** * Populate the test database by creating the database schema from 'schema.sql' and inserting the test data in * 'testdata.sql'. */ public void populate() { Connection connection = null; try { connection = dataSource.getConnection(); createDatabaseSchema(connection); insertTestData(connection); } catch (SQLException e) { throw new RuntimeException("SQL exception occurred acquiring connection", e); } finally { if (connection != null) { try { connection.close(); } catch (SQLException e) { } } } } // create the application's database schema (tables, indexes, etc.) private void createDatabaseSchema(Connection connection) { try { String sql = parseSqlIn(schemaLocation); executeSql(sql, connection); } catch (IOException e) { throw new RuntimeException("I/O exception occurred accessing the database schema file", e); } catch (SQLException e) { throw new RuntimeException("SQL exception occurred exporting database schema", e); } } // populate the tables with test data private void insertTestData(Connection connection) { try { String sql = parseSqlIn(testDataLocation); executeSql(sql, connection); } catch (IOException e) { throw new RuntimeException("I/O exception occurred accessing the test data file", e); } catch (SQLException e) { throw new RuntimeException("SQL exception occurred loading test data", e); } } // utility method to read a .sql txt input stream private String parseSqlIn(Resource resource) throws IOException { InputStream is = null; try { is = resource.getInputStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is)); StringWriter sw = new StringWriter(); BufferedWriter writer = new BufferedWriter(sw); for (int c=reader.read(); c != -1; c=reader.read()) { writer.write(c); } writer.flush(); return sw.toString(); } finally { if (is != null) { is.close(); } } } // utility method to run the parsed sql private void executeSql(String sql, Connection connection) throws SQLException { Statement statement = connection.createStatement(); statement.execute(sql); } } }