001/*
002 * Copyright 2002-2019 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.init;
018
019import java.sql.Connection;
020import java.util.ArrayList;
021import java.util.Arrays;
022import java.util.List;
023
024import javax.sql.DataSource;
025
026import org.springframework.core.io.Resource;
027import org.springframework.core.io.support.EncodedResource;
028import org.springframework.lang.Nullable;
029import org.springframework.util.Assert;
030import org.springframework.util.StringUtils;
031
032/**
033 * Populates, initializes, or cleans up a database using SQL scripts defined in
034 * external resources.
035 *
036 * <ul>
037 * <li>Call {@link #addScript} to add a single SQL script location.
038 * <li>Call {@link #addScripts} to add multiple SQL script locations.
039 * <li>Consult the setter methods in this class for further configuration options.
040 * <li>Call {@link #populate} or {@link #execute} to initialize or clean up the
041 * database using the configured scripts.
042 * </ul>
043 *
044 * @author Keith Donald
045 * @author Dave Syer
046 * @author Juergen Hoeller
047 * @author Chris Beams
048 * @author Oliver Gierke
049 * @author Sam Brannen
050 * @author Chris Baldwin
051 * @author Phillip Webb
052 * @since 3.0
053 * @see DatabasePopulatorUtils
054 * @see ScriptUtils
055 */
056public class ResourceDatabasePopulator implements DatabasePopulator {
057
058        List<Resource> scripts = new ArrayList<>();
059
060        @Nullable
061        private String sqlScriptEncoding;
062
063        private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR;
064
065        private String[] commentPrefixes = ScriptUtils.DEFAULT_COMMENT_PREFIXES;
066
067        private String blockCommentStartDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_START_DELIMITER;
068
069        private String blockCommentEndDelimiter = ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER;
070
071        private boolean continueOnError = false;
072
073        private boolean ignoreFailedDrops = false;
074
075
076        /**
077         * Construct a new {@code ResourceDatabasePopulator} with default settings.
078         * @since 4.0.3
079         */
080        public ResourceDatabasePopulator() {
081        }
082
083        /**
084         * Construct a new {@code ResourceDatabasePopulator} with default settings
085         * for the supplied scripts.
086         * @param scripts the scripts to execute to initialize or clean up the database
087         * (never {@code null})
088         * @since 4.0.3
089         */
090        public ResourceDatabasePopulator(Resource... scripts) {
091                setScripts(scripts);
092        }
093
094        /**
095         * Construct a new {@code ResourceDatabasePopulator} with the supplied values.
096         * @param continueOnError flag to indicate that all failures in SQL should be
097         * logged but not cause a failure
098         * @param ignoreFailedDrops flag to indicate that a failed SQL {@code DROP}
099         * statement can be ignored
100         * @param sqlScriptEncoding the encoding for the supplied SQL scripts
101         * (may be {@code null} or <em>empty</em> to indicate platform encoding)
102         * @param scripts the scripts to execute to initialize or clean up the database
103         * (never {@code null})
104         * @since 4.0.3
105         */
106        public ResourceDatabasePopulator(boolean continueOnError, boolean ignoreFailedDrops,
107                        @Nullable String sqlScriptEncoding, Resource... scripts) {
108
109                this.continueOnError = continueOnError;
110                this.ignoreFailedDrops = ignoreFailedDrops;
111                setSqlScriptEncoding(sqlScriptEncoding);
112                setScripts(scripts);
113        }
114
115
116        /**
117         * Add a script to execute to initialize or clean up the database.
118         * @param script the path to an SQL script (never {@code null})
119         */
120        public void addScript(Resource script) {
121                Assert.notNull(script, "'script' must not be null");
122                this.scripts.add(script);
123        }
124
125        /**
126         * Add multiple scripts to execute to initialize or clean up the database.
127         * @param scripts the scripts to execute (never {@code null})
128         */
129        public void addScripts(Resource... scripts) {
130                assertContentsOfScriptArray(scripts);
131                this.scripts.addAll(Arrays.asList(scripts));
132        }
133
134        /**
135         * Set the scripts to execute to initialize or clean up the database,
136         * replacing any previously added scripts.
137         * @param scripts the scripts to execute (never {@code null})
138         */
139        public void setScripts(Resource... scripts) {
140                assertContentsOfScriptArray(scripts);
141                // Ensure that the list is modifiable
142                this.scripts = new ArrayList<>(Arrays.asList(scripts));
143        }
144
145        private void assertContentsOfScriptArray(Resource... scripts) {
146                Assert.notNull(scripts, "'scripts' must not be null");
147                Assert.noNullElements(scripts, "'scripts' must not contain null elements");
148        }
149
150        /**
151         * Specify the encoding for the configured SQL scripts,
152         * if different from the platform encoding.
153         * @param sqlScriptEncoding the encoding used in scripts
154         * (may be {@code null} or empty to indicate platform encoding)
155         * @see #addScript(Resource)
156         */
157        public void setSqlScriptEncoding(@Nullable String sqlScriptEncoding) {
158                this.sqlScriptEncoding = (StringUtils.hasText(sqlScriptEncoding) ? sqlScriptEncoding : null);
159        }
160
161        /**
162         * Specify the statement separator, if a custom one.
163         * <p>Defaults to {@code ";"} if not specified and falls back to {@code "\n"}
164         * as a last resort; may be set to {@link ScriptUtils#EOF_STATEMENT_SEPARATOR}
165         * to signal that each script contains a single statement without a separator.
166         * @param separator the script statement separator
167         */
168        public void setSeparator(String separator) {
169                this.separator = separator;
170        }
171
172        /**
173         * Set the prefix that identifies single-line comments within the SQL scripts.
174         * <p>Defaults to {@code "--"}.
175         * @param commentPrefix the prefix for single-line comments
176         * @see #setCommentPrefixes(String...)
177         */
178        public void setCommentPrefix(String commentPrefix) {
179                Assert.hasText(commentPrefix, "'commentPrefix' must not be null or empty");
180                this.commentPrefixes = new String[] { commentPrefix };
181        }
182
183        /**
184         * Set the prefixes that identify single-line comments within the SQL scripts.
185         * <p>Defaults to {@code ["--"]}.
186         * @param commentPrefixes the prefixes for single-line comments
187         * @since 5.2
188         */
189        public void setCommentPrefixes(String... commentPrefixes) {
190                Assert.notEmpty(commentPrefixes, "'commentPrefixes' must not be null or empty");
191                Assert.noNullElements(commentPrefixes, "'commentPrefixes' must not contain null elements");
192                this.commentPrefixes = commentPrefixes;
193        }
194
195        /**
196         * Set the start delimiter that identifies block comments within the SQL
197         * scripts.
198         * <p>Defaults to {@code "/*"}.
199         * @param blockCommentStartDelimiter the start delimiter for block comments
200         * (never {@code null} or empty)
201         * @since 4.0.3
202         * @see #setBlockCommentEndDelimiter
203         */
204        public void setBlockCommentStartDelimiter(String blockCommentStartDelimiter) {
205                Assert.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
206                this.blockCommentStartDelimiter = blockCommentStartDelimiter;
207        }
208
209        /**
210         * Set the end delimiter that identifies block comments within the SQL
211         * scripts.
212         * <p>Defaults to <code>"*&#47;"</code>.
213         * @param blockCommentEndDelimiter the end delimiter for block comments
214         * (never {@code null} or empty)
215         * @since 4.0.3
216         * @see #setBlockCommentStartDelimiter
217         */
218        public void setBlockCommentEndDelimiter(String blockCommentEndDelimiter) {
219                Assert.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");
220                this.blockCommentEndDelimiter = blockCommentEndDelimiter;
221        }
222
223        /**
224         * Flag to indicate that all failures in SQL should be logged but not cause a failure.
225         * <p>Defaults to {@code false}.
226         * @param continueOnError {@code true} if script execution should continue on error
227         */
228        public void setContinueOnError(boolean continueOnError) {
229                this.continueOnError = continueOnError;
230        }
231
232        /**
233         * Flag to indicate that a failed SQL {@code DROP} statement can be ignored.
234         * <p>This is useful for a non-embedded database whose SQL dialect does not
235         * support an {@code IF EXISTS} clause in a {@code DROP} statement.
236         * <p>The default is {@code false} so that if the populator runs accidentally, it will
237         * fail fast if a script starts with a {@code DROP} statement.
238         * @param ignoreFailedDrops {@code true} if failed drop statements should be ignored
239         */
240        public void setIgnoreFailedDrops(boolean ignoreFailedDrops) {
241                this.ignoreFailedDrops = ignoreFailedDrops;
242        }
243
244
245        /**
246         * {@inheritDoc}
247         * @see #execute(DataSource)
248         */
249        @Override
250        public void populate(Connection connection) throws ScriptException {
251                Assert.notNull(connection, "'connection' must not be null");
252                for (Resource script : this.scripts) {
253                        EncodedResource encodedScript = new EncodedResource(script, this.sqlScriptEncoding);
254                        ScriptUtils.executeSqlScript(connection, encodedScript, this.continueOnError, this.ignoreFailedDrops,
255                                        this.commentPrefixes, this.separator, this.blockCommentStartDelimiter, this.blockCommentEndDelimiter);
256                }
257        }
258
259        /**
260         * Execute this {@code ResourceDatabasePopulator} against the given
261         * {@link DataSource}.
262         * <p>Delegates to {@link DatabasePopulatorUtils#execute}.
263         * @param dataSource the {@code DataSource} to execute against (never {@code null})
264         * @throws ScriptException if an error occurs
265         * @since 4.1
266         * @see #populate(Connection)
267         */
268        public void execute(DataSource dataSource) throws ScriptException {
269                DatabasePopulatorUtils.execute(this, dataSource);
270        }
271
272}