001/*
002 * Copyright 2002-2017 the original author or authors.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *      https://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016
017package org.springframework.jdbc.datasource.embedded;
018
019import java.io.PrintWriter;
020import java.sql.Connection;
021import java.sql.SQLException;
022import java.util.UUID;
023import java.util.logging.Logger;
024import javax.sql.DataSource;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028
029import org.springframework.jdbc.datasource.SimpleDriverDataSource;
030import org.springframework.jdbc.datasource.init.DatabasePopulator;
031import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils;
032import org.springframework.util.Assert;
033
034/**
035 * Factory for creating an {@link EmbeddedDatabase} instance.
036 *
037 * <p>Callers are guaranteed that the returned database has been fully
038 * initialized and populated.
039 *
040 * <p>The factory can be configured as follows:
041 * <ul>
042 * <li>Call {@link #generateUniqueDatabaseName} to set a unique, random name
043 * for the database.
044 * <li>Call {@link #setDatabaseName} to set an explicit name for the database.
045 * <li>Call {@link #setDatabaseType} to set the database type if you wish to
046 * use one of the supported types.
047 * <li>Call {@link #setDatabaseConfigurer} to configure support for a custom
048 * embedded database type.
049 * <li>Call {@link #setDatabasePopulator} to change the algorithm used to
050 * populate the database.
051 * <li>Call {@link #setDataSourceFactory} to change the type of
052 * {@link DataSource} used to connect to the database.
053 * </ul>
054 *
055 * <p>After configuring the factory, call {@link #getDatabase()} to obtain
056 * a reference to the {@link EmbeddedDatabase} instance.
057 *
058 * @author Keith Donald
059 * @author Juergen Hoeller
060 * @author Sam Brannen
061 * @since 3.0
062 */
063public class EmbeddedDatabaseFactory {
064
065        /**
066         * Default name for an embedded database: {@value}
067         */
068        public static final String DEFAULT_DATABASE_NAME = "testdb";
069
070        private static final Log logger = LogFactory.getLog(EmbeddedDatabaseFactory.class);
071
072        private boolean generateUniqueDatabaseName = false;
073
074        private String databaseName = DEFAULT_DATABASE_NAME;
075
076        private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory();
077
078        private EmbeddedDatabaseConfigurer databaseConfigurer;
079
080        private DatabasePopulator databasePopulator;
081
082        private DataSource dataSource;
083
084
085        /**
086         * Set the {@code generateUniqueDatabaseName} flag to enable or disable
087         * generation of a pseudo-random unique ID to be used as the database name.
088         * <p>Setting this flag to {@code true} overrides any explicit name set
089         * via {@link #setDatabaseName}.
090         * @since 4.2
091         * @see #setDatabaseName
092         */
093        public void setGenerateUniqueDatabaseName(boolean generateUniqueDatabaseName) {
094                this.generateUniqueDatabaseName = generateUniqueDatabaseName;
095        }
096
097        /**
098         * Set the name of the database.
099         * <p>Defaults to {@value #DEFAULT_DATABASE_NAME}.
100         * <p>Will be overridden if the {@code generateUniqueDatabaseName} flag
101         * has been set to {@code true}.
102         * @param databaseName name of the embedded database
103         * @see #setGenerateUniqueDatabaseName
104         */
105        public void setDatabaseName(String databaseName) {
106                Assert.hasText(databaseName, "Database name is required");
107                this.databaseName = databaseName;
108        }
109
110        /**
111         * Set the factory to use to create the {@link DataSource} instance that
112         * connects to the embedded database.
113         * <p>Defaults to {@link SimpleDriverDataSourceFactory}.
114         */
115        public void setDataSourceFactory(DataSourceFactory dataSourceFactory) {
116                Assert.notNull(dataSourceFactory, "DataSourceFactory is required");
117                this.dataSourceFactory = dataSourceFactory;
118        }
119
120        /**
121         * Set the type of embedded database to use.
122         * <p>Call this when you wish to configure one of the pre-supported types.
123         * <p>Defaults to HSQL.
124         * @param type the database type
125         */
126        public void setDatabaseType(EmbeddedDatabaseType type) {
127                this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(type);
128        }
129
130        /**
131         * Set the strategy that will be used to configure the embedded database instance.
132         * <p>Call this when you wish to use an embedded database type not already supported.
133         */
134        public void setDatabaseConfigurer(EmbeddedDatabaseConfigurer configurer) {
135                this.databaseConfigurer = configurer;
136        }
137
138        /**
139         * Set the strategy that will be used to initialize or populate the embedded
140         * database.
141         * <p>Defaults to {@code null}.
142         */
143        public void setDatabasePopulator(DatabasePopulator populator) {
144                this.databasePopulator = populator;
145        }
146
147        /**
148         * Factory method that returns the {@linkplain EmbeddedDatabase embedded database}
149         * instance, which is also a {@link DataSource}.
150         */
151        public EmbeddedDatabase getDatabase() {
152                if (this.dataSource == null) {
153                        initDatabase();
154                }
155                return new EmbeddedDataSourceProxy(this.dataSource);
156        }
157
158
159        /**
160         * Hook to initialize the embedded database.
161         * <p>If the {@code generateUniqueDatabaseName} flag has been set to {@code true},
162         * the current value of the {@linkplain #setDatabaseName database name} will
163         * be overridden with an auto-generated name.
164         * <p>Subclasses may call this method to force initialization; however,
165         * this method should only be invoked once.
166         * <p>After calling this method, {@link #getDataSource()} returns the
167         * {@link DataSource} providing connectivity to the database.
168         */
169        protected void initDatabase() {
170                if (this.generateUniqueDatabaseName) {
171                        setDatabaseName(UUID.randomUUID().toString());
172                }
173
174                // Create the embedded database first
175                if (this.databaseConfigurer == null) {
176                        this.databaseConfigurer = EmbeddedDatabaseConfigurerFactory.getConfigurer(EmbeddedDatabaseType.HSQL);
177                }
178                this.databaseConfigurer.configureConnectionProperties(
179                                this.dataSourceFactory.getConnectionProperties(), this.databaseName);
180                this.dataSource = this.dataSourceFactory.getDataSource();
181
182                if (logger.isInfoEnabled()) {
183                        if (this.dataSource instanceof SimpleDriverDataSource) {
184                                SimpleDriverDataSource simpleDriverDataSource = (SimpleDriverDataSource) this.dataSource;
185                                logger.info(String.format("Starting embedded database: url='%s', username='%s'",
186                                                simpleDriverDataSource.getUrl(), simpleDriverDataSource.getUsername()));
187                        }
188                        else {
189                                logger.info(String.format("Starting embedded database '%s'", this.databaseName));
190                        }
191                }
192
193                // Now populate the database
194                if (this.databasePopulator != null) {
195                        try {
196                                DatabasePopulatorUtils.execute(this.databasePopulator, this.dataSource);
197                        }
198                        catch (RuntimeException ex) {
199                                // failed to populate, so leave it as not initialized
200                                shutdownDatabase();
201                                throw ex;
202                        }
203                }
204        }
205
206        /**
207         * Hook to shutdown the embedded database. Subclasses may call this method
208         * to force shutdown.
209         * <p>After calling, {@link #getDataSource()} returns {@code null}.
210         * <p>Does nothing if no embedded database has been initialized.
211         */
212        protected void shutdownDatabase() {
213                if (this.dataSource != null) {
214                        if (logger.isInfoEnabled()) {
215                                if (this.dataSource instanceof SimpleDriverDataSource) {
216                                        logger.info(String.format("Shutting down embedded database: url='%s'",
217                                                ((SimpleDriverDataSource) this.dataSource).getUrl()));
218                                }
219                                else {
220                                        logger.info(String.format("Shutting down embedded database '%s'", this.databaseName));
221                                }
222                        }
223                        this.databaseConfigurer.shutdown(this.dataSource, this.databaseName);
224                        this.dataSource = null;
225                }
226        }
227
228        /**
229         * Hook that gets the {@link DataSource} that provides the connectivity to the
230         * embedded database.
231         * <p>Returns {@code null} if the {@code DataSource} has not been initialized
232         * or if the database has been shut down. Subclasses may call this method to
233         * access the {@code DataSource} instance directly.
234         */
235        protected final DataSource getDataSource() {
236                return this.dataSource;
237        }
238
239
240        private class EmbeddedDataSourceProxy implements EmbeddedDatabase {
241
242                private final DataSource dataSource;
243
244                public EmbeddedDataSourceProxy(DataSource dataSource) {
245                        this.dataSource = dataSource;
246                }
247
248                @Override
249                public Connection getConnection() throws SQLException {
250                        return this.dataSource.getConnection();
251                }
252
253                @Override
254                public Connection getConnection(String username, String password) throws SQLException {
255                        return this.dataSource.getConnection(username, password);
256                }
257
258                @Override
259                public PrintWriter getLogWriter() throws SQLException {
260                        return this.dataSource.getLogWriter();
261                }
262
263                @Override
264                public void setLogWriter(PrintWriter out) throws SQLException {
265                        this.dataSource.setLogWriter(out);
266                }
267
268                @Override
269                public int getLoginTimeout() throws SQLException {
270                        return this.dataSource.getLoginTimeout();
271                }
272
273                @Override
274                public void setLoginTimeout(int seconds) throws SQLException {
275                        this.dataSource.setLoginTimeout(seconds);
276                }
277
278                @Override
279                public <T> T unwrap(Class<T> iface) throws SQLException {
280                        return this.dataSource.unwrap(iface);
281                }
282
283                @Override
284                public boolean isWrapperFor(Class<?> iface) throws SQLException {
285                        return this.dataSource.isWrapperFor(iface);
286                }
287
288                // getParentLogger() is required for JDBC 4.1 compatibility
289                @Override
290                public Logger getParentLogger() {
291                        return Logger.getLogger(Logger.GLOBAL_LOGGER_NAME);
292                }
293
294                @Override
295                public void shutdown() {
296                        shutdownDatabase();
297                }
298        }
299
300}