/*
* Copyright 2010-2017 Boxfuse GmbH
*
* 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.flywaydb.gradle.task;
import org.flywaydb.core.Flyway;
import org.flywaydb.core.api.FlywayException;
import org.flywaydb.core.internal.util.ClassUtils;
import org.flywaydb.core.internal.util.Location;
import org.flywaydb.core.internal.util.StringUtils;
import org.flywaydb.core.internal.util.UrlUtils;
import org.flywaydb.gradle.FlywayExtension;
import org.gradle.api.DefaultTask;
import org.gradle.api.artifacts.ResolvedArtifact;
import org.gradle.api.plugins.JavaPluginConvention;
import org.gradle.api.tasks.SourceSet;
import org.gradle.api.tasks.TaskAction;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
/**
* A base class for all flyway tasks.
*/
public abstract class AbstractFlywayTask extends DefaultTask {
/**
* Property name prefix for placeholders that are configured through System properties.
*/
private static final String PLACEHOLDERS_PROPERTY_PREFIX = "flyway.placeholders.";
/**
* The flyway {} block in the build script.
*/
protected FlywayExtension extension;
/**
* The fully qualified classname of the jdbc driver to use to connect to the database
*/
public String driver;
/**
* The jdbc url to use to connect to the database
*/
public String url;
/**
* The user to use to connect to the database
*/
public String user;
/**
* The password to use to connect to the database
*/
public String password;
/**
* The name of Flyway's metadata table
*/
public String table;
/**
* The case-sensitive list of schemas managed by Flyway
*/
public String[] schemas;
/**
* The version to tag an existing schema with when executing baseline. (default: 1)
*/
public String baselineVersion;
/**
* The description to tag an existing schema with when executing baseline. (default: << Flyway Baseline >>)
*/
public String baselineDescription;
/**
* Locations to scan recursively for migrations. The location type is determined by its prefix.
* (default: filesystem:src/main/resources/db/migration)
* <p>
* <tt>Unprefixed locations or locations starting with classpath:</tt>
* point to a package on the classpath and may contain both sql and java-based migrations.
* <p>
* <tt>Locations starting with filesystem:</tt>
* point to a directory on the filesystem and may only contain sql migrations.
*/
public String[] locations;
/**
* The fully qualified class names of the custom MigrationResolvers to be used in addition (default)
* or as a replacement (using skipDefaultResolvers) to the built-in ones for resolving Migrations to
* apply.
* <p>(default: none)</p>
*/
public String[] resolvers;
/**
* If set to true, default built-in resolvers will be skipped, only custom migration resolvers will be used.
* <p>(default: false)</p>
*/
public Boolean skipDefaultResolvers;
/**
* The file name prefix for Sql migrations
* <p>
* <p>Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix ,
* which using the defaults translates to V1_1__My_description.sql</p>
*/
public String sqlMigrationPrefix;
/**
* The file name prefix for repeatable sql migrations (default: R).
* <p>
* <p>Repeatable sql migrations have the following file name structure: prefixSeparatorDESCRIPTIONsuffix ,
* which using the defaults translates to R__My_description.sql</p>
*/
public String repeatableSqlMigrationPrefix;
/**
* The file name prefix for Sql migrations
* <p>
* <p>Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix ,
* which using the defaults translates to V1_1__My_description.sql</p>
*/
public String sqlMigrationSeparator;
/**
* The file name suffix for Sql migrations
* <p>
* <p>Sql migrations have the following file name structure: prefixVERSIONseparatorDESCRIPTIONsuffix ,
* which using the defaults translates to V1_1__My_description.sql</p>
*/
public String sqlMigrationSuffix;
/**
* The encoding of Sql migrations
*/
public String encoding;
/**
* Placeholders to replace in Sql migrations
*/
public Map<Object, Object> placeholders;
/**
* Whether placeholders should be replaced.
*/
public Boolean placeholderReplacement;
/**
* The prefix of every placeholder
*/
public String placeholderPrefix;
/**
* The suffix of every placeholder
*/
public String placeholderSuffix;
/**
* The target version up to which Flyway should consider migrations.
* Migrations with a higher version number will be ignored.
* The special value {@code current} designates the current version of the schema.
*/
public String target;
/**
* An array of fully qualified FlywayCallback class implementations
*/
public String[] callbacks;
/**
* If set to true, default built-in callbacks will be skipped, only custom migration callbacks will be used.
* <p>(default: false)</p>
*/
public Boolean skipDefaultCallbacks;
/**
* Allows migrations to be run "out of order"
*/
public Boolean outOfOrder;
/**
* Whether to automatically call validate or not when running migrate. (default: true)
*/
public Boolean validateOnMigrate;
/**
* Whether to automatically call clean or not when a validation error occurs
*/
public Boolean cleanOnValidationError;
/**
* Ignore missing migrations when reading the metadata table. These are migrations that were performed by an
* older deployment of the application that are no longer available in this version. For example: we have migrations
* available on the classpath with versions 1.0 and 3.0. The metadata table indicates that a migration with version 2.0
* (unknown to us) has also been applied. Instead of bombing out (fail fast) with an exception, a
* warning is logged and Flyway continues normally. This is useful for situations where one must be able to deploy
* a newer version of the application even though it doesn't contain migrations included with an older one anymore.
*
* {@code true} to continue normally and log a warning, {@code false} to fail fast with an exception.
* (default: {@code false})
*/
public Boolean ignoreMissingMigrations;
/**
* Ignore future migrations when reading the metadata table. These are migrations that were performed by a
* newer deployment of the application that are not yet available in this version. For example: we have migrations
* available on the classpath up to version 3.0. The metadata table indicates that a migration to version 4.0
* (unknown to us) has already been applied. Instead of bombing out (fail fast) with an exception, a
* warning is logged and Flyway continues normally. This is useful for situations where one must be able to redeploy
* an older version of the application after the database has been migrated by a newer one. (default: {@code true})
*/
public Boolean ignoreFutureMigrations;
/**
* Whether to disable clean. (default: {@code false})
* <p>This is especially useful for production environments where running clean can be quite a career limiting move.</p>
*/
public Boolean cleanDisabled;
/**
* <p>
* Whether to automatically call baseline when migrate is executed against a non-empty schema with no metadata table.
* This schema will then be baselined with the {@code baselineVersion} before executing the migrations.
* Only migrations above {@code baselineVersion} will then be applied.
* </p>
* <p>
* This is useful for initial Flyway production deployments on projects with an existing DB.
* </p>
* <p>
* Be careful when enabling this as it removes the safety net that ensures
* Flyway does not migrate the wrong database in case of a configuration mistake!
* </p>
*
* <p>{@code true} if baseline should be called on migrate for non-empty schemas, {@code false} if not. (default: {@code false})</p>
*/
public Boolean baselineOnMigrate;
/**
* Whether to allow mixing transactional and non-transactional statements within the same migration.
* <p>{@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false})</p>
* @deprecated Use <code>mixed</code> instead. Will be removed in Flyway 5.0.
*/
@Deprecated
public Boolean allowMixedMigrations;
/**
* Whether to allow mixing transactional and non-transactional statements within the same migration.
* <p>{@code true} if mixed migrations should be allowed. {@code false} if an error should be thrown instead. (default: {@code false})</p>
*/
public Boolean mixed;
/**
* Whether to group all pending migrations together in the same transaction when applying them (only recommended for databases with support for DDL transactions).
* <p>{@code true} if migrations should be grouped. {@code false} if they should be applied individually instead. (default: {@code false})</p>
*/
public Boolean group;
/**
* The username that will be recorded in the metadata table as having applied the migration.
* <p>
* {@code null} for the current database user of the connection. (default: {@code null}).
*/
public String installedBy;
public AbstractFlywayTask() {
super();
setGroup("Flyway");
extension = (FlywayExtension) getProject().getExtensions().getByName("flyway");
}
@TaskAction
public Object runTask() {
try {
List<URL> extraURLs = new ArrayList<URL>();
if (isJavaProject()) {
JavaPluginConvention plugin = getProject().getConvention().getPlugin(JavaPluginConvention.class);
for (SourceSet sourceSet : plugin.getSourceSets()) {
URL classesUrl = sourceSet.getOutput().getClassesDir().toURI().toURL();
getLogger().debug("Adding directory to Classpath: " + classesUrl);
extraURLs.add(classesUrl);
URL resourcesUrl = sourceSet.getOutput().getResourcesDir().toURI().toURL();
getLogger().debug("Adding directory to Classpath: " + resourcesUrl);
extraURLs.add(resourcesUrl);
}
addDependenciesWithScope(extraURLs,"compile");
addDependenciesWithScope(extraURLs,"runtime");
addDependenciesWithScope(extraURLs,"testCompile");
addDependenciesWithScope(extraURLs,"testRuntime");
}
ClassLoader classLoader = new URLClassLoader(
extraURLs.toArray(new URL[extraURLs.size()]),
getProject().getBuildscript().getClassLoader());
Flyway flyway = new Flyway();
flyway.setClassLoader(classLoader);
flyway.configure(createFlywayConfig());
return run(flyway);
} catch (Exception e) {
handleException(e);
return null;
}
}
private void addDependenciesWithScope(List<URL> urls, String scope) throws IOException {
for (ResolvedArtifact artifact : getProject().getConfigurations().getByName(scope).getResolvedConfiguration().getResolvedArtifacts()) {
URL artifactUrl = artifact.getFile().toURI().toURL();
getLogger().debug("Adding Dependency to Classpath: " + artifactUrl);
urls.add(artifactUrl);
}
}
/**
* Executes the task's custom behavior.
*/
protected abstract Object run(Flyway flyway);
/**
* Creates the Flyway config to use.
*/
private Map<String, String> createFlywayConfig() {
Map<String, String> conf = new HashMap<String, String>();
putIfSet(conf, "driver", driver, extension.driver);
putIfSet(conf, "url", url, extension.url);
putIfSet(conf, "user", user, extension.user);
putIfSet(conf, "password", password, extension.password);
putIfSet(conf, "table", table, extension.table);
putIfSet(conf, "baselineVersion", baselineVersion, extension.baselineVersion);
putIfSet(conf, "baselineDescription", baselineDescription, extension.baselineDescription);
putIfSet(conf, "sqlMigrationPrefix", sqlMigrationPrefix, extension.sqlMigrationPrefix);
putIfSet(conf, "repeatableSqlMigrationPrefix", repeatableSqlMigrationPrefix, extension.repeatableSqlMigrationPrefix);
putIfSet(conf, "sqlMigrationSeparator", sqlMigrationSeparator, extension.sqlMigrationSeparator);
putIfSet(conf, "sqlMigrationSuffix", sqlMigrationSuffix, extension.sqlMigrationSuffix);
putIfSet(conf, "allowMixedMigrations", allowMixedMigrations, extension.allowMixedMigrations);
putIfSet(conf, "mixed", mixed, extension.mixed);
putIfSet(conf, "group", group, extension.group);
putIfSet(conf, "installedBy", installedBy, extension.installedBy);
putIfSet(conf, "encoding", encoding, extension.encoding);
putIfSet(conf, "placeholderReplacement", placeholderReplacement, extension.placeholderReplacement);
putIfSet(conf, "placeholderPrefix", placeholderPrefix, extension.placeholderPrefix);
putIfSet(conf, "placeholderSuffix", placeholderSuffix, extension.placeholderSuffix);
putIfSet(conf, "target", target, extension.target);
putIfSet(conf, "outOfOrder", outOfOrder, extension.outOfOrder);
putIfSet(conf, "validateOnMigrate", validateOnMigrate, extension.validateOnMigrate);
putIfSet(conf, "cleanOnValidationError", cleanOnValidationError, extension.cleanOnValidationError);
putIfSet(conf, "ignoreMissingMigrations", ignoreMissingMigrations, extension.ignoreMissingMigrations);
putIfSet(conf, "ignoreFutureMigrations", ignoreFutureMigrations, extension.ignoreFutureMigrations);
putIfSet(conf, "cleanDisabled", cleanDisabled, extension.cleanDisabled);
putIfSet(conf, "baselineOnMigrate", baselineOnMigrate, extension.baselineOnMigrate);
putIfSet(conf, "skipDefaultResolvers", skipDefaultResolvers, extension.skipDefaultResolvers);
putIfSet(conf, "skipDefaultCallbacks", skipDefaultCallbacks, extension.skipDefaultCallbacks);
putIfSet(conf, "schemas", StringUtils.arrayToCommaDelimitedString(schemas), StringUtils.arrayToCommaDelimitedString(extension.schemas));
conf.put("flyway.locations", Location.FILESYSTEM_PREFIX + getProject().getProjectDir().getAbsolutePath() + "/src/main/resources/db/migration");
putIfSet(conf, "locations", StringUtils.arrayToCommaDelimitedString(locations), StringUtils.arrayToCommaDelimitedString(extension.locations));
putIfSet(conf, "resolvers", StringUtils.arrayToCommaDelimitedString(resolvers), StringUtils.arrayToCommaDelimitedString(extension.resolvers));
putIfSet(conf, "callbacks", StringUtils.arrayToCommaDelimitedString(callbacks), StringUtils.arrayToCommaDelimitedString(extension.callbacks));
if (placeholders != null) {
for (Map.Entry<Object, Object> entry : placeholders.entrySet()) {
conf.put(PLACEHOLDERS_PROPERTY_PREFIX + entry.getKey().toString(), entry.getValue().toString());
}
}
if (extension.placeholders != null) {
for (Map.Entry<Object, Object> entry : extension.placeholders.entrySet()) {
conf.put(PLACEHOLDERS_PROPERTY_PREFIX + entry.getKey().toString(), entry.getValue().toString());
}
}
addConfigFromProperties(conf, getProject().getProperties());
addConfigFromProperties(conf, System.getProperties());
return conf;
}
private static void addConfigFromProperties(Map<String, String> config, Properties properties) {
for (String prop : properties.stringPropertyNames()) {
if (prop.startsWith("flyway.")) {
config.put(prop, properties.getProperty(prop));
}
}
}
private static void addConfigFromProperties(Map<String, String> config, Map<String, ?> properties) {
for (String prop : properties.keySet()) {
if (prop.startsWith("flyway.")) {
config.put(prop, properties.get(prop).toString());
}
}
}
/**
* @param throwable Throwable instance to be handled
*/
private void handleException(Throwable throwable) {
String message = "Error occurred while executing " + getName();
throw new FlywayException(collectMessages(throwable, message), throwable);
}
/**
* Collect error messages from the stack trace
*
* @param throwable Throwable instance from which the message should be build
* @param message the message to which the error message will be appended
* @return a String containing the composed messages
*/
private String collectMessages(Throwable throwable, String message) {
if (throwable != null) {
message += "\n" + throwable.getMessage();
return collectMessages(throwable.getCause(), message);
}
return message;
}
/**
* Puts this property in the config if it has been set either in the task or the extension.
*
* @param config The config.
* @param key The peoperty name.
* @param propValue The value in the plugin.
* @param extensionValue The value in the extension.
*/
private void putIfSet(Map<String, String> config, String key, Object propValue, Object extensionValue) {
if (propValue != null) {
config.put("flyway." + key, propValue.toString());
} else if (extensionValue != null) {
config.put("flyway." + key, extensionValue.toString());
}
}
private boolean isJavaProject() {
return getProject().getPluginManager().hasPlugin("java");
}
}