Skip to content

Commit

Permalink
Support system properties as a way of providing configuration options…
Browse files Browse the repository at this point in the history
… to the CLI (#4104)

* #3777 Support system properties as a way of providing configuration options to the CLI.

* #3777 Define a more convenient naming convention for JVM properties.

* Improve JVM system property to environment mapping by using an enum class instead of a Map.

* Provide a friendly error message in case of liquibase home misconfiguration.

* Externalize command line settings allowing for being reused by both LiquibaseCommandLine and LiquibaseLauncher.


---------
Co-authored-by: filipe <flautert@liquibase.org>
  • Loading branch information
jccampanero committed Oct 12, 2023
1 parent d093b2a commit 9790070
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 70 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package liquibase.integration.commandline;

import static liquibase.integration.commandline.LiquibaseLauncherSettings.LiquibaseLauncherSetting.LIQUIBASE_HOME;
import static liquibase.integration.commandline.LiquibaseLauncherSettings.getSetting;

import liquibase.Scope;
import liquibase.command.CommandArgumentDefinition;
import liquibase.command.CommandDefinition;
Expand Down Expand Up @@ -1267,7 +1270,7 @@ public String[] getVersion() throws Exception {
String liquibaseHome;
Path liquibaseHomePath = null;
try {
liquibaseHomePath = new File(ObjectUtil.defaultIfNull(System.getenv("LIQUIBASE_HOME"), workingDirectory.toAbsolutePath().toString())).getAbsoluteFile().getCanonicalFile().toPath();
liquibaseHomePath = new File(ObjectUtil.defaultIfNull(getSetting(LIQUIBASE_HOME), workingDirectory.toAbsolutePath().toString())).getAbsoluteFile().getCanonicalFile().toPath();
liquibaseHome = liquibaseHomePath.toString();
} catch (IOException e) {
liquibaseHome = "Cannot resolve LIQUIBASE_HOME: " + e.getMessage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,58 @@
import liquibase.util.StringUtil;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import static liquibase.integration.commandline.LiquibaseLauncherSettings.LiquibaseLauncherSetting.*;
import static liquibase.integration.commandline.LiquibaseLauncherSettings.getSetting;

/**
* Launcher which builds up the classpath needed to run Liquibase, then calls {@link LiquibaseCommandLine#main(String[])}.
* <p>
* It looks for a LIQUIBASE_LAUNCHER_DEBUG env variable to determine if it should log what it is doing to stderr.
* Supports the following configuration options that can be passed as JVM properties and/or environment variables, taking
* the former precedence over the latter:
* <table>
* <thead>
* <tr>
* <th><b>Environment variable</b></th>
* <th><b>JVM property</b></th>
* </tr>
* <tr>
* <th colspan="2"><b>Meaning</b></th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td><code>LIQUIBASE_HOME</code></td>
* <td><code>liquibase.home</code></td>
* </tr>
* <tr>
* <td colspan="2">Liquibase home. This option is mandatory.</td>
* </tr>
* <tr>
* <td><code>LIQUIBASE_LAUNCHER_DEBUG</code></td>
* <td><code>liquibase.launcher.debug</code></td>
* </tr>
* <tr>
* <td colspan="2">Determine if it should, when <code>true</code>, log what it is doing to <code>stderr</code>.
* Defaults to <code>false</code>.</td>
* </tr>
* <tr>
* <td><code>LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER</code></td>
* <td><code>liquibase.launcher.parent_classloader</code></td>
* </tr>
* <tr>
* <td colspan="2">Classloader that will be used to run Liquibase, either <code>system</code> or <code>thread</code>.
* Defaults to <code>system</code>.</td>
* </tr>
* </tbody>
* </table>
*/
public class LiquibaseLauncher {

Expand All @@ -27,71 +68,72 @@ public class LiquibaseLauncher {

public static void main(final String[] args) throws Exception {

final String debugSetting = System.getenv("LIQUIBASE_LAUNCHER_DEBUG");
if (debugSetting != null && debugSetting.equals("true")) {
final String debugSetting = getSetting(LIQUIBASE_LAUNCHER_DEBUG);
if ("true".equals(debugSetting)) {
LiquibaseLauncher.debug = true;
debug("Debug mode enabled because LIQUIBASE_LAUNCHER_DEBUG is set to " + debugSetting);
debug("Debug mode enabled because either the JVM property 'liquibase.launcher.debug' or the environment " +
"variable 'LIQUIBASE_LAUNCHER_DEBUG' is set to " + debugSetting);
}

String parentLoaderSetting = System.getenv("LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER");
String parentLoaderSetting = getSetting(LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER);
if (parentLoaderSetting == null) {
parentLoaderSetting = "system";
}
debug("LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER is set to " + parentLoaderSetting);
debug("Liquibase launcher parent classloader is set to " + parentLoaderSetting);

final String liquibaseHomeEnv = System.getenv("LIQUIBASE_HOME");
debug("LIQUIBASE_HOME: " + liquibaseHomeEnv);
final String liquibaseHomeEnv = getSetting(LIQUIBASE_HOME);
debug("Liquibase home: " + liquibaseHomeEnv);
if (liquibaseHomeEnv == null || liquibaseHomeEnv.equals("")) {
throw new IllegalArgumentException("Unable to find LIQUIBASE_HOME environment variable");
throw new IllegalArgumentException("Unable to find either 'liquibase.home' JVM property nor " +
"'LIQUIBASE_HOME' environment variable");
}
File liquibaseHome = new File(liquibaseHomeEnv);

List<URL> urls = new ArrayList<>();
urls.add(new File(liquibaseHome, "internal/lib/liquibase-core.jar").toURI().toURL()); //make sure liquibase-core.jar is first in the list
List<URL> libUrls = getLibUrls(liquibaseHome);
checkForDuplicatedJars(libUrls);

File[] libDirs = new File[]{
new File("./liquibase_libs"),
new File(liquibaseHome, "lib"),
new File(liquibaseHome, "internal/lib"),
};

for (File libDirFile : libDirs) {
debug("Looking for libraries in " + libDirFile.getAbsolutePath());

if (!libDirFile.exists()) {
debug("Skipping directory " + libDirFile.getAbsolutePath() + " because it does not exist");
continue;
}
final File[] files = libDirFile.listFiles();
if (files == null) {
debug("Skipping directory " + libDirFile.getAbsolutePath() + " because it does not list files");
continue;
if (debug) {
debug("Final Classpath:");
for (URL url : libUrls) {
debug(" " + url.toString());
}
}

for (File lib : files) {
if (lib.getName().toLowerCase(Locale.US).endsWith(".jar") && !lib.getName().toLowerCase(Locale.US).equals("liquibase-core.jar")) {
try {
urls.add(lib.toURI().toURL());
debug("Added " + lib.getAbsolutePath() + " to classpath");
} catch (Exception e) {
debug("Error adding " + lib.getAbsolutePath() + ":" + e.getMessage(), e);
}
}
}
ClassLoader parentLoader = getClassLoader(parentLoaderSetting);

//add the dir itself
try {
urls.add(libDirFile.toURI().toURL());
debug("Added " + libDirFile.getAbsolutePath() + " to classpath");
} catch (Exception e) {
debug("Error adding " + libDirFile.getAbsolutePath() + ":" + e.getMessage(), e);
}
final URLClassLoader classloader = new URLClassLoader(libUrls.toArray(new URL[0]), parentLoader);
Thread.currentThread().setContextClassLoader(classloader);

Class<?> cli = null;
try {
cli = classloader.loadClass(LiquibaseCommandLine.class.getName());
} catch (ClassNotFoundException classNotFoundException) {
throw new RuntimeException(
String.format("Unable to find Liquibase classes in the configured home: '%s'.", liquibaseHome)
);
}

//
// Check for duplicate core and commercial JAR files
//
cli.getMethod("main", String[].class).invoke(null, new Object[]{args});
}

private static ClassLoader getClassLoader(String parentLoaderSetting) {
if (parentLoaderSetting.equalsIgnoreCase("system")) {
//loading with the regular system classloader includes liquibase.jar in the parent.
//That causes the parent classloader to load LiquibaseCommandLine which makes it not able to access files in the child classloader
//The system classloader's parent is the boot classloader, which keeps the only classloader with liquibase-core.jar the same as the rest of the classes it needs to access.
return ClassLoader.getSystemClassLoader().getParent();

} else if (parentLoaderSetting.equalsIgnoreCase("thread")) {
return Thread.currentThread().getContextClassLoader();
} else {
throw new RuntimeException("Unknown liquibase launcher parent classloader value: "+ parentLoaderSetting);
}
}

/**
* Check for duplicate core and commercial JAR files
*/
private static void checkForDuplicatedJars(List<URL> urls) {
List<String> duplicateCore =
urls
.stream()
Expand Down Expand Up @@ -130,32 +172,51 @@ public static void main(final String[] args) throws Exception {
buildDupsMessage(value, key);
}
});
if (debug) {
debug("Final Classpath:");
for (URL url : urls) {
debug(" " + url.toString());
}
}
}

ClassLoader parentLoader;
if (parentLoaderSetting.equalsIgnoreCase("system")) {
//loading with the regular system classloader includes liquibase.jar in the parent.
//That causes the parent classloader to load LiquibaseCommandLine which makes it not able to access files in the child classloader
//The system classloader's parent is the boot classloader, which keeps the only classloader with liquibase-core.jar the same as the rest of the classes it needs to access.
parentLoader = ClassLoader.getSystemClassLoader().getParent();
private static List<URL> getLibUrls(File liquibaseHome) throws MalformedURLException {
List<URL> urls = new ArrayList<>();
urls.add(new File(liquibaseHome, "internal/lib/liquibase-core.jar").toURI().toURL()); //make sure liquibase-core.jar is first in the list

} else if (parentLoaderSetting.equalsIgnoreCase("thread")) {
parentLoader = Thread.currentThread().getContextClassLoader();
} else {
throw new RuntimeException("Unknown LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER value: "+parentLoaderSetting);
}
File[] libDirs = new File[]{
new File("./liquibase_libs"),
new File(liquibaseHome, "lib"),
new File(liquibaseHome, "internal/lib"),
};

final URLClassLoader classloader = new URLClassLoader(urls.toArray(new URL[0]), parentLoader);
Thread.currentThread().setContextClassLoader(classloader);
for (File libDirFile : libDirs) {
debug("Looking for libraries in " + libDirFile.getAbsolutePath());

final Class<?> cli = classloader.loadClass(LiquibaseCommandLine.class.getName());
if (!libDirFile.exists()) {
debug("Skipping directory " + libDirFile.getAbsolutePath() + " because it does not exist");
continue;
}
final File[] files = libDirFile.listFiles();
if (files == null) {
debug("Skipping directory " + libDirFile.getAbsolutePath() + " because it does not list files");
continue;
}

cli.getMethod("main", String[].class).invoke(null, new Object[]{args});
for (File lib : files) {
if (lib.getName().toLowerCase(Locale.US).endsWith(".jar") && !lib.getName().toLowerCase(Locale.US).equals("liquibase-core.jar")) {
try {
urls.add(lib.toURI().toURL());
debug("Added " + lib.getAbsolutePath() + " to classpath");
} catch (Exception e) {
debug("Error adding " + lib.getAbsolutePath() + ":" + e.getMessage(), e);
}
}
}

//add the dir itself
try {
urls.add(libDirFile.toURI().toURL());
debug("Added " + libDirFile.getAbsolutePath() + " to classpath");
} catch (Exception e) {
debug("Error adding " + libDirFile.getAbsolutePath() + ":" + e.getMessage(), e);
}
}
return urls;
}

private static void buildDupsMessage(List<String> duplicates, String title) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package liquibase.integration.commandline;

/**
* Convenience class for reading well known Liquibase command line settings (System and/or environment properties).
*
* @see LiquibaseLauncher
*/
class LiquibaseLauncherSettings {

private static final String LIQUIBASE_HOME_JVM_PROPERTY_NAME = "liquibase.home";
private static final String LIQUIBASE_LAUNCHER_DEBUG_JVM_PROPERTY_NAME = "liquibase.launcher.debug";
private static final String LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER_JVM_PROPERTY_NAME = "liquibase.launcher.parent_classloader";

/**
* Agglutinates the different settings, i.e., environment variables or associated JVM system properties, that can be
* used for customizing the behavior of the class.
*/
enum LiquibaseLauncherSetting {
LIQUIBASE_HOME(LIQUIBASE_HOME_JVM_PROPERTY_NAME),
LIQUIBASE_LAUNCHER_DEBUG(LIQUIBASE_LAUNCHER_DEBUG_JVM_PROPERTY_NAME),
LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER(LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER_JVM_PROPERTY_NAME);

private final String jvmPropertyName;

LiquibaseLauncherSetting(String jvmPropertyName) {
this.jvmPropertyName = jvmPropertyName;
}

String getJvmPropertyName() {
return this.jvmPropertyName;
}
}

static String getSetting(LiquibaseLauncherSetting setting) {
String value = System.getProperty(setting.getJvmPropertyName());
if (value != null) {
return value;
}

return System.getenv(setting.name());
}
}

0 comments on commit 9790070

Please sign in to comment.