liquibase.integration.commandline.LiquibaseLauncher Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-core Show documentation
Show all versions of liquibase-core Show documentation
Liquibase is a tool for managing and executing database changes.
The newest version!
package liquibase.integration.commandline;
import liquibase.Scope;
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.util.LiquibaseLauncherSettings.LiquibaseLauncherSetting.*;
import static liquibase.util.LiquibaseLauncherSettings.getSetting;
/**
* Launcher which builds up the classpath needed to run Liquibase, then calls {@link LiquibaseCommandLine#main(String[])}.
*
* Supports the following configuration options that can be passed as JVM properties and/or environment variables, taking
* the former precedence over the latter:
*
*
*
* Environment variable
* JVM property
*
*
* Meaning
*
*
*
*
* LIQUIBASE_HOME
* liquibase.home
*
*
* Liquibase home. This option is mandatory.
*
*
* LIQUIBASE_LAUNCHER_DEBUG
* liquibase.launcher.debug
*
*
* Determine if it should, when true
, log what it is doing to stderr
.
* Defaults to false
.
*
*
* LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER
* liquibase.launcher.parent_classloader
*
*
* Classloader that will be used to run Liquibase, either system
or thread
.
* Defaults to system
.
*
*
*
*/
public class LiquibaseLauncher {
private static final String LIQUIBASE_CORE_JAR_PATTERN = ".*?/liquibase-core([-0-9.])*.jar";
private static final String LIQUIBASE_COMMERCIAL_JAR_PATTERN = ".*?/liquibase-commercial([-0-9.])*.jar";
private static final String LIQUIBASE_CORE_MESSAGE = "Liquibase Core";
private static final String LIQUIBASE_COMMERCIAL_MESSAGE = "Liquibase Commercial";
private static final String DEPENDENCY_JAR_VERSION_PATTERN = "(.*?)-?[0-9.]*.jar";
private static boolean debug = false;
public static void main(final String[] args) throws Exception {
final String debugSetting = getSetting(LIQUIBASE_LAUNCHER_DEBUG);
if ("true".equals(debugSetting)) {
LiquibaseLauncher.debug = true;
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 = getSetting(LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER);
if (parentLoaderSetting == null) {
parentLoaderSetting = "system";
}
debug("Liquibase launcher parent classloader is set to " + parentLoaderSetting);
final String liquibaseHomeEnv = getSetting(LIQUIBASE_HOME);
debug("Liquibase home: " + liquibaseHomeEnv);
if (liquibaseHomeEnv == null || liquibaseHomeEnv.equals("")) {
throw new IllegalArgumentException("Unable to find either 'liquibase.home' JVM property nor " +
"'LIQUIBASE_HOME' environment variable");
}
File liquibaseHome = new File(liquibaseHomeEnv);
List libUrls = getLibUrls(liquibaseHome);
checkForDuplicatedJars(libUrls);
if (debug) {
debug("Final Classpath:");
for (URL url : libUrls) {
debug(" " + url.toString());
}
}
ClassLoader parentLoader = getClassLoader(parentLoaderSetting);
final URLClassLoader classloader = new URLClassLoader(libUrls.toArray(new URL[0]), parentLoader);
Thread.currentThread().setContextClassLoader(classloader);
Class> cli;
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), classNotFoundException
);
}
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 urls) {
List duplicateCore =
urls
.stream()
.map(URL::getFile)
.filter(file -> file.matches(LIQUIBASE_CORE_JAR_PATTERN))
.collect(Collectors.toList());
List duplicateCommercial =
urls
.stream()
.map(URL::getFile)
.filter(file -> file.matches(LIQUIBASE_COMMERCIAL_JAR_PATTERN))
.collect(Collectors.toList());
if (duplicateCore.size() > 1) {
buildDupsMessage(duplicateCore, LIQUIBASE_CORE_MESSAGE);
}
if (duplicateCommercial.size() > 1) {
buildDupsMessage(duplicateCommercial, LIQUIBASE_COMMERCIAL_MESSAGE);
}
Map> duplicates = new LinkedHashMap<>();
findVersionedDuplicates(urls, Pattern.compile(DEPENDENCY_JAR_VERSION_PATTERN), duplicates);
findExactDuplicates(urls, duplicates);
duplicates.forEach((key, value) -> {
if (value.size() > 1) {
buildDupsMessage(value, key);
}
});
}
private static List getLibUrls(File liquibaseHome) throws MalformedURLException {
List 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
File[] libDirs = new File[]{
new File("./liquibase_libs"),
new File(liquibaseHome, "lib"),
new File(liquibaseHome, "internal/lib"),
new File(liquibaseHome, "internal/extensions"),
};
// We released libraries containing the version in the file name,
// and we want to ignore them in the classpath as the installer/zip/tgz is
// not able to update them .
List libsToIgnoreInClasspath = Arrays.asList(
new File(liquibaseHome, "internal/extensions/liquibase-commercial-bigquery-4.29.0.jar"),
new File(liquibaseHome, "internal/extensions/liquibase-commercial-bigquery-4.29.1.jar")
);
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;
}
for (File lib : files) {
if (lib.getName().toLowerCase(Locale.US).endsWith(".jar") && !lib.getName().toLowerCase(Locale.US).equals("liquibase-core.jar")) {
try {
if (libsToIgnoreInClasspath.stream().anyMatch(l -> l.getAbsoluteFile().equals(lib.getAbsoluteFile()))) {
debug("Ignoring " + lib.getAbsolutePath() + " in classpath");
continue; // skip the file if it is in the ignore list
}
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;
}
//
// Find duplicates that match pattern -, like snakeyaml-1.33.0 and snakeyaml-2.0
//
private static void findVersionedDuplicates(List urls, Pattern versionedJarPattern, Map> duplicates) {
urls
.forEach(url -> {
Matcher m = versionedJarPattern.matcher(new File(url.getFile()).getName());
if (m.find()) {
String key = m.group(1);
List dupEntries = duplicates.get(key);
if (dupEntries == null) {
dupEntries = new ArrayList<>();
}
dupEntries.add(url.getFile());
duplicates.put(key, dupEntries);
}
});
}
//
// Find exact duplicates in the list of URL
// skip core and commercial JARs and any directories
//
private static void findExactDuplicates(List urls, Map> duplicates) {
urls
.forEach(url -> {
String key = new File(url.getFile()).getName();
if (! (url.toString().endsWith("/") || url.toString().endsWith("\\") ||
key.contains("liquibase-core") || key.contains("liquibase-commercial"))) {
List dupEntries = duplicates.get(key);
if (dupEntries == null) {
dupEntries = new ArrayList<>();
}
dupEntries.add(url.getFile());
duplicates.put(key, dupEntries);
}
});
}
private static void buildDupsMessage(List duplicates, String title) {
String jarString = StringUtil.join(duplicates, System.lineSeparator());
String message = String.format("*** Duplicate %s JAR files ***%n%s", title, jarString);
Scope.getCurrentScope().getUI().sendMessage(String.format("WARNING: %s", message));
}
private static void debug(String message) {
if (debug) {
debug(message, null);
}
}
private static void debug(String message, Throwable e) {
if (debug) {
System.err.println("[LIQUIBASE LAUNCHER DEBUG] " + message);
if (e != null) {
e.printStackTrace(System.err);
}
}
}
}