diff --git a/liquibase-core/src/main/java/liquibase/Liquibase.java b/liquibase-core/src/main/java/liquibase/Liquibase.java index a34ce05e540..7d29b0ab54d 100644 --- a/liquibase-core/src/main/java/liquibase/Liquibase.java +++ b/liquibase-core/src/main/java/liquibase/Liquibase.java @@ -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; @@ -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); } @@ -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); } diff --git a/liquibase-core/src/main/java/liquibase/parser/core/xml/LiquibaseEntityResolver.java b/liquibase-core/src/main/java/liquibase/parser/core/xml/LiquibaseEntityResolver.java index 8419bfa0d6f..b79279d2c09 100644 --- a/liquibase-core/src/main/java/liquibase/parser/core/xml/LiquibaseEntityResolver.java +++ b/liquibase-core/src/main/java/liquibase/parser/core/xml/LiquibaseEntityResolver.java @@ -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. @@ -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") @@ -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()) { @@ -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-|-)(?[\\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 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. @@ -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; + } } diff --git a/liquibase-core/src/main/java/liquibase/parser/core/xml/XMLChangeLogSAXParser.java b/liquibase-core/src/main/java/liquibase/parser/core/xml/XMLChangeLogSAXParser.java index 5c4e78467ca..e39e750f923 100644 --- a/liquibase-core/src/main/java/liquibase/parser/core/xml/XMLChangeLogSAXParser.java +++ b/liquibase-core/src/main/java/liquibase/parser/core/xml/XMLChangeLogSAXParser.java @@ -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)) { diff --git a/liquibase-core/src/test/groovy/liquibase/parser/core/xml/LiquibaseEntityResolverTest.groovy b/liquibase-core/src/test/groovy/liquibase/parser/core/xml/LiquibaseEntityResolverTest.groovy index 222bc5941d1..05f1e03959a 100644 --- a/liquibase-core/src/test/groovy/liquibase/parser/core/xml/LiquibaseEntityResolverTest.groovy +++ b/liquibase-core/src/test/groovy/liquibase/parser/core/xml/LiquibaseEntityResolverTest.groovy @@ -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 @@ -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) != 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 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: