Skip to content

Commit

Permalink
warn if XSD version does not match build version in validate command …
Browse files Browse the repository at this point in the history
…(DAT-9874) (#3016)

Warn if specified XSD version is different than the version of the build when running the validate command.
  • Loading branch information
StevenMassaro committed Jul 11, 2022
1 parent 4fb56c5 commit 896ca4b
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 1 deletion.
16 changes: 15 additions & 1 deletion liquibase-core/src/main/java/liquibase/Liquibase.java
Expand Up @@ -33,6 +33,7 @@
import liquibase.logging.core.CompositeLogService;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.parser.core.xml.XMLChangeLogSAXParser;
import liquibase.resource.InputStreamList;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.ChangeLogSerializer;
Expand Down Expand Up @@ -361,8 +362,21 @@ public Connection getConnection(DatabaseChangeLog changeLog) throws LiquibaseHub


public DatabaseChangeLog getDatabaseChangeLog() throws LiquibaseException {
return getDatabaseChangeLog(false);
}

/**
* @param shouldWarnOnMismatchedXsdVersion When set to true, a warning will be printed to the console if the XSD
* version used does not match the version of Liquibase. If "latest" is used
* as the XSD version, no warning is printed. If the changelog is not xml
* format, no warning is printed.
*/
private DatabaseChangeLog getDatabaseChangeLog(boolean shouldWarnOnMismatchedXsdVersion) throws LiquibaseException {
if (databaseChangeLog == null && changeLogFile != null) {
ChangeLogParser parser = ChangeLogParserFactory.getInstance().getParser(changeLogFile, resourceAccessor);
if (parser instanceof XMLChangeLogSAXParser) {
((XMLChangeLogSAXParser) parser).setShouldWarnOnMismatchedXsdVersion(shouldWarnOnMismatchedXsdVersion);
}
databaseChangeLog = parser.parse(changeLogFile, changeLogParameters, resourceAccessor);
}

Expand Down Expand Up @@ -2273,7 +2287,7 @@ public DiffResult diff(Database referenceDatabase, Database targetDatabase, Comp
*/
public void validate() throws LiquibaseException {

DatabaseChangeLog changeLog = getDatabaseChangeLog();
DatabaseChangeLog changeLog = getDatabaseChangeLog(true);
changeLog.validate(database);
}

Expand Down
Expand Up @@ -6,12 +6,15 @@
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.InputStreamList;
import liquibase.resource.ResourceAccessor;
import liquibase.util.LiquibaseUtil;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.ext.EntityResolver2;

import java.io.IOException;
import java.io.InputStream;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
* Finds the Liquibase schema from the classpath rather than fetching it over the Internet.
Expand All @@ -20,6 +23,11 @@
public class LiquibaseEntityResolver implements EntityResolver2 {

private static ClassLoaderResourceAccessor fallbackResourceAccessor;
private boolean shouldWarnOnMismatchedXsdVersion = false;
/**
* The warning message should only be printed once.
*/
private static boolean hasWarnedAboutMismatchedXsdVersion = false;

@Override
@java.lang.SuppressWarnings("squid:S2095")
Expand All @@ -37,6 +45,10 @@ public InputSource resolveEntity(String name, String publicId, String baseURI, S
.replace("http://www.liquibase.org/xml/ns/migrator/", "http://www.liquibase.org/xml/ns/dbchangelog/")
.replaceFirst("https?://", "");

if (shouldWarnOnMismatchedXsdVersion && !hasWarnedAboutMismatchedXsdVersion) {
warnForMismatchedXsdVersion(systemId);
}

ResourceAccessor resourceAccessor = Scope.getCurrentScope().getResourceAccessor();
InputStreamList streams = resourceAccessor.openStreams(null, path);
if (streams.isEmpty()) {
Expand Down Expand Up @@ -70,6 +82,32 @@ public InputSource resolveEntity(String name, String publicId, String baseURI, S

}

/**
* Print a warning message to the logs and UI if the build version does not match the XSD version. This is a best
* effort check, this method will never throw an exception.
*/
private void warnForMismatchedXsdVersion(String systemId) {
try {
Pattern versionPattern = Pattern.compile("(?:-pro-|-)(?<version>[\\d.]*)\\.xsd");
Matcher versionMatcher = versionPattern.matcher(systemId);
boolean found = versionMatcher.find();
if (found) {
String buildVersion = LiquibaseUtil.getBuildVersion();
if (!buildVersion.equals("DEV")) {
String xsdVersion = versionMatcher.group("version");
if (!buildVersion.startsWith(xsdVersion)) {
hasWarnedAboutMismatchedXsdVersion = true;
String msg = "INFO: An older version of the XSD is specified in one or more changelog's <databaseChangeLog> header. This can lead to unexpected outcomes. If a specific XSD is not required, please replace all XSD version references with \"-latest\". Learn more at https://docs.liquibase.com";
Scope.getCurrentScope().getLog(getClass()).info(msg);
Scope.getCurrentScope().getUI().sendMessage(msg);
}
}
}
} catch (Exception e) {
Scope.getCurrentScope().getLog(getClass()).fine("Failed to compare XSD version with build version.", e);
}
}

/**
* ResourceAccessor to use if the standard one does not have the XSD files in it.
* Returns a ClassLoaderResourceAccessor that checks the system classloader which should include the liquibase.jar.
Expand All @@ -91,4 +129,12 @@ public InputSource resolveEntity(String publicId, String systemId) throws SAXExc
Scope.getCurrentScope().getLog(getClass()).warning("The current XML parser does not seems to not support EntityResolver2. External entities may not be correctly loaded");
return resolveEntity(null, publicId, null, systemId);
}

/**
* When set to true, a warning will be printed to the console if the XSD version used does not match the version
* of Liquibase. If "latest" is used as the XSD version, no warning is printed.
*/
public void setShouldWarnOnMismatchedXsdVersion(boolean shouldWarnOnMismatchedXsdVersion) {
this.shouldWarnOnMismatchedXsdVersion = shouldWarnOnMismatchedXsdVersion;
}
}
Expand Up @@ -59,6 +59,14 @@ protected SAXParserFactory getSaxParserFactory() {
return saxParserFactory;
}

/**
* When set to true, a warning will be printed to the console if the XSD version used does not match the version
* of Liquibase. If "latest" is used as the XSD version, no warning is printed.
*/
public void setShouldWarnOnMismatchedXsdVersion(boolean shouldWarnOnMismatchedXsdVersion) {
resolver.setShouldWarnOnMismatchedXsdVersion(shouldWarnOnMismatchedXsdVersion);
}

@Override
protected ParsedNode parseToNode(String physicalChangeLogLocation, ChangeLogParameters changeLogParameters, ResourceAccessor resourceAccessor) throws ChangeLogParseException {
try (InputStream inputStream = resourceAccessor.openStream(null, physicalChangeLogLocation)) {
Expand Down
@@ -1,8 +1,11 @@
package liquibase.parser.core.xml

import liquibase.GlobalConfiguration
import liquibase.LiquibaseTest
import liquibase.Scope
import liquibase.resource.FileSystemResourceAccessor
import liquibase.util.LiquibaseUtil
import org.xml.sax.InputSource
import spock.lang.Specification
import spock.lang.Unroll

Expand All @@ -25,6 +28,48 @@ class LiquibaseEntityResolverTest extends Specification {
]
}

@Unroll
def "warning message for mismatched xsd and build versions #systemId /// #buildVersion"() {
given:
def uiService = new LiquibaseTest.TestConsoleUIService()
// Save these props for later
def originalProperties = LiquibaseUtil.liquibaseBuildProperties
LiquibaseUtil.liquibaseBuildProperties = new Properties()
LiquibaseUtil.liquibaseBuildProperties.put("build.version", buildVersion)
def er = new LiquibaseEntityResolver()
er.setShouldWarnOnMismatchedXsdVersion(true)
er.hasWarnedAboutMismatchedXsdVersion = false

expect:
Scope.child([
(Scope.Attr.ui.name()) : uiService
], {
er.resolveEntity(null, null, null, systemId)
} as Scope.ScopedRunnerWithReturn<InputSource>) != null

// This is an ugly assertion line, it is essentially saying, either we expect the message, so make sure it's there
// or we expect no message, so make sure there are no messages.
((expectedWarningMessage && uiService.getMessages().contains("INFO: An older version of the XSD is specified in one or more changelog's <databaseChangeLog> header. This can lead to unexpected outcomes. If a specific XSD is not required, please replace all XSD version references with \"-latest\". Learn more at https://docs.liquibase.com"))
|| (!expectedWarningMessage && uiService.getMessages().isEmpty()))

cleanup:
// Set the build properties back to what they were before the test.
LiquibaseUtil.liquibaseBuildProperties = originalProperties

where:
buildVersion | systemId | expectedWarningMessage
"3.1.0" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd" | false
"3.1.1" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd" | false
"4.12.0" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd" | true
"4.12.0" | "https://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd" | true
"4.12.0" | "http://www.liquibase.org/xml/ns/migrator/dbchangelog-3.1.xsd" | true
"4.12.0" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-next.xsd" | false
"4.12.0" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd" | false
"4.12.0" | "http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd" | false
"4.12.0" | "/liquibase/banner.txt" | false
"4.12.0" | "http://liquibase/banner.txt" | false
}

@Unroll
def "resolveEntity finds packaged files correctly even if the configured resourceAccessor doesn't have it"() {
expect:
Expand Down

0 comments on commit 896ca4b

Please sign in to comment.