diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java index 5093a52732b..5f7abbe080f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/EmbeddedDatabaseBeanDefinitionParser.java @@ -48,10 +48,16 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser */ static final String DB_NAME_ATTRIBUTE = "database-name"; + /** + * Constant for the "generate-name" attribute. + */ + static final String GENERATE_NAME_ATTRIBUTE = "generate-name"; + @Override protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(EmbeddedDatabaseFactoryBean.class); + setGenerateUniqueDatabaseNameFlag(element, builder); setDatabaseName(element, builder); setDatabaseType(element, builder); DatabasePopulatorConfigUtils.setDatabasePopulator(element, builder); @@ -64,6 +70,13 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser return true; } + private void setGenerateUniqueDatabaseNameFlag(Element element, BeanDefinitionBuilder builder) { + String generateName = element.getAttribute(GENERATE_NAME_ATTRIBUTE); + if (StringUtils.hasText(generateName)) { + builder.addPropertyValue("generateUniqueDatabaseName", generateName); + } + } + private void setDatabaseName(Element element, BeanDefinitionBuilder builder) { // 1) Check for an explicit database name String name = element.getAttribute(DB_NAME_ATTRIBUTE); @@ -76,8 +89,7 @@ class EmbeddedDatabaseBeanDefinitionParser extends AbstractBeanDefinitionParser if (StringUtils.hasText(name)) { builder.addPropertyValue("databaseName", name); } - - // 3) Let EmbeddedDatabaseFactory set the default "testdb" name + // else, let EmbeddedDatabaseFactory use the default "testdb" name } private void setDatabaseType(Element element, BeanDefinitionBuilder builder) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java index 6c5ad88f826..e0c340fedaf 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabase.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -23,7 +23,7 @@ import javax.sql.DataSource; * *

An {@code EmbeddedDatabase} is also a {@link DataSource} and adds a * {@link #shutdown} operation so that the embedded database instance can be - * shutdown. + * shut down gracefully. * * @author Keith Donald * @author Sam Brannen @@ -32,7 +32,7 @@ import javax.sql.DataSource; public interface EmbeddedDatabase extends DataSource { /** - * Shutdown this embedded database. + * Shut down this embedded database. */ void shutdown(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java index 8e94ae3b521..13f66dcd5b1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -78,12 +78,32 @@ public class EmbeddedDatabaseBuilder { this.resourceLoader = resourceLoader; } + /** + * Specify whether a unique ID should be generated and used as the database name. + *

If the configuration for this builder is reused across multiple + * application contexts within a single JVM, this flag should be enabled + * (i.e., set to {@code true}) in order to ensure that each application context + * gets its own embedded database. + *

Enabling this flag overrides any explicit name set via {@link #setName}. + * @param flag {@code true} if a unique database name should be generated + * @return {@code this}, to facilitate method chaining + * @see #setName + * @since 4.2 + */ + public EmbeddedDatabaseBuilder generateUniqueName(boolean flag) { + this.databaseFactory.setGenerateUniqueDatabaseName(flag); + return this; + } + /** * Set the name of the embedded database. *

Defaults to {@link EmbeddedDatabaseFactory#DEFAULT_DATABASE_NAME} if * not called. + *

Will be overridden if the {@code generateUniqueName} flag has been + * set to {@code true}. * @param databaseName the name of the embedded database to build * @return {@code this}, to facilitate method chaining + * @see #generateUniqueName */ public EmbeddedDatabaseBuilder setName(String databaseName) { this.databaseFactory.setDatabaseName(databaseName); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index 9ce946ee76e..0b4f43c0356 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * Copyright 2002-2015 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. @@ -19,7 +19,9 @@ package org.springframework.jdbc.datasource.embedded; import java.io.PrintWriter; import java.sql.Connection; import java.sql.SQLException; +import java.util.UUID; import java.util.logging.Logger; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -30,25 +32,28 @@ import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; import org.springframework.util.Assert; /** - * Factory for creating {@link EmbeddedDatabase} instances. + * Factory for creating an {@link EmbeddedDatabase} instance. * - *

Callers are guaranteed that a returned database has been fully initialized - * and populated. + *

Callers are guaranteed that the returned database has been fully + * initialized and populated. * - *

Can be configured: + *

The factory can be configured as follows: *

* - *

Call {@link #getDatabase()} to get the {@link EmbeddedDatabase} instance. + *

After configuring the factory, call {@link #getDatabase()} to obtain + * a reference to the {@link EmbeddedDatabase} instance. * * @author Keith Donald * @author Juergen Hoeller @@ -62,9 +67,10 @@ public class EmbeddedDatabaseFactory { */ public static final String DEFAULT_DATABASE_NAME = "testdb"; - private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class); + private boolean generateUniqueDatabaseName = false; + private String databaseName = DEFAULT_DATABASE_NAME; private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory(); @@ -76,10 +82,25 @@ public class EmbeddedDatabaseFactory { private DataSource dataSource; + /** + * Set the {@code generateUniqueDatabaseName} flag to enable or disable + * generation of a pseudo-random unique ID to be used as the database name. + *

Setting this flag to {@code true} overrides any explicit name set + * via {@link #setDatabaseName}. + * @see #setDatabaseName + * @since 4.2 + */ + public void setGenerateUniqueDatabaseName(boolean generateUniqueDatabaseName) { + this.generateUniqueDatabaseName = generateUniqueDatabaseName; + } + /** * Set the name of the database. *

Defaults to {@value #DEFAULT_DATABASE_NAME}. + *

Will be overridden if the {@code generateUniqueDatabaseName} flag + * has been set to {@code true}. * @param databaseName name of the embedded database + * @see #setGenerateUniqueDatabaseName */ public void setDatabaseName(String databaseName) { Assert.hasText(databaseName, "Database name is required"); @@ -124,7 +145,7 @@ public class EmbeddedDatabaseFactory { } /** - * Factory method that returns the {@link EmbeddedDatabase embedded database} + * Factory method that returns the {@linkplain EmbeddedDatabase embedded database} * instance, which is also a {@link DataSource}. */ public EmbeddedDatabase getDatabase() { @@ -136,12 +157,20 @@ public class EmbeddedDatabaseFactory { /** - * Hook to initialize the embedded database. Subclasses may call this method - * to force initialization. + * Hook to initialize the embedded database. + *

If the {@code generateUniqueDatabaseName} flag has been set to {@code true}, + * the current value of the {@linkplain #setDatabaseName database name} will + * be overridden with an auto-generated name. + *

Subclasses may call this method to force initialization; however, + * this method should only be invoked once. *

After calling this method, {@link #getDataSource()} returns the * {@link DataSource} providing connectivity to the database. */ protected void initDatabase() { + if (this.generateUniqueDatabaseName) { + setDatabaseName(UUID.randomUUID().toString()); + } + // Create the embedded database source first if (logger.isInfoEnabled()) { logger.info("Creating embedded database '" + this.databaseName + "'"); diff --git a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd index c307361c686..c5f03511f4f 100644 --- a/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd +++ b/spring-jdbc/src/main/resources/org/springframework/jdbc/config/spring-jdbc-4.2.xsd @@ -44,6 +44,17 @@ ]]> + + + + If set to "true", a pseudo-random unique name will be generated for the embedded + database, overriding any implicit name provided via the 'id' attribute or any + explicit name provided via the 'database-name' attribute. + Note that this is not the bean name but rather the name of the embedded database + as used in the JDBC connection URL for the database. + + + url.endsWith(DEFAULT_DATABASE_NAME)); } @Test public void createWithImplicitDatabaseName() throws Exception { - assertCorrectSetupForSingleDataSource("jdbc-config-db-name-implicit.xml", "dataSource"); + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-implicit.xml", (url) -> url.endsWith("dataSource")); } @Test public void createWithExplicitDatabaseName() throws Exception { - assertCorrectSetupForSingleDataSource("jdbc-config-db-name-explicit.xml", "customDbName"); + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-explicit.xml", (url) -> url.endsWith("customDbName")); + } + + @Test + public void createWithGeneratedDatabaseName() throws Exception { + Predicate urlPredicate = (url) -> url.startsWith("jdbc:hsqldb:mem:"); + urlPredicate.and((url) -> !url.endsWith("dataSource")); + urlPredicate.and((url) -> !url.endsWith("shouldBeOverriddenByGeneratedName")); + + assertCorrectSetupForSingleDataSource("jdbc-config-db-name-generated.xml", urlPredicate); } @Test @@ -189,14 +200,14 @@ public class JdbcNamespaceIntegrationTests { } } - private void assertCorrectSetupForSingleDataSource(String file, String dbName) { + private void assertCorrectSetupForSingleDataSource(String file, Predicate urlPredicate) { ConfigurableApplicationContext context = context(file); try { DataSource dataSource = context.getBean(DataSource.class); assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1); assertTrue(dataSource instanceof AbstractDriverBasedDataSource); AbstractDriverBasedDataSource adbDataSource = (AbstractDriverBasedDataSource) dataSource; - assertThat(adbDataSource.getUrl(), containsString(dbName)); + assertTrue(urlPredicate.test(adbDataSource.getUrl())); } finally { context.close(); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java index 3d4300648ed..3c0dea20d6f 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseBuilderTests.java @@ -17,10 +17,10 @@ package org.springframework.jdbc.datasource.embedded; import org.junit.Test; - import org.springframework.core.io.ClassRelativeResourceLoader; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.init.CannotReadScriptException; +import org.springframework.jdbc.datasource.init.ScriptStatementFailedException; import static org.junit.Assert.*; import static org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.*; @@ -160,14 +160,62 @@ public class EmbeddedDatabaseBuilderTests { }); } + @Test + public void createSameSchemaTwiceWithoutUniqueDbNames() throws Exception { + EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql").build(); + + try { + new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql").build(); + + fail("Should have thrown a ScriptStatementFailedException"); + } + catch (ScriptStatementFailedException e) { + // expected + } + finally { + db1.shutdown(); + } + } + + @Test + public void createSameSchemaTwiceWithGeneratedUniqueDbNames() throws Exception { + EmbeddedDatabase db1 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// + .generateUniqueName(true)// + .build(); + + JdbcTemplate template1 = new JdbcTemplate(db1); + assertNumRowsInTestTable(template1, 1); + template1.update("insert into T_TEST (NAME) values ('Sam')"); + assertNumRowsInTestTable(template1, 2); + + EmbeddedDatabase db2 = new EmbeddedDatabaseBuilder(new ClassRelativeResourceLoader(getClass()))// + .addScripts("db-schema-without-dropping.sql", "db-test-data.sql")// + .generateUniqueName(true)// + .build(); + assertDatabaseCreated(db2); + + db1.shutdown(); + db2.shutdown(); + } + private void doTwice(Runnable test) { test.run(); test.run(); } + private void assertNumRowsInTestTable(JdbcTemplate template, int count) { + assertEquals(count, template.queryForObject("select count(*) from T_TEST", Integer.class).intValue()); + } + + private void assertDatabaseCreated(EmbeddedDatabase db) { + assertNumRowsInTestTable(new JdbcTemplate(db), 1); + } + private void assertDatabaseCreatedAndShutdown(EmbeddedDatabase db) { - JdbcTemplate template = new JdbcTemplate(db); - assertEquals("Keith", template.queryForObject("select NAME from T_TEST", String.class)); + assertDatabaseCreated(db); db.shutdown(); } diff --git a/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml new file mode 100644 index 00000000000..7a146db8797 --- /dev/null +++ b/spring-jdbc/src/test/resources/org/springframework/jdbc/config/jdbc-config-db-name-generated.xml @@ -0,0 +1,12 @@ + + + + +