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

warn if XSD version does not match build version in validate command (DAT-9874) #3016

Merged
merged 5 commits into from Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -364,8 +365,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 @@ -2278,7 +2292,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