Skip to content

Commit

Permalink
Added liquibase.changelogParseMode setting (#3057)
Browse files Browse the repository at this point in the history
Added LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER setting for LiquibaseLauncher to better support running in IDEs
Added new liquibase.changelogParseMode setting for unknown change and precondition types
  • Loading branch information
nvoxland committed Jul 20, 2022
1 parent 8cff0ce commit eb62560
Show file tree
Hide file tree
Showing 13 changed files with 145 additions and 274 deletions.
Expand Up @@ -24,6 +24,12 @@ public static void main(final String[] args) throws Exception {
debug("Debug mode enabled because LIQUIBASE_LAUNCHER_DEBUG is set to " + debugSetting);
}

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

final String liquibaseHomeEnv = System.getenv("LIQUIBASE_HOME");
debug("LIQUIBASE_HOME: " + liquibaseHomeEnv);
if (liquibaseHomeEnv == null || liquibaseHomeEnv.equals("")) {
Expand Down Expand Up @@ -80,10 +86,20 @@ public static void main(final String[] args) throws Exception {
}
}

//loading with the regular system classloader includes liquibase.jar in the parent.
//That causes the parent classloader to load LiqiuabaseCommandLine 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.jar the same as the rest of the classes it needs to access.
final URLClassLoader classloader = new URLClassLoader(urls.toArray(new URL[0]), ClassLoader.getSystemClassLoader().getParent());
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.jar the same as the rest of the classes it needs to access.
parentLoader = ClassLoader.getSystemClassLoader().getParent();

} else if (parentLoaderSetting.equalsIgnoreCase("thread")) {
parentLoader = Thread.currentThread().getContextClassLoader();
} else {
throw new RuntimeException("Unknown LIQUIBASE_LAUNCHER_PARENT_CLASSLOADER value: "+parentLoaderSetting);
}

final URLClassLoader classloader = new URLClassLoader(urls.toArray(new URL[0]), parentLoader);
Thread.currentThread().setContextClassLoader(classloader);

final Class<?> cli = classloader.loadClass(LiquibaseCommandLine.class.getName());
Expand Down
53 changes: 27 additions & 26 deletions liquibase-core/src/main/java/liquibase/changelog/ChangeSet.java
@@ -1,6 +1,7 @@
package liquibase.changelog;

import liquibase.ContextExpression;
import liquibase.GlobalConfiguration;
import liquibase.Labels;
import liquibase.Scope;
import liquibase.change.*;
Expand All @@ -16,6 +17,7 @@
import liquibase.executor.ExecutorService;
import liquibase.executor.LoggingExecutor;
import liquibase.logging.Logger;
import liquibase.parser.ChangeLogParserConfiguration;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.precondition.Conditional;
Expand Down Expand Up @@ -194,7 +196,7 @@ public String toString() {
*/
private PreconditionContainer preconditions;

/**
/**
* ChangeSet level attribute to specify an Executor
*/
private String runWith;
Expand Down Expand Up @@ -276,6 +278,7 @@ public String getFilePath() {

/**
* The logical file path defined directly on this node. Return null if not set.
*
* @return
*/
public String getLogicalFilePath() {
Expand Down Expand Up @@ -367,7 +370,7 @@ public void load(ParsedNode node, ResourceAccessor resourceAccessor) throws Pars
}
} else {
filePath = filePath.replaceAll("\\\\", "/")
.replaceFirst("^/", "");
.replaceFirst("^/", "");

}

Expand Down Expand Up @@ -440,11 +443,7 @@ protected void handleChildNode(ParsedNode child, ResourceAccessor resourceAccess
break;
case "preConditions":
this.preconditions = new PreconditionContainer();
try {
this.preconditions.load(child, resourceAccessor);
} catch (ParsedNodeException e) {
e.printStackTrace();
}
this.preconditions.load(child, resourceAccessor);
break;
case "changes":
for (ParsedNode changeNode : child.getChildren()) {
Expand Down Expand Up @@ -517,6 +516,14 @@ protected void handleRollbackNode(ParsedNode rollbackNode, ResourceAccessor reso
protected Change toChange(ParsedNode value, ResourceAccessor resourceAccessor) throws ParsedNodeException {
Change change = Scope.getCurrentScope().getSingleton(ChangeFactory.class).create(value.getName());
if (change == null) {
if (value.getChildren().size() > 0 && ChangeLogParserConfiguration.CHANGELOG_PARSE_MODE.getCurrentValue().equals(ChangeLogParserConfiguration.ChangelogParseMode.STRICT)) {
String message = "";
if (this.getChangeLog() != null && this.getChangeLog().getPhysicalFilePath() != null) {
message = "Error parsing " + this.getChangeLog().getPhysicalFilePath() + ": ";
}
message += "Unknown change type '" + value.getName() + "'. Check for spelling or capitalization errors and missing extensions such as liquibase-commercial.";
throw new ParsedNodeException(message);
}
return null;
} else {
change.load(value, resourceAccessor);
Expand Down Expand Up @@ -730,7 +737,7 @@ private Executor setupCustomExecutorIfNecessary(Database database) {
Scope.getCurrentScope().getSingleton(ExecutorService.class).setExecutor("jdbc", database, customExecutor);
List<Change> changes = getChanges();
for (Change change : changes) {
if (! (change instanceof AbstractChange)) {
if (!(change instanceof AbstractChange)) {
continue;
}
final ResourceAccessor resourceAccessor = ((AbstractChange) change).getResourceAccessor();
Expand All @@ -743,21 +750,19 @@ private Executor setupCustomExecutorIfNecessary(Database database) {
}

/**
*
* Look for a configuration property that matches liquibase.<executor name>.executor
* and if found, return its value as the executor name
*
* @param executorName The value from the input changeset runWith attribute
* @return String The mapped value
*
* @param executorName The value from the input changeset runWith attribute
* @return String The mapped value
*/
public static String lookupExecutor(String executorName) {
if (StringUtil.isEmpty(executorName)) {
return null;
}
String key = "liquibase." + executorName.toLowerCase() + ".executor";
String replacementExecutorName =
(String)Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class).getCurrentConfiguredValue(null, null, key).getValue();
(String) Scope.getCurrentScope().getSingleton(LiquibaseConfiguration.class).getCurrentConfiguredValue(null, null, key).getValue();
if (replacementExecutorName != null) {
Scope.getCurrentScope().getLog(ChangeSet.class).info("Mapped '" + executorName + "' to executor '" + replacementExecutorName + "'");
return replacementExecutorName;
Expand Down Expand Up @@ -906,7 +911,7 @@ public void setIgnore(boolean ignore) {
}

public boolean isInheritableIgnore() {
DatabaseChangeLog changeLog = getChangeLog();
DatabaseChangeLog changeLog = getChangeLog();
if (changeLog == null) {
return false;
}
Expand Down Expand Up @@ -945,11 +950,9 @@ public Collection<Labels> getInheritableLabels() {
}

/**
*
* Build and return a string which contains both the changeset and inherited context
*
* @return String
*
* @return String
*/
public String buildFullContext() {
StringBuilder contextExpression = new StringBuilder();
Expand All @@ -966,11 +969,9 @@ public String buildFullContext() {
}

/**
*
* Build and return a string which contains both the changeset and inherited labels
*
* @return String
*
* @return String
*/
public String buildFullLabels() {
StringBuilder labels = new StringBuilder();
Expand Down Expand Up @@ -1219,11 +1220,11 @@ public String getSerializedObjectName() {
@Override
public Set<String> getSerializableFields() {
return new LinkedHashSet<>(
Arrays.asList(
"id", "author", "runAlways", "runOnChange", "failOnError", "context", "labels", "dbms",
"objectQuotingStrategy", "comment", "preconditions", "changes", "rollback", "labels",
"logicalFilePath", "created", "runInTransaction", "runOrder", "ignore"
)
Arrays.asList(
"id", "author", "runAlways", "runOnChange", "failOnError", "context", "labels", "dbms",
"objectQuotingStrategy", "comment", "preconditions", "changes", "rollback", "labels",
"logicalFilePath", "created", "runInTransaction", "runOrder", "ignore"
)
);
}

Expand Down Expand Up @@ -1308,7 +1309,7 @@ public Object getSerializableFieldValue(String field) {
}

if ("logicalFilePath".equals(field)) {
return getLogicalFilePath();
return getLogicalFilePath();
}

if ("rollback".equals(field)) {
Expand Down
Expand Up @@ -426,13 +426,9 @@ protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccesso
}
case "preConditions": {
PreconditionContainer parsedContainer = new PreconditionContainer();
try {
parsedContainer.load(node, resourceAccessor);
this.preconditionContainer.addNestedPrecondition(parsedContainer);
parsedContainer.load(node, resourceAccessor);
this.preconditionContainer.addNestedPrecondition(parsedContainer);

} catch (ParsedNodeException e) {
e.printStackTrace();
}
break;
}
case "property": {
Expand Down
@@ -1,5 +1,6 @@
package liquibase.parser;

import liquibase.GlobalConfiguration;
import liquibase.configuration.AutoloadedConfigurations;
import liquibase.configuration.ConfigurationDefinition;

Expand All @@ -12,6 +13,9 @@ public class ChangeLogParserConfiguration implements AutoloadedConfigurations {
public static final ConfigurationDefinition<Boolean> USE_PROCEDURE_SCHEMA;
public static final ConfigurationDefinition<MissingPropertyMode> MISSING_PROPERTY_MODE;

public static final ConfigurationDefinition<ChangelogParseMode> CHANGELOG_PARSE_MODE;


static {
ConfigurationDefinition.Builder builder = new ConfigurationDefinition.Builder("liquibase");

Expand All @@ -30,11 +34,23 @@ public class ChangeLogParserConfiguration implements AutoloadedConfigurations {
.setDescription("How to handle changelog property expressions where a value is not set. For example, a string '${address}' when no 'address' property was defined. Values can be: 'preserve' which leaves the string as-is, 'empty' which replaces it with an empty string, or 'error' which stops processing with an error.")
.setDefaultValue(MissingPropertyMode.PRESERVE)
.build();


CHANGELOG_PARSE_MODE = builder.define("changelogParseMode", ChangelogParseMode.class)
.setDescription("Configures how to handle unknown fields in changelog files. Possible values: STRICT which causes parsing to fail, and LAX which continues with the parsing.")
.setDefaultValue(ChangelogParseMode.STRICT)
.build();

}

public enum MissingPropertyMode {
PRESERVE,
EMPTY,
ERROR,
}

public enum ChangelogParseMode {
STRICT,
LAX,
}
}
@@ -1,7 +1,9 @@
package liquibase.precondition;

import liquibase.GlobalConfiguration;
import liquibase.database.Database;
import liquibase.exception.ValidationErrors;
import liquibase.parser.ChangeLogParserConfiguration;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.resource.ResourceAccessor;
Expand Down Expand Up @@ -47,6 +49,10 @@ public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throw
protected Precondition toPrecondition(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
Precondition precondition = PreconditionFactory.getInstance().create(node.getName());
if (precondition == null) {
if (node.getChildren() != null && node.getChildren().size() > 0 && ChangeLogParserConfiguration.CHANGELOG_PARSE_MODE.getCurrentValue().equals(ChangeLogParserConfiguration.ChangelogParseMode.STRICT)) {
throw new ParsedNodeException("Unknown precondition '" + node.getName() + "'. Check for spelling or capitalization errors and missing extensions such as liquibase-commercial.");
}

return null;
}

Expand Down
@@ -1,7 +1,10 @@
package liquibase.changelog


import liquibase.Scope
import liquibase.change.CheckSum
import liquibase.change.core.*
import liquibase.parser.ChangeLogParserConfiguration
import liquibase.parser.core.ParsedNode
import liquibase.parser.core.ParsedNodeException
import liquibase.precondition.core.RunningAsPrecondition
Expand Down Expand Up @@ -187,6 +190,39 @@ public class ChangeSetTest extends Specification {
changeSet.changes[1].tableName == "table_2"
}

def "load node with unknown change types and strict parsing"() {
when:
def changeSet = new ChangeSet(new DatabaseChangeLog("com/example/test.xml"))
def node = new ParsedNode(null, "changeSet")
.addChildren([id: "1", author: "nvoxland"])
.addChild(new ParsedNode(null, "createTable").addChild(null, "tableName", "table_1"))
.addChild(new ParsedNode(null, "invalid").addChild(null, "tableName", "table_2"))
changeSet.load(node, resourceSupplier.simpleResourceAccessor)

then:
def e = thrown(ParsedNodeException)
e.message == "Error parsing com/example/test.xml: Unknown change type 'invalid'. Check for spelling or capitalization errors and missing extensions such as liquibase-commercial."
}

def "load node with unknown change types and lax parsing"() {
when:
def changeSet = new ChangeSet(new DatabaseChangeLog("com/example/test.xml"))
def node = new ParsedNode(null, "changeSet")
.addChildren([id: "1", author: "nvoxland"])
.addChild(new ParsedNode(null, "createTable").addChild(null, "tableName", "table_1"))
.addChild(new ParsedNode(null, "invalid").addChild(null, "tableName", "table_2"))

Scope.child([(ChangeLogParserConfiguration.CHANGELOG_PARSE_MODE.getKey()): ChangeLogParserConfiguration.ChangelogParseMode.LAX], {
->
changeSet.load(node, resourceSupplier.simpleResourceAccessor)
} as Scope.ScopedRunner)


then:
notThrown(ParsedNodeException)
changeSet.getChanges().size() == 1
}

def "load node with rollback containing sql as value"() {
when:
def changeSet = new ChangeSet(new DatabaseChangeLog("com/example/test.xml"))
Expand Down
Expand Up @@ -300,29 +300,6 @@ public class YamlChangeLogParser_RealFile_Test extends Specification {
assert e.message.startsWith("Syntax error in file liquibase/parser/core/yaml/malformedChangeLog.yaml")
}

def "elements that don't correspond to anything in liquibase are ignored"() throws Exception {
def path = "liquibase/parser/core/yaml/unusedTagsChangeLog.yaml"
expect:
DatabaseChangeLog changeLog = new YamlChangeLogParser().parse(path, new ChangeLogParameters(), new JUnitResourceAccessor());

changeLog.getLogicalFilePath() == path
changeLog.getPhysicalFilePath() == path

changeLog.getPreconditions().getNestedPreconditions().size() == 0
changeLog.getChangeSets().size() == 1

ChangeSet changeSet = changeLog.getChangeSets().get(0);
changeSet.getAuthor() == "nvoxland"
changeSet.getId() == "1"
changeSet.getChanges().size() == 1
changeSet.getFilePath() == path
changeSet.getComments() == "Some comments go here"

Change change = changeSet.getChanges().get(0);
Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(change).getName() == "createTable"
assert change instanceof CreateTableChange
}

def "changeLog parameters are correctly expanded"() throws Exception {
when:
def params = new ChangeLogParameters(new MockDatabase());
Expand Down

0 comments on commit eb62560

Please sign in to comment.