Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added liquibase.changelogParseMode setting #3057

Merged
merged 10 commits into from Jul 20, 2022
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
12 changes: 12 additions & 0 deletions liquibase-core/src/main/java/liquibase/GlobalConfiguration.java
Expand Up @@ -34,6 +34,8 @@ public class GlobalConfiguration implements AutoloadedConfigurations {

public static final ConfigurationDefinition<DuplicateFileMode> DUPLICATE_FILE_MODE;

public static final ConfigurationDefinition<ChangelogParseMode> CHANGELOG_PARSE_MODE;

/**
* @deprecated No longer used
*/
Expand Down Expand Up @@ -210,10 +212,20 @@ public class GlobalConfiguration implements AutoloadedConfigurations {
SEARCH_PATH = builder.define("searchPath", String.class)
.setDescription("Complete list of Location(s) to search for files such as changelog files in. Multiple paths can be specified by separating them with commas.")
.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 DuplicateFileMode {
WARN,
ERROR,
}

public enum ChangelogParseMode {
STRICT,
LAX,
}
}
46 changes: 25 additions & 21 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 Down Expand Up @@ -194,7 +195,7 @@ public String toString() {
*/
private PreconditionContainer preconditions;

/**
/**
* ChangeSet level attribute to specify an Executor
*/
private String runWith;
Expand Down Expand Up @@ -276,6 +277,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 +369,7 @@ public void load(ParsedNode node, ResourceAccessor resourceAccessor) throws Pars
}
} else {
filePath = filePath.replaceAll("\\\\", "/")
.replaceFirst("^/", "");
.replaceFirst("^/", "");

}

Expand Down Expand Up @@ -517,6 +519,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 && GlobalConfiguration.CHANGELOG_PARSE_MODE.getCurrentValue().equals(GlobalConfiguration.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 +740,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 +753,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 +914,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 +953,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 +972,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 +1223,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 +1312,7 @@ public Object getSerializableFieldValue(String field) {
}

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

if ("rollback".equals(field)) {
Expand Down
@@ -1,5 +1,6 @@
package liquibase.precondition;

import liquibase.GlobalConfiguration;
import liquibase.database.Database;
import liquibase.exception.ValidationErrors;
import liquibase.parser.core.ParsedNode;
Expand Down Expand Up @@ -47,6 +48,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 && GlobalConfiguration.CHANGELOG_PARSE_MODE.getCurrentValue().equals(GlobalConfiguration.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.GlobalConfiguration
import liquibase.Scope
import liquibase.change.CheckSum
import liquibase.change.core.*
import liquibase.exception.ChangeLogParseException
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([(GlobalConfiguration.CHANGELOG_PARSE_MODE.getKey()) : GlobalConfiguration.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
@@ -1,5 +1,7 @@
package liquibase.precondition.core

import liquibase.GlobalConfiguration
import liquibase.Scope
import liquibase.exception.SetupException
import liquibase.parser.core.ParsedNode
import liquibase.parser.core.ParsedNodeException
Expand All @@ -9,7 +11,8 @@ import spock.lang.Specification

class PreconditionContainerTest extends Specification {

@Shared resourceSupplier = new ResourceSupplier()
@Shared
resourceSupplier = new ResourceSupplier()

def "load handles empty node with params"() {
when:
Expand Down Expand Up @@ -101,8 +104,8 @@ class PreconditionContainerTest extends Specification {
def node = new ParsedNode(null, "preConditions").addChildren([onFail: "MARK_RAN"])
.addChild(new ParsedNode(null, "runningAs").addChildren([username: "my_user"]))
.addChild(new ParsedNode(null, "or")
.addChildren([runningAs: [username: "other_user"]])
.addChildren([runningAs: [username: "yet_other_user"]])
.addChildren([runningAs: [username: "other_user"]])
.addChildren([runningAs: [username: "yet_other_user"]])
)
.addChild(new ParsedNode(null, "tableExists").addChildren([tableName: "my_table"]))

Expand All @@ -128,4 +131,35 @@ class PreconditionContainerTest extends Specification {

}

def "load handles node with unknown preconditions in strict mode"() {
when:
def node = new ParsedNode(null, "preConditions").addChildren([onFail: "MARK_RAN"])
.addChild(new ParsedNode(null, "invalid").addChildren([tableName: "my_table"]))
.addChild(new ParsedNode(null, "tableExists").addChildren([tableName: "my_table"]))

def container = new PreconditionContainer()
container.load(node, resourceSupplier.simpleResourceAccessor)

then:
def e = thrown(ParsedNodeException)
e.message == "Unknown precondition 'invalid'. Check for spelling or capitalization errors and missing extensions such as liquibase-commercial."
}

def "load handles node with unknown preconditions in lax mode"() {
when:
def node = new ParsedNode(null, "preConditions").addChildren([onFail: "MARK_RAN"])
.addChild(new ParsedNode(null, "invalid").addChildren([tableName: "my_table"]))
.addChild(new ParsedNode(null, "tableExists").addChildren([tableName: "my_table"]))

def container = new PreconditionContainer()
Scope.currentScope.child([(GlobalConfiguration.CHANGELOG_PARSE_MODE.key): GlobalConfiguration.ChangelogParseMode.LAX], {
->
container.load(node, resourceSupplier.simpleResourceAccessor)
} as Scope.ScopedRunner)


then:
notThrown(ParsedNodeException)
container.getNestedPreconditions().size() == 1
}
}