diff --git a/liquibase-core/src/main/java/liquibase/command/CommandScope.java b/liquibase-core/src/main/java/liquibase/command/CommandScope.java index 8b300bbce32..895d9580fca 100644 --- a/liquibase-core/src/main/java/liquibase/command/CommandScope.java +++ b/liquibase-core/src/main/java/liquibase/command/CommandScope.java @@ -3,8 +3,11 @@ import liquibase.Scope; import liquibase.configuration.*; import liquibase.exception.CommandExecutionException; +import liquibase.exception.CommandValidationException; import liquibase.util.StringUtil; +import java.io.FilterOutputStream; +import java.io.IOException; import java.io.OutputStream; import java.util.*; @@ -126,16 +129,21 @@ public T getArgumentValue(CommandArgumentDefinition argument) { * Think "what would be piped out", not "what the user is told about what is happening". */ public CommandScope setOutput(OutputStream outputStream) { - this.outputStream = outputStream; + /* + This is an UnclosableOutputStream because we do not want individual command steps to inadvertently (or + intentionally) close the System.out OutputStream. Closing System.out renders it unusable for other command + steps which expect it to still be open. If the passed OutputStream is null then we do not create it. + */ + if (outputStream != null) { + this.outputStream = new UnclosableOutputStream(outputStream); + } else { + this.outputStream = null; + } return this; } - /** - * Executes the command in this scope, and returns the results. - */ - public CommandResults execute() throws CommandExecutionException { - CommandResultsBuilder resultsBuilder = new CommandResultsBuilder(this, outputStream); + public void validate() throws CommandValidationException { for (ConfigurationValueProvider provider : Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class).getProviders()) { provider.validate(this); } @@ -151,6 +159,15 @@ public CommandResults execute() throws CommandExecutionException { for (CommandStep step : pipeline) { step.validate(this); } + } + + /** + * Executes the command in this scope, and returns the results. + */ + public CommandResults execute() throws CommandExecutionException { + CommandResultsBuilder resultsBuilder = new CommandResultsBuilder(this, outputStream); + final List pipeline = commandDefinition.getPipeline(); + validate(); try { for (CommandStep command : pipeline) { command.run(resultsBuilder); @@ -191,6 +208,29 @@ private ConfigurationDefinition createConfigurationDefinition(CommandArgu .buildTemporary(); } + /** + * This class is a wrapper around OutputStreams, and makes them impossible for callers to close. + */ + private static class UnclosableOutputStream extends FilterOutputStream { + public UnclosableOutputStream(OutputStream out) { + super(out); + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + out.write(b, off, len); + } + + /** + * This method does not actually close the underlying stream, but rather only flushes it. Callers should not be + * closing the stream they are given. + */ + @Override + public void close() throws IOException { + out.flush(); + } + } + private class CommandScopeValueProvider extends AbstractMapConfigurationValueProvider { @Override diff --git a/liquibase-core/src/main/java/liquibase/exception/CommandValidationException.java b/liquibase-core/src/main/java/liquibase/exception/CommandValidationException.java index cd78ced0d04..acf80bfb104 100644 --- a/liquibase-core/src/main/java/liquibase/exception/CommandValidationException.java +++ b/liquibase-core/src/main/java/liquibase/exception/CommandValidationException.java @@ -13,6 +13,10 @@ public CommandValidationException(String message) { super(message); } + public CommandValidationException(Throwable cause) { + super(cause); + } + public CommandValidationException(String argument, String message, Throwable cause) { super(buildMessage(argument, message), cause); } diff --git a/liquibase-core/src/main/java/liquibase/integration/commandline/Main.java b/liquibase-core/src/main/java/liquibase/integration/commandline/Main.java index 429b2c0f3d9..d6a23ad5b5d 100644 --- a/liquibase-core/src/main/java/liquibase/integration/commandline/Main.java +++ b/liquibase-core/src/main/java/liquibase/integration/commandline/Main.java @@ -288,8 +288,10 @@ public Integer run() throws Exception { java.util.logging.Logger liquibaseLogger = java.util.logging.Logger.getLogger("liquibase"); liquibaseLogger.setParent(rootLogger); - final JavaLogService logService = (JavaLogService) Scope.getCurrentScope().get(Scope.Attr.logService, LogService.class); - logService.setParent(liquibaseLogger); + LogService logService = Scope.getCurrentScope().get(Scope.Attr.logService, LogService.class); + if (logService instanceof JavaLogService) { + ((JavaLogService) logService).setParent(liquibaseLogger); + } if (main.logLevel == null) { String defaultLogLevel = System.getProperty("liquibase.log.level"); diff --git a/liquibase-core/src/main/java/liquibase/util/CollectionUtil.java b/liquibase-core/src/main/java/liquibase/util/CollectionUtil.java index f89bb8dabfc..a03b624fd8d 100644 --- a/liquibase-core/src/main/java/liquibase/util/CollectionUtil.java +++ b/liquibase-core/src/main/java/liquibase/util/CollectionUtil.java @@ -78,7 +78,7 @@ public static T[] createIfNull(T[] arguments) { } /** - * Returns a new empty set if the passed array is null. + * Returns a new empty set if the passed set is null. */ public static Set createIfNull(Set currentValue) { if (currentValue == null) { @@ -88,6 +88,17 @@ public static Set createIfNull(Set currentValue) { } } + /** + * Returns a new empty map if the passed map is null. + */ + public static Map createIfNull(Map currentValue) { + if (currentValue == null) { + return new HashMap<>(); + } else { + return currentValue; + } + } + /** * Converts a set of nested maps (like from yaml/json) into a flat map with dot-separated properties */ diff --git a/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/command/CommandTests.groovy b/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/command/CommandTests.groovy index cedc7f2dd68..9138511ec78 100644 --- a/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/command/CommandTests.groovy +++ b/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/command/CommandTests.groovy @@ -1105,6 +1105,9 @@ Long Description: ${commandDefinition.getLongDescription() ?: "NOT SET"} this.setups.add(new SetupModifyTextFile(textFile, originalString, newString)) } + void modifyDbCredentials(File textFile) { + this.setups.add(new SetupModifyDbCredentials(textFile)) + } private void validate() throws IllegalArgumentException { } diff --git a/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/setup/SetupModifyDbCredentials.groovy b/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/setup/SetupModifyDbCredentials.groovy new file mode 100644 index 00000000000..07991b5cded --- /dev/null +++ b/liquibase-extension-testing/src/main/groovy/liquibase/extension/testing/setup/SetupModifyDbCredentials.groovy @@ -0,0 +1,31 @@ +package liquibase.extension.testing.setup + +import liquibase.util.FileUtil + +/** + * + * This class allows modification of a text file to + * replace tokens with the actual database credential + * as specified in the environment + * + */ +class SetupModifyDbCredentials extends TestSetup { + + private static final String URL = "_URL_" + private static final String USERNAME = "_USERNAME_" + private static final String PASSWORD = "_PASSWORD_" + private final File textFile + + SetupModifyDbCredentials(File textFile) { + this.textFile = textFile + } + + @Override + void setup(TestSetupEnvironment testSetupEnvironment) throws Exception { + String contents = FileUtil.getContents(textFile) + contents = contents.replaceAll(URL, testSetupEnvironment.url) + contents = contents.replaceAll(USERNAME, testSetupEnvironment.username) + contents = contents.replaceAll(PASSWORD, testSetupEnvironment.password) + FileUtil.write(contents, textFile) + } +} \ No newline at end of file diff --git a/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseFlowMojo.java b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseFlowMojo.java new file mode 100644 index 00000000000..91fab049793 --- /dev/null +++ b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/AbstractLiquibaseFlowMojo.java @@ -0,0 +1,67 @@ +package org.liquibase.maven.plugins; + +import liquibase.Liquibase; +import liquibase.Scope; +import liquibase.command.CommandScope; +import liquibase.configuration.LiquibaseConfiguration; +import liquibase.exception.CommandExecutionException; +import liquibase.exception.LiquibaseException; +import org.liquibase.maven.property.PropertyElement; +import org.liquibase.maven.provider.FlowCommandArgumentValueProvider; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.util.Map; + +public abstract class AbstractLiquibaseFlowMojo extends AbstractLiquibaseMojo { + /** + * Specifies the flowFile to use. If not specified, the default + * checks will be used and no file will be created. + * + * @parameter property="liquibase.flowFile" + */ + @PropertyElement + protected String flowFile; + + /** + * @parameter property="liquibase.outputFile" + */ + @PropertyElement + protected File outputFile; + + /** + * Arbitrary map of parameters that the underlying liquibase command will use. These arguments will be passed + * verbatim to the underlying liquibase command that is being run. + * + * @parameter property="flowCommandArguments" + */ + @PropertyElement + protected Map flowCommandArguments; + + @Override + public boolean databaseConnectionRequired() { + return false; + } + + @Override + protected void performLiquibaseTask(Liquibase liquibase) throws LiquibaseException { + CommandScope liquibaseCommand = new CommandScope(getCommandName()); + liquibaseCommand.addArgumentValue("flowFile", flowFile); + liquibaseCommand.addArgumentValue("flowIntegration", "maven"); + if (flowCommandArguments != null) { + FlowCommandArgumentValueProvider flowCommandArgumentValueProvider = new FlowCommandArgumentValueProvider(flowCommandArguments); + Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class).registerProvider(flowCommandArgumentValueProvider); + } + if (outputFile != null) { + try { + liquibaseCommand.setOutput(new FileOutputStream(outputFile)); + } catch (FileNotFoundException e) { + throw new CommandExecutionException(e); + } + } + liquibaseCommand.execute(); + } + + public abstract String[] getCommandName(); +} diff --git a/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowMojo.java b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowMojo.java new file mode 100644 index 00000000000..87e19d3613a --- /dev/null +++ b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowMojo.java @@ -0,0 +1,14 @@ +package org.liquibase.maven.plugins; + +/** + * Run a series of commands contained in one or more stages, as configured in a liquibase flow-file. + * + * @goal flow + */ +public class LiquibaseFlowMojo extends AbstractLiquibaseFlowMojo { + + @Override + public String[] getCommandName() { + return new String[]{"flow"}; + } +} diff --git a/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowValidateMojo.java b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowValidateMojo.java new file mode 100644 index 00000000000..043674ebf5c --- /dev/null +++ b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/plugins/LiquibaseFlowValidateMojo.java @@ -0,0 +1,13 @@ +package org.liquibase.maven.plugins; + +/** + * Validate a series of commands contained in one or more stages, as configured in a liquibase flow-file. + * + * @goal flow.validate + */ +public class LiquibaseFlowValidateMojo extends AbstractLiquibaseFlowMojo{ + @Override + public String[] getCommandName() { + return new String[]{"flow", "validate"}; + } +} diff --git a/liquibase-maven-plugin/src/main/java/org/liquibase/maven/provider/FlowCommandArgumentValueProvider.java b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/provider/FlowCommandArgumentValueProvider.java new file mode 100644 index 00000000000..b7f57b78761 --- /dev/null +++ b/liquibase-maven-plugin/src/main/java/org/liquibase/maven/provider/FlowCommandArgumentValueProvider.java @@ -0,0 +1,41 @@ +package org.liquibase.maven.provider; + +import liquibase.configuration.AbstractMapConfigurationValueProvider; + +import java.util.Map; + +public class FlowCommandArgumentValueProvider extends AbstractMapConfigurationValueProvider { + + private final Map args; + + public FlowCommandArgumentValueProvider(Map args) { + this.args = args; + } + + @Override + public int getPrecedence() { + return 250; + } + + @Override + protected Map getMap() { + return args; + } + + @Override + protected String getSourceDescription() { + return "Arguments provided through maven when invoking flow or flow.validate maven goals"; + } + + @Override + protected boolean keyMatches(String wantedKey, String storedKey) { + if (super.keyMatches(wantedKey, storedKey)) { + return true; + } + if (wantedKey.startsWith("liquibase.command.")) { + return super.keyMatches(wantedKey.replaceFirst("^liquibase\\.command\\.", ""), storedKey); + } + + return super.keyMatches(wantedKey.replaceFirst("^liquibase\\.", ""), storedKey); + } +}