org.liquibase.maven.plugins.AbstractLiquibaseMojo Maven / Gradle / Ivy
Show all versions of liquibase-maven-plugin Show documentation
package org.liquibase.maven.plugins;
import liquibase.*;
import liquibase.changelog.visitor.ChangeExecListener;
import liquibase.changelog.visitor.DefaultChangeExecListener;
import liquibase.command.core.helpers.DbUrlConnectionCommandStep;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.configuration.core.DefaultsFileValueProvider;
import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.integration.IntegrationDetails;
import liquibase.integration.commandline.ChangeExecListenerUtils;
import liquibase.integration.commandline.CommandLineUtils;
import liquibase.integration.commandline.LiquibaseCommandLineConfiguration;
import liquibase.logging.LogFormat;
import liquibase.logging.LogService;
import liquibase.logging.core.JavaLogService;
import liquibase.logging.core.LogServiceFactory;
import liquibase.resource.DirectoryResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.resource.SearchPathResourceAccessor;
import liquibase.util.FileUtil;
import liquibase.util.StringUtil;
import org.apache.maven.artifact.manager.WagonManager;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.descriptor.MojoDescriptor;
import org.apache.maven.plugin.descriptor.Parameter;
import org.apache.maven.plugin.descriptor.PluginDescriptor;
import org.apache.maven.plugin.logging.Log;
import org.apache.maven.project.MavenProject;
import org.apache.maven.wagon.authentication.AuthenticationInfo;
import org.liquibase.maven.property.PropertyElement;
import javax.xml.bind.annotation.XmlSchema;
import java.io.*;
import java.lang.reflect.Field;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.text.MessageFormat;
import java.util.*;
import java.util.logging.Handler;
import static java.util.ResourceBundle.getBundle;
import static liquibase.configuration.LiquibaseConfiguration.REGISTERED_VALUE_PROVIDERS_KEY;
/**
* A base class for providing Liquibase {@link liquibase.Liquibase} functionality.
*
* @author Peter Murray
* @author Florent Biville
*
* Test dependency is used because when you run a goal outside the build phases you want to have the same dependencies
* that it would have if it was run inside test phase
* @requiresDependencyResolution test
*/
@SuppressWarnings("java:S2583")
public abstract class AbstractLiquibaseMojo extends AbstractMojo {
static {
// If maven is called with -T and a value larger than 1, it can get confused under heavy thread load
Scope.setScopeManager( new ThreadLocalScopeManager(null));
}
/**
* Suffix for fields that are representing a default value for a another field.
*/
private static final String DEFAULT_FIELD_SUFFIX = "Default";
private MavenLog logCache;
/**
*
* Specifies whether to preserve the case of schemas and catalogs
*
* @parameter property="liquibase.preserveSchemaCase"
*
*/
@PropertyElement
protected Boolean preserveSchemaCase;
/**
* Specifies the driver class name to use for the database connection.
*
* @parameter property="liquibase.driver"
*/
@PropertyElement
protected String driver;
/**
* Specifies the database URL you want to use to execute Liquibase.
*
* @parameter property="liquibase.url"
*/
@PropertyElement
protected String url;
/**
* The Maven Wagon manager to use when obtaining server authentication details.
*
* @component role="org.apache.maven.artifact.manager.WagonManager"
* @required
* @readonly
*/
protected WagonManager wagonManager;
/**
* Specifies the database username for database connection.
*
* @parameter property="liquibase.username"
*/
@PropertyElement
protected String username;
/**
* Specifies the database password for database connection.
*
* @parameter property="liquibase.password"
*/
@PropertyElement
protected String password;
/**
* Use an empty string as the password for the database connection. This should not be
* used along side the {@link #password} setting.
*
* @parameter property="liquibase.emptyPassword" default-value="false"
* @deprecated Use an empty or null value for the password instead.
*/
@PropertyElement
protected boolean emptyPassword;
/**
* Specifies whether to ignore the schema name.
*
* @parameter property="liquibase.outputDefaultSchema" default-value="false"
*/
@PropertyElement
protected boolean outputDefaultSchema;
/**
* Specifies whether to ignore the catalog/database name.
*
* @parameter property="liquibase.outputDefaultCatalog" default-value="false"
*/
@PropertyElement
protected boolean outputDefaultCatalog;
/**
* Specifies the default catalog name to use for the database connection.
*
* @parameter property="liquibase.defaultCatalogName"
*/
@PropertyElement
protected String defaultCatalogName;
/**
* Specifies the default schema name to use for the database connection.
*
* @parameter property="liquibase.defaultSchemaName"
*/
@PropertyElement
protected String defaultSchemaName;
/**
* Specifies the database object class.
*
* @parameter property="liquibase.databaseClass"
*/
@PropertyElement
protected String databaseClass;
/**
* Specifies the property provider which must be a java.util.Properties implementation.
*
* @parameter property="liquibase.propertyProviderClass"
*/
@PropertyElement
protected String propertyProviderClass;
/**
* (DEPRECATED) Controls whether users are prompted before executing changeSet to a non-local database.
*
* @parameter property="liquibase.promptOnNonLocalDatabase" default-value="false"
* @deprecated No longer prompts
*/
@PropertyElement
protected boolean promptOnNonLocalDatabase;
/**
* Includes a Maven project artifact in the class loader which obtains the liquibase.properties and changelog files.
*
* @parameter property="liquibase.includeArtifact" default-value="true"
*/
@PropertyElement
protected boolean includeArtifact;
/**
* Includes the Maven test output directory in the class loader which obtains the liquibase.properties and changelog files.
*
* @parameter property="liquibase.includeTestOutputDirectory" default-value="true"
*/
@PropertyElement
protected boolean includeTestOutputDirectory;
/**
* Controls the amount of output detail when you call the plugin.
*
* @parameter property="liquibase.verbose" default-value="false"
*/
@PropertyElement
protected boolean verbose;
/**
* Deprecated and ignored configuration property. Logging is managed via the standard maven logging system
* either using the -e, -X or -q flags or the ${maven.home}/conf/logging/simplelogger.properties file.
*
* @see maven-logging for more information.
*
* @parameter property="liquibase.logging"
* @deprecated Logging managed by maven
*/
@PropertyElement
protected String logging;
/**
* Determines the minimum log level liquibase uses when logging.
*
* Supported values are:
*
*
* - DEBUG
* - INFO
* - WARNING
* - ERROR
*
*
* The primary use case for this option is to reduce the amount of logs from liquibase, while
* not changing the log level of maven itself, without changing ${maven.home}/conf/logging/simplelogger.properties.
*
* NOTE: The final log level is the maximum of this value and the maven log level.
* Thus, it is not possible to decrease the effective log level with this option.
*
* @parameter property="liquibase.logLevel" default-value="DEBUG"
*/
@PropertyElement
protected String logLevel;
/**
* Specifies the liquibase.properties you want to use to configure Liquibase.
*
* @parameter property="liquibase.propertyFile"
*/
@PropertyElement
protected String propertyFile;
/**
* A flag which indicates you want the liquibase.properties file to override any settings provided in the Maven plugin configuration.
* By default, if a property is explicitly specified it is
* not overridden if it also appears in the properties file.
*
* @parameter property="liquibase.propertyFileWillOverride" default-value="false"
*/
@PropertyElement
protected boolean propertyFileWillOverride;
/**
* A flag that forces checksums to be cleared from the DATABASECHANGELOG table.
*
* @parameter property="liquibase.clearCheckSums" default-value="false"
*/
@PropertyElement
protected boolean clearCheckSums;
/**
* Specifies a list of system properties you want to pass to the database.
*
* @parameter
*/
@PropertyElement
protected Properties systemProperties;
/**
* The Maven project that plugin is running under.
*
* @parameter property="project"
* @required
* @readonly
*/
protected MavenProject project;
/**
* Specifies whether to skip running Liquibase.
* The use of this parameter is NOT RECOMMENDED but can be used when needed.
*
* @parameter property="liquibase.skip" default-value="false"
*/
@PropertyElement
protected boolean skip;
/**
* Skip plugin execution if the specified file exists.
* The use of this parameter is NOT RECOMMENDED but can be used when needed.
*
* @parameter property="liquibase.skipOnFileExists"
*/
@PropertyElement
protected String skipOnFileExists;
/**
* A flag which indicates you want to set the character encoding of the output file during the updateSQL phase.
*
* @parameter property="liquibase.outputFileEncoding"
*/
@PropertyElement
protected String outputFileEncoding;
/**
* Specifies the schema Liquibase will use to create your changelog tables.
*
* @parameter property="liquibase.changelogCatalogName"
*/
@PropertyElement
protected String changelogCatalogName;
/**
* Specifies the schema Liquibase will use to create your changelog table.
*
* @parameter property="liquibase.changelogSchemaName"
*/
@PropertyElement
protected String changelogSchemaName;
/**
* Specifies the table name to use for the DATABASECHANGELOG table.
*
* @parameter property="liquibase.databaseChangeLogTableName"
*/
@PropertyElement
protected String databaseChangeLogTableName;
/**
* Specifies the table name to use for the DATABASECHANGELOGLOCK table.
*
* @parameter property="liquibase.databaseChangeLogLockTableName"
*/
@PropertyElement
protected String databaseChangeLogLockTableName;
/**
* Show the liquibase banner in output.
*
* @parameter property="liquibase.showBanner"
*/
@PropertyElement
protected boolean showBanner = true;
/**
* Specifies the server ID in the Maven settings.xml to use when authenticating.
*
* @parameter property="liquibase.server"
*/
@PropertyElement
private String server;
/**
* The {@link Liquibase} object used modify the database.
*/
@PropertyElement
private Liquibase liquibase;
/**
* Specifies the locations where Liquibase can find your changelog files.
*
* @parameter property="liquibase.searchPath"
*/
@PropertyElement
protected String searchPath;
/**
* A property-based collection of changelog properties to apply.
*
* @parameter
*/
private Properties expressionVars;
/**
* A map-based collection of changelog properties to apply.
*
* @parameter
*/
private Map expressionVariables;
/**
* Specifies the location of a JDBC connection-properties file which contains properties the driver will use.
*
* @parameter
*/
private File driverPropertiesFile;
/**
* Specifies your Liquibase Pro license key. This has been deprecated in favor of using
* "liquibase.liquibaseLicenseKey", but this property will continue to be operational.
*
* @parameter property="liquibase.liquibaseProLicenseKey"
*/
@PropertyElement
@Deprecated
private String liquibaseProLicenseKey;
/**
* Specifies your Liquibase license key.
*
* @parameter property="liquibase.licenseKey"
*/
@PropertyElement
private String liquibaseLicenseKey;
/**
* Specifies your psql path.
*
* @parameter property="liquibase.psql.path"
*/
@PropertyElement
protected String psqlPath;
/**
* Specifies whether to keep generated psql files.
*
* @parameter property="liquibase.psql.keep.temp"
*/
@PropertyElement
protected Boolean psqlKeepTemp;
/**
* Specifies the name of generated psql files.
*
* @parameter property="liquibase.psql.keep.temp.name"
*/
@PropertyElement
protected String psqlKeepTempName;
/**
* Specifies where to keep generated psql files.
*
* @parameter property="liquibase.psql.keep.temp.path"
*/
@PropertyElement
protected String psqlKeepTempPath;
/**
* Specifies additional psql args.
*
* @parameter property="liquibase.psql.args"
*/
@PropertyElement
protected String psqlArgs;
/**
* Specifies psql timeout.
*
* @parameter property="liquibase.psql.timeout"
*/
@PropertyElement
protected Integer psqlTimeout;
/**
* Specifies where to output psql logs.
*
* @parameter property="liquibase.psql.logFile"
*/
@PropertyElement
protected String psqlLogFile;
/**
* Specifies your sqlplus path.
*
* @parameter property="liquibase.sqlplus.path"
*/
@PropertyElement
protected String sqlPlusPath;
/**
* Specifies whether to keep generated sqlplus files.
*
* @parameter property="liquibase.sqlplus.keep.temp"
*/
@PropertyElement
protected Boolean sqlPlusKeepTemp;
/**
* Specifies the name of generated sqlplus files.
*
* @parameter property="liquibase.sqlplus.keep.temp.name"
*/
@PropertyElement
protected String sqlPlusKeepTempName;
/**
* Specifies where to keep generated sqlplus files.
*
* @parameter property="liquibase.sqlplus.keep.temp.path"
*/
@PropertyElement
protected String sqlPlusKeepTempPath;
/**
* Specifies whether to overwrite generated sqlplus files.
*
* @parameter property="liquibase.sqlplus.keep.temp.overwrite"
*/
@PropertyElement
protected Boolean sqlPlusKeepTempOverwrite;
/**
* Specifies additional sqlplus args.
*
* @parameter property="liquibase.sqlplus.args"
*/
@PropertyElement
protected String sqlPlusArgs;
/**
* Specifies sqlplus timeout.
*
* @parameter property="liquibase.sqlplus.timeout"
*/
@PropertyElement
protected Integer sqlPlusTimeout;
/**
* Specifies where to output sqlplus logs.
*
* @parameter property="liquibase.sqlplus.logFile"
*/
@PropertyElement
protected String sqlPlusLogFile;
/**
* Specifies your sqlcmd path.
*
* @parameter property="liquibase.sqlcmd.path"
*/
@PropertyElement
protected String sqlcmdPath;
/**
* Specifies whether to keep generated sqlcmd files.
*
* @parameter property="liquibase.sqlcmd.keep.temp"
*/
@PropertyElement
protected Boolean sqlcmdKeepTemp;
/**
* Specifies the name of generated sqlcmd files.
*
* @parameter property="liquibase.sqlcmd.keep.temp.name"
*/
@PropertyElement
protected String sqlcmdKeepTempName;
/**
* Specifies where to keep generated sqlcmd files.
*
* @parameter property="liquibase.sqlcmd.keep.temp.path"
*/
@PropertyElement
protected String sqlcmdKeepTempPath;
/**
* Specifies whether to overwrite generated sqlcmd files.
*
* @parameter property="liquibase.sqlcmd.keep.temp.overwrite"
*/
@PropertyElement
protected Boolean sqlcmdKeepTempOverwrite;
/**
* Specifies additional sqlcmd args.
*
* @parameter property="liquibase.sqlcmd.args"
*/
@PropertyElement
protected String sqlcmdArgs;
/**
* Specifies sqlcmd timeout.
*
* @parameter property="liquibase.sqlcmd.timeout"
*/
@PropertyElement
protected Integer sqlcmdTimeout;
/**
* Specifies where to output sqlcmd logs.
*
* @parameter property="liquibase.sqlcmd.logFile"
*/
@PropertyElement
protected String sqlcmdLogFile;
/**
* Specifies sqlcmd catalog name.
*
* @parameter property="liquibase.sqlcmd.catalogName"
*/
@PropertyElement
protected String sqlcmdCatalogName;
/**
* Specifies the fully qualified class name of the custom ChangeExecListener
*
* @parameter property="liquibase.changeExecListenerClass"
*/
@PropertyElement
protected String changeExecListenerClass;
/**
* Specifies the property file for controlling the custom ChangeExecListener
*
* @parameter property="liquibase.changeExecListenerPropertiesFile"
*/
@PropertyElement
protected String changeExecListenerPropertiesFile;
/**
* Sets the format of log output to console or log files.
* Open Source users default to unstructured TXT logs to the console or output log files.
* Pro users have the option to set value as JSON or JSON_PRETTY to enable json-structured log files to the console or output log files.
*
* @parameter property="liquibase.logFormat"
*/
@PropertyElement
protected String logFormat;
protected String commandName;
protected DefaultChangeExecListener defaultChangeExecListener;
private static final ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");
/**
* Get the specified license key. This first checks liquibaseLicenseKey and if no key is found, then returns
* liquibaseProLicenseKey.
*/
protected String getLicenseKey() {
if (StringUtil.isNotEmpty(liquibaseLicenseKey)) {
return liquibaseLicenseKey;
} else {
return liquibaseProLicenseKey;
}
}
protected Writer getOutputWriter(final File outputFile) throws IOException {
String encoding = this.outputFileEncoding;
if (encoding == null) {
encoding = GlobalConfiguration.OUTPUT_FILE_ENCODING.getCurrentValue();
}
getLog().debug("Writing output file with '" + encoding + "' file encoding.");
return new BufferedWriter(new OutputStreamWriter(Files.newOutputStream(outputFile.toPath()), encoding));
}
private Map setUpLogging() throws Exception {
// First determine whether the specified log format requires the use of the standard Scope logger.
boolean useScopeLogger = false;
if (this.logFormat != null) {
try {
useScopeLogger = LogFormat.valueOf(this.logFormat.toUpperCase()).isUseScopeLoggerInMaven();
} catch (Exception ignored) {
}
}
Map scopeAttrs = new HashMap<>();
if (!useScopeLogger) {
// If the specified log format does not require the use of the standard Liquibase logger, just return the
// Maven log service as is traditionally done.
scopeAttrs.put(Scope.Attr.logService.name(), new MavenLogService(getLog()));
scopeAttrs.put(Scope.Attr.ui.name(), new MavenUi(getLog()));
return scopeAttrs;
} else {
// The log format requires the use of the standard Liquibase logger, so set it up.
scopeAttrs.put(LiquibaseCommandLineConfiguration.LOG_FORMAT.getKey(), this.logFormat);
scopeAttrs.put(REGISTERED_VALUE_PROVIDERS_KEY, true);
// Get a new log service after registering the value providers, since the log service might need to load parameters using newly registered value providers.
LogService newLogService = Scope.child(scopeAttrs, () -> Scope.getCurrentScope().getSingleton(LogServiceFactory.class).getDefaultLogService());
// Set the formatter on all the handlers.
java.util.logging.Logger rootLogger = java.util.logging.Logger.getLogger("");
for (Handler handler : rootLogger.getHandlers()) {
JavaLogService.setFormatterOnHandler(newLogService, handler);
}
scopeAttrs.put(Scope.Attr.logService.name(), newLogService);
return scopeAttrs;
}
}
@Override
public void execute() throws MojoExecutionException, MojoFailureException {
if (StringUtil.trimToNull(logging) != null) {
getLog().error("The liquibase-maven-plugin now manages logging via the standard maven logging config, not the 'logging' configuration. Use the -e, -X or -q flags or see https://maven.apache.org/maven-logging.html");
}
if (skip) {
getLog().warn("Liquibase skipped due to Maven configuration");
return;
}
if (skipOnFileExists != null) {
File f = new File(skipOnFileExists);
if (f.exists()) {
getLog().warn("Liquibase skipped because file " + skipOnFileExists + " exists");
return;
}
getLog().warn("Liquibase NOT skipped because file " + skipOnFileExists + " does NOT exists");
}
try {
Scope.child(setUpLogging(), () -> {
getLog().info(MavenUtils.LOG_SEPARATOR);
if (server != null) {
AuthenticationInfo info = wagonManager.getAuthenticationInfo(server);
if (info != null) {
username = info.getUserName();
password = info.getPassword();
}
}
processSystemProperties();
if (!LiquibaseCommandLineConfiguration.SHOULD_RUN.getCurrentValue()) {
getLog().info("Liquibase did not run because " + LiquibaseCommandLineConfiguration.SHOULD_RUN.getKey() + " was set to false");
return;
}
ClassLoader mavenClassLoader = getClassLoaderIncludingProjectClasspath();
Map scopeValues = new HashMap<>();
scopeValues.put(Scope.Attr.resourceAccessor.name(), getResourceAccessor(mavenClassLoader));
scopeValues.put(Scope.Attr.classLoader.name(), getClassLoaderIncludingProjectClasspath());
IntegrationDetails integrationDetails = new IntegrationDetails();
integrationDetails.setName("maven");
final PluginDescriptor pluginDescriptor = (PluginDescriptor) getPluginContext().get("pluginDescriptor");
for (MojoDescriptor descriptor : pluginDescriptor.getMojos()) {
if (!descriptor.getImplementationClass().equals(this.getClass())) {
continue;
}
for (Parameter param : descriptor.getParameters()) {
final String name = param.getName();
if (name.equalsIgnoreCase("project") || name.equalsIgnoreCase("systemProperties")) {
continue;
}
final Field field = getField(this.getClass(), name);
if (field == null) {
getLog().debug("Cannot read current maven value for: " + name);
} else {
field.setAccessible(true);
final Object value = field.get(this);
if (value != null) {
try {
integrationDetails.setParameter("maven__" + param.getName().replaceAll("[${}]", ""), String.valueOf(value));
} catch (Throwable e) {
e.printStackTrace();
}
}
}
}
}
//
// Add properties to this top-level scope
//
scopeValues.put("integrationDetails", integrationDetails);
scopeValues.put("liquibase.licenseKey", getLicenseKey());
String key = GlobalConfiguration.PRESERVE_SCHEMA_CASE.getKey();
scopeValues.put(key, preserveSchemaCase);
scopeValues.putAll(getNativeExecutorProperties());
Scope.child(scopeValues, () -> {
configureFieldsAndValues();
if (showBanner) {
getLog().info(CommandLineUtils.getBanner());
}
// Displays the settings for the Mojo depending on verbosity mode.
displayMojoSettings();
// Check that all the parameters that must be specified have been by the user.
checkRequiredParametersAreSpecified();
Database database = null;
try {
if (databaseConnectionRequired()) {
String dbPassword = (emptyPassword || (password == null)) ? "" : password;
String driverPropsFile = (driverPropertiesFile == null) ? null : driverPropertiesFile.getAbsolutePath();
database = CommandLineUtils.createDatabaseObject(mavenClassLoader,
url,
username,
dbPassword,
driver,
defaultCatalogName,
defaultSchemaName,
outputDefaultCatalog,
outputDefaultSchema,
databaseClass,
driverPropsFile,
propertyProviderClass,
changelogCatalogName,
changelogSchemaName,
databaseChangeLogTableName,
databaseChangeLogLockTableName);
DbUrlConnectionCommandStep.logMdc(url, database);
liquibase = createLiquibase(database);
configureChangeLogProperties();
ChangeExecListener listener = ChangeExecListenerUtils.getChangeExecListener(
liquibase.getDatabase(), liquibase.getResourceAccessor(),
changeExecListenerClass, changeExecListenerPropertiesFile);
defaultChangeExecListener = new DefaultChangeExecListener(listener);
liquibase.setChangeExecListener(defaultChangeExecListener);
getLog().debug("expressionVars = " + expressionVars);
if (expressionVars != null) {
for (Map.Entry