From 2e45a19be3258d6f41312be96d7422ebe5389fe0 Mon Sep 17 00:00:00 2001 From: Keith Donald Date: Wed, 22 Apr 2009 17:07:20 +0000 Subject: [PATCH] embedded database support initial commit; moved from spring-test; ResourceDatabasePopulator duplicate code with test.jdbc package that needs review --- org.springframework.jdbc/.classpath | 1 + org.springframework.jdbc/ivy.xml | 1 + .../embedded/DatabasePopulator.java | 34 +++ .../datasource/embedded/EmbeddedDatabase.java | 31 +++ .../embedded/EmbeddedDatabaseBuilder.java | 139 +++++++++++ .../embedded/EmbeddedDatabaseConfigurer.java | 43 ++++ .../EmbeddedDatabaseConfigurerFactory.java | 32 +++ .../embedded/EmbeddedDatabaseFactory.java | 191 +++++++++++++++ .../embedded/EmbeddedDatabaseType.java | 24 ++ .../HsqlEmbeddedDatabaseConfigurer.java | 47 ++++ .../embedded/ResourceDatabasePopulator.java | 220 ++++++++++++++++++ .../EmbeddedDatabaseBuilderTests.java | 52 +++++ .../EmbeddedDatabaseFactoryTests.java | 34 +++ .../jdbc/datasource/embedded/db-schema.sql | 3 + .../jdbc/datasource/embedded/db-test-data.sql | 1 + .../src/test/java/schema.sql | 3 + .../src/test/java/test-data.sql | 1 + 17 files changed, 857 insertions(+) create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DatabasePopulator.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java create mode 100644 org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ResourceDatabasePopulator.java create mode 100644 org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java create mode 100644 org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java create mode 100644 org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-schema.sql create mode 100644 org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-test-data.sql create mode 100644 org.springframework.jdbc/src/test/java/schema.sql create mode 100644 org.springframework.jdbc/src/test/java/test-data.sql diff --git a/org.springframework.jdbc/.classpath b/org.springframework.jdbc/.classpath index 8f39dc5929..0336a77928 100644 --- a/org.springframework.jdbc/.classpath +++ b/org.springframework.jdbc/.classpath @@ -15,5 +15,6 @@ + diff --git a/org.springframework.jdbc/ivy.xml b/org.springframework.jdbc/ivy.xml index a433afa453..16facba880 100644 --- a/org.springframework.jdbc/ivy.xml +++ b/org.springframework.jdbc/ivy.xml @@ -30,6 +30,7 @@ + diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DatabasePopulator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DatabasePopulator.java new file mode 100644 index 0000000000..1b2c6bfbc9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DatabasePopulator.java @@ -0,0 +1,34 @@ +/* + * 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 org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; + +/** + * Strategy for populating a database with data. + * + * @see ResourceDatabasePopulator + */ +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 + */ + void populate(JdbcTemplate template); +} \ No newline at end of file diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java new file mode 100644 index 0000000000..057ef92ec7 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java @@ -0,0 +1,31 @@ +/* + * 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; + +/** + * A handle to an EmbeddedDatabase instance. + * Is a {@link DataSource}. + * Adds a shutdown operation so the embedded database instance can be shutdown. + */ +public interface EmbeddedDatabase extends DataSource { + + /** + * Shutdown this embedded database. + */ + public void shutdown(); +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java new file mode 100644 index 0000000000..ce77dd67f9 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java @@ -0,0 +1,139 @@ +/* + * 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 org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; + +/** + * A builder that provides a fluent API for constructing an embedded database. + * Usage example: + *
+ * EmbeddedDatabaseBuilder builder = new EmbeddedDatabaseBuilder();
+ * EmbeddedDatabase db = builder.schema("schema.sql").testData("test-data.sql").build();
+ * db.shutdown();
+ * 
+ * + * @author Keith Donald + */ +public class EmbeddedDatabaseBuilder { + + private EmbeddedDatabaseFactory databaseFactory; + + private ResourceDatabasePopulator databasePopulator; + + private ResourceLoader resourceLoader; + + /** + * Creates a new embedded database builder. + */ + public EmbeddedDatabaseBuilder() { + init(new DefaultResourceLoader()); + } + + /** + * Sets the name of the embedded database + * Defaults to 'testdb' if not called. + * @param databaseType the embedded database type + * @return this, for fluent call chaining + */ + public EmbeddedDatabaseBuilder name(String databaseName) { + databaseFactory.setDatabaseName(databaseName); + return this; + } + + /** + * Sets the type of embedded database. + * Defaults to HSQL if not called. + * @param databaseType the embedded database type + * @return this, for fluent call chaining + */ + public EmbeddedDatabaseBuilder type(EmbeddedDatabaseType databaseType) { + databaseFactory.setDatabaseType(databaseType); + return this; + } + + /** + * Sets the location of the schema SQL to run to create the database structure. + * Defaults to classpath:schema.sql if not called. + * @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)); + return this; + } + + /** + * Build the embedded database. + * @return the embedded database + */ + public EmbeddedDatabase build() { + return databaseFactory.getDatabase(); + } + + /** + * Factory method that creates a embedded database builder that loads resources relative to the provided class. + * @param clazz the class to load relative to + * @return the embedded database builder + */ + public static EmbeddedDatabaseBuilder relativeTo(final Class clazz) { + ResourceLoader loader = new ResourceLoader() { + public ClassLoader getClassLoader() { + return getClass().getClassLoader(); + } + + public Resource getResource(String location) { + return new ClassPathResource(location, clazz); + } + }; + return new EmbeddedDatabaseBuilder(loader); + } + + /** + * 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. + * @return an embedded database + */ + public static EmbeddedDatabase buildDefault() { + return new EmbeddedDatabaseBuilder().build(); + } + + private EmbeddedDatabaseBuilder(ResourceLoader loader) { + init(loader); + } + + private void init(ResourceLoader loader) { + databaseFactory = new EmbeddedDatabaseFactory(); + databasePopulator = new ResourceDatabasePopulator(); + databaseFactory.setDatabasePopulator(databasePopulator); + resourceLoader = loader; + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000000..534277a864 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurer.java @@ -0,0 +1,43 @@ +/* + * 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; + +/** + * 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. + * + * @see EmbeddedDatabaseConfigurerFactory + */ +abstract class 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); + + /** + * Shutdown the embedded database instance that backs dataSource. + * @param dataSource the data source + */ + public abstract void shutdown(DataSource dataSource); + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java new file mode 100644 index 0000000000..57d51a94d4 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseConfigurerFactory.java @@ -0,0 +1,32 @@ +/* + * 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; + +class EmbeddedDatabaseConfigurerFactory { + + public static EmbeddedDatabaseConfigurer getConfigurer(EmbeddedDatabaseType type) throws IllegalStateException { + try { + if (type == EmbeddedDatabaseType.HSQL) { + return HsqlEmbeddedDatabaseConfigurer.getInstance(); + } else { + throw new UnsupportedOperationException("Other types not yet supported"); + } + } catch (ClassNotFoundException e) { + throw new IllegalStateException("Drivers for test database type [" + type + "] are not available in the classpath", e); + } + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java new file mode 100644 index 0000000000..b52700178b --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -0,0 +1,191 @@ +/* + * 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 java.io.PrintWriter; +import java.sql.Connection; +import java.sql.SQLException; + +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.jdbc.datasource.SimpleDriverDataSource; +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. + * + * 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 #setDatabasePopulator(DatabasePopulator)} to change the algorithm used to populate the database. + * Call {@link #getDatabase()} to the {@link EmbeddedDatabase} instance. + */ +public class EmbeddedDatabaseFactory { + + private static Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class); + + private String databaseName; + + private EmbeddedDatabaseConfigurer dataSourceConfigurer; + + private DatabasePopulator databasePopulator; + + private EmbeddedDatabase database; + + /** + * Creates a new EmbeddedDatabaseFactory that uses the default {@link ResourceDatabasePopulator}. + */ + public EmbeddedDatabaseFactory() { + setDatabaseName("testdb"); + setDatabaseType(EmbeddedDatabaseType.HSQL); + setDatabasePopulator(new ResourceDatabasePopulator()); + } + + /** + * Sets the name of the test 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 test database to use. + * 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); + } + + /** + * Sets the helper that will be invoked to populate the test database with data. + * Defaults a {@link ResourceDatabasePopulator}. + * @param populator + */ + public void setDatabasePopulator(DatabasePopulator populator) { + Assert.notNull(populator, "The TestDatabasePopulator is required"); + databasePopulator = populator; + } + + // other public methods + + /** + * Factory method that returns the embedded database instance. + */ + public EmbeddedDatabase getDatabase() { + if (database == null) { + initDatabase(); + } + return database; + } + + // 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 + database = new EmbeddedDataSourceProxy(createDataSource()); + if (logger.isInfoEnabled()) { + logger.info("Created embedded database '" + databaseName + "'"); + } + if (databasePopulator != null) { + // now populate the database + populateDatabase(); + } + } + + private DataSource createDataSource() { + SimpleDriverDataSource dataSource = new SimpleDriverDataSource(); + dataSourceConfigurer.configureConnectionProperties(dataSource, databaseName); + return dataSource; + } + + private void populateDatabase() { + TransactionTemplate template = new TransactionTemplate(new DataSourceTransactionManager(database)); + template.execute(new TransactionCallbackWithoutResult() { + @Override + protected void doInTransactionWithoutResult(TransactionStatus status) { + databasePopulator.populate(new JdbcTemplate(database)); + } + }); + } + + class EmbeddedDataSourceProxy implements EmbeddedDatabase { + private DataSource dataSource; + + public EmbeddedDataSourceProxy(DataSource dataSource) { + this.dataSource = dataSource; + } + + public Connection getConnection() throws SQLException { + return dataSource.getConnection(); + } + + public Connection getConnection(String username, String password) + throws SQLException { + return dataSource.getConnection(username, password); + } + + public int getLoginTimeout() throws SQLException { + return dataSource.getLoginTimeout(); + } + + public PrintWriter getLogWriter() throws SQLException { + return dataSource.getLogWriter(); + } + + public void setLoginTimeout(int seconds) throws SQLException { + dataSource.setLoginTimeout(seconds); + } + + public void setLogWriter(PrintWriter out) throws SQLException { + dataSource.setLogWriter(out); + } + + public boolean isWrapperFor(Class iface) throws SQLException { + return dataSource.isWrapperFor(iface); + } + + public T unwrap(Class iface) throws SQLException { + return dataSource.unwrap(iface); + } + + public void shutdown() { + shutdownDatabase(); + } + + } + + private void shutdownDatabase() { + if (database != null) { + dataSourceConfigurer.shutdown(database); + database = null; + } + } + +} \ No newline at end of file diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java new file mode 100644 index 0000000000..30085eb3eb --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseType.java @@ -0,0 +1,24 @@ +/* + * 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; + +/** + * A supported embedded database type. + * @author Keith Donald + */ +public enum EmbeddedDatabaseType { + HSQL; +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java new file mode 100644 index 0000000000..b5ee37ef39 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java @@ -0,0 +1,47 @@ +/* + * 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.core.JdbcTemplate; +import org.springframework.jdbc.datasource.SimpleDriverDataSource; +import org.springframework.util.ClassUtils; + +class HsqlEmbeddedDatabaseConfigurer extends EmbeddedDatabaseConfigurer { + + private static HsqlEmbeddedDatabaseConfigurer INSTANCE; + + public static synchronized HsqlEmbeddedDatabaseConfigurer getInstance() throws ClassNotFoundException { + if (INSTANCE == null) { + ClassUtils.forName("org.hsqldb.jdbcDriver", HsqlEmbeddedDatabaseConfigurer.class.getClassLoader()); + INSTANCE = new HsqlEmbeddedDatabaseConfigurer(); + } + 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 shutdown(DataSource dataSource) { + new JdbcTemplate(dataSource).execute("SHUTDOWN"); + } + +} diff --git a/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ResourceDatabasePopulator.java b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ResourceDatabasePopulator.java new file mode 100644 index 0000000000..aee842d818 --- /dev/null +++ b/org.springframework.jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/ResourceDatabasePopulator.java @@ -0,0 +1,220 @@ +/* + * 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 java.io.IOException; +import java.io.LineNumberReader; +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; +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. + * + * 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 { + + 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; + + /** + * Sets the location of .sql file containing the database schema to create. + * @param schemaLocation the path to the db schema definition + */ + 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; + } + + /** + * Specify the encoding for SQL scripts, if different from the platform encoding. + */ + public void setSqlScriptEncoding(String sqlScriptEncoding) { + this.sqlScriptEncoding = sqlScriptEncoding; + } + + 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); + } + + // From SimpleJdbcTestUtils - TODO address duplication + + /** + * Execute the given SQL script. + *

The script will normally be loaded by classpath. There should be one statement + * per line. Any semicolons will be removed. Do not use this method to execute + * DDL if you expect rollback. + * @param simpleJdbcTemplate 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 false + */ + static void executeSqlScript(JdbcTemplate jdbcTemplate, + EncodedResource resource, boolean continueOnError) throws DataAccessException { + if (logger.isInfoEnabled()) { + logger.info("Executing SQL script from " + resource); + } + long startTime = System.currentTimeMillis(); + List statements = new LinkedList(); + 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 = jdbcTemplate.update(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; + } + } + } + long elapsedTime = System.currentTimeMillis() - startTime; + if (logger.isInfoEnabled()) { + logger.info("Done executing SQL script from " + resource + " in " + elapsedTime + " ms."); + } + } + catch (IOException ex) { + throw new DataAccessResourceFailureException("Failed to open SQL script from " + resource, ex); + } + } + + // From JdbcTestUtils - TODO address duplication + + /** + * Read a script from the LineNumberReader and build a String containing the lines. + * @param lineNumberReader the LineNumberReader containing the script to be processed + * @return String containing the script lines + * @throws IOException + */ + static String readScript(LineNumberReader lineNumberReader) throws IOException { + String currentStatement = lineNumberReader.readLine(); + StringBuilder scriptBuilder = new StringBuilder(); + while (currentStatement != null) { + if (StringUtils.hasText(currentStatement)) { + if (scriptBuilder.length() > 0) { + scriptBuilder.append('\n'); + } + scriptBuilder.append(currentStatement); + } + currentStatement = lineNumberReader.readLine(); + } + return scriptBuilder.toString(); + } + + /** + * Does the provided SQL script contain the specified delimiter? + * @param script the SQL script + * @param delim charecter delimiting each statement - typically a ';' character + */ + static boolean containsSqlScriptDelimiters(String script, char delim) { + boolean inLiteral = false; + char[] content = script.toCharArray(); + for (int i = 0; i < script.length(); i++) { + if (content[i] == '\'') { + inLiteral = !inLiteral; + } + if (content[i] == delim && !inLiteral) { + return true; + } + } + return false; + } + + /** + * Split an SQL script into separate statements delimited with the provided delimiter character. Each + * individual statement will be added to the provided List. + * @param script the SQL script + * @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 statements) { + StringBuilder sb = new StringBuilder(); + boolean inLiteral = false; + char[] content = script.toCharArray(); + for (int i = 0; i < script.length(); i++) { + if (content[i] == '\'') { + inLiteral = !inLiteral; + } + if (content[i] == delim && !inLiteral) { + if (sb.length() > 0) { + statements.add(sb.toString()); + sb = new StringBuilder(); + } + } + else { + sb.append(content[i]); + } + } + if (sb.length() > 0) { + statements.add(sb.toString()); + } + } + +} \ No newline at end of file diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java new file mode 100644 index 0000000000..e292e46d3c --- /dev/null +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -0,0 +1,52 @@ +package org.springframework.jdbc.datasource.embedded; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.fail; + +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder; + +public class EmbeddedDatabaseBuilderTests { + + @Test + public void testBuildDefaults() { + EmbeddedDatabase db = EmbeddedDatabaseBuilder.buildDefault(); + JdbcTemplate template = new JdbcTemplate(db); + assertEquals("Keith", template.queryForObject("select NAME from T_TEST", String.class)); + db.shutdown(); + } + + @Test + public void testBuild() { + EmbeddedDatabaseBuilder builder = EmbeddedDatabaseBuilder.relativeTo(getClass()); + EmbeddedDatabase db = builder.schema("db-schema.sql").testData("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() { + 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(); + fail("Should have failed"); + } catch (DataAccessException e) { + + } + } + + +} diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java new file mode 100644 index 0000000000..baff4ba25c --- /dev/null +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryTests.java @@ -0,0 +1,34 @@ +package org.springframework.jdbc.datasource.embedded; + +import static org.junit.Assert.assertTrue; + +import org.junit.Test; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.embedded.DatabasePopulator; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabase; +import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory; + +public class EmbeddedDatabaseFactoryTests { + + private EmbeddedDatabaseFactory factory = new EmbeddedDatabaseFactory(); + + @Test + public void testGetDataSource() { + StubDatabasePopulator populator = new StubDatabasePopulator(); + factory.setDatabasePopulator(populator); + EmbeddedDatabase db = factory.getDatabase(); + assertTrue(populator.populateCalled); + db.shutdown(); + } + + private static class StubDatabasePopulator implements DatabasePopulator { + + private boolean populateCalled; + + public void populate(JdbcTemplate template) throws DataAccessException { + this.populateCalled = true; + } + + } +} diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-schema.sql b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-schema.sql new file mode 100644 index 0000000000..73d0feb406 --- /dev/null +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-schema.sql @@ -0,0 +1,3 @@ +drop table T_TEST if exists; + +create table T_TEST (NAME varchar(50) not null); \ No newline at end of file diff --git a/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-test-data.sql b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-test-data.sql new file mode 100644 index 0000000000..51de08aac1 --- /dev/null +++ b/org.springframework.jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/db-test-data.sql @@ -0,0 +1 @@ +insert into T_TEST (NAME) values ('Keith'); \ No newline at end of file diff --git a/org.springframework.jdbc/src/test/java/schema.sql b/org.springframework.jdbc/src/test/java/schema.sql new file mode 100644 index 0000000000..73d0feb406 --- /dev/null +++ b/org.springframework.jdbc/src/test/java/schema.sql @@ -0,0 +1,3 @@ +drop table T_TEST if exists; + +create table T_TEST (NAME varchar(50) not null); \ No newline at end of file diff --git a/org.springframework.jdbc/src/test/java/test-data.sql b/org.springframework.jdbc/src/test/java/test-data.sql new file mode 100644 index 0000000000..51de08aac1 --- /dev/null +++ b/org.springframework.jdbc/src/test/java/test-data.sql @@ -0,0 +1 @@ +insert into T_TEST (NAME) values ('Keith'); \ No newline at end of file -- GitLab