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

[DAT-16377] Verify if supports method is implemented in a change to prevent unexpected behaviors #5903

Merged
merged 21 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
48062f9
chore: define a constant value for LiquibaseUtil.DEV_VERSION
filipelautert May 13, 2024
27d816f
chore: Verify if the supports method is implemented in the change if …
filipelautert May 13, 2024
8e8726a
chore: Fix tests
filipelautert May 13, 2024
5a03ba8
chore: lombok + typo
filipelautert May 13, 2024
9fa290e
chore: formatting
filipelautert May 13, 2024
3b47101
chore: adding unit tests
filipelautert May 13, 2024
9283f12
chore: revert if condition for clarity
filipelautert May 13, 2024
69469d4
chore: dev should be checked before flags
filipelautert May 14, 2024
5b89de9
Revert "chore: adding unit tests"
filipelautert May 14, 2024
3d53b59
chore: do not ignore pro-package
filipelautert May 14, 2024
655a16c
Revert "Revert "chore: adding unit tests""
filipelautert May 14, 2024
6d3db58
chore: mock ServiceLocator instead of loading it
filipelautert May 14, 2024
928bdf9
chore: try to reset scope manager after execution.
filipelautert May 14, 2024
5e6107d
chore: null scope manager
filipelautert May 14, 2024
93375de
chore: rename var + tests
filipelautert May 14, 2024
7645f1c
chore(tests): lower priority otherwise it gets in the way of other Se…
filipelautert May 14, 2024
a43eb42
chore: fix all tests using reflection
filipelautert May 15, 2024
974bff3
chore: fix param name in tests
filipelautert May 15, 2024
e90bd94
Update liquibase-standard/src/main/java/liquibase/change/ChangeFactor…
filipelautert May 15, 2024
e1e4f8f
chore: we should not remove the plugin in warn level.
filipelautert May 15, 2024
c785d44
chore: fix test order
filipelautert May 15, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,16 @@ Global Options
supportPropertyEscaping', environment variable:
'LIQUIBASE_SUPPORT_PROPERTY_ESCAPING')

--supports-method-validation-levels=PARAM
filipelautert marked this conversation as resolved.
Show resolved Hide resolved
Controls the level of validation performed on the
supports method of Change classes. Options are
OFF, WARN, FAIL.
DEFAULT: WARN
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should the default be FAIL so that we notice if we violate this rule when we run tests in all of the extensions? If we just warn, we probably won't ever actually notice until runtime, at which point it will be too late.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could change to FAIL, but this could also break existing extensions. I think we should start with WARN for 1-2 releases and then switch to FAIL - I can create a dat ticket for that.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do and put it in a future sprint.

(defaults file: 'liquibase.
supportsMethodValidationLevels', environment
variable:
'LIQUIBASE_SUPPORTS_METHOD_VALIDATION_LEVELS')

--ui-service=PARAM Changes the default UI Service Logger used by
Liquibase. Options are CONSOLE or LOGGER.
DEFAULT: CONSOLE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ public class GlobalConfiguration implements AutoloadedConfigurations {
public static final ConfigurationDefinition<String> SEARCH_PATH;

public static final ConfigurationDefinition<UIServiceEnum> UI_SERVICE;
public static final ConfigurationDefinition<SupportsMethodValidationLevelsEnum> SUPPORTS_METHOD_VALIDATION_LEVELS;

static {
ConfigurationDefinition.Builder builder = new ConfigurationDefinition.Builder("liquibase");
Expand Down Expand Up @@ -242,10 +243,16 @@ public class GlobalConfiguration implements AutoloadedConfigurations {
.setDescription("Changes the default UI Service Logger used by Liquibase. Options are CONSOLE or LOGGER.")
.setDefaultValue(UIServiceEnum.CONSOLE)
.build();

SUPPORTS_METHOD_VALIDATION_LEVELS = builder.define("supportsMethodValidationLevels", SupportsMethodValidationLevelsEnum.class)
.setDescription("Controls the level of validation performed on the supports method of Change classes. Options are OFF, WARN, FAIL.")
.setDefaultValue(SupportsMethodValidationLevelsEnum.WARN)
.build();
}

public enum DuplicateFileMode {
WARN,
ERROR,
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package liquibase;

/**
* Enum to control the level of validation to check if a change's supports method is properly implemented.
*/
public enum SupportsMethodValidationLevelsEnum {
OFF,
WARN,
FAIL
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package liquibase.change;

import liquibase.ChecksumVersion;
import liquibase.GlobalConfiguration;
import liquibase.Scope;
import liquibase.database.Database;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.plugin.AbstractPluginFactory;
import liquibase.plugin.Plugin;
import liquibase.servicelocator.ServiceLocator;
import liquibase.SupportsMethodValidationLevelsEnum;
import liquibase.util.LiquibaseUtil;
import lombok.Setter;

import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
Expand All @@ -21,8 +25,15 @@
public class ChangeFactory extends AbstractPluginFactory<Change>{

private final Map<String, ChangeMetaData> cachedMetadata = new ConcurrentHashMap<>();

/**
* Should the change be checked to see if it supports the current database?
*/
@Setter
private boolean performSupportsDatabaseValidation = true;

private static final String SUPPORTS_METHOD_REQUIRED_MESSAGE = "%s class does not implement the 'supports(Database)' method and may incorrectly override other databases changes causing unexpected behavior. Please report this to the Liquibase developers or if you are developing this change please fix it ;)";

private ChangeFactory() {

}
Expand Down Expand Up @@ -104,6 +115,9 @@ public Change create(String name) {
if (plugins.isEmpty()) {
return null;
} else if (plugins.size() > 1) {
// we only verify supports method if we have more than 1 implementation for this change,
// otherwise there is no use in doing that as we will use the only implementation available
this.verifySupportsMethodImplementation(plugins);
Database database = Scope.getCurrentScope().getDatabase();
if (database != null && performSupportsDatabaseValidation) {
plugins.removeIf(a -> !a.supports(database));
Expand All @@ -120,6 +134,43 @@ public Change create(String name) {
}
}

/**
* Verify if the supports method is implemented in the change if it's not part of the default liquibase changes
*/
private void verifySupportsMethodImplementation(Set<Change> plugins) {
if (GlobalConfiguration.SUPPORTS_METHOD_VALIDATION_LEVELS.getCurrentValue().equals(SupportsMethodValidationLevelsEnum.OFF)) {
return;
}
//we only verify supports method if this is not part of the default liquibase changes
for (Change plugin : plugins) {
String packageName = plugin.getClass().getPackage().getName();
if (packageName.startsWith("liquibase.change") || packageName.startsWith("com.datical.liquibase.ext")) {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should I keep pro package here or should we verify it too?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that is okay to keep it out.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In fact I think you should remove it since it's an extension.

continue;
}

try {
// if the supports method is not implemented in the plugin show the wraning according to the defined level
filipelautert marked this conversation as resolved.
Show resolved Hide resolved
if (plugin.getClass().getMethod("supports", Database.class).getDeclaringClass().getPackage().getName().startsWith("liquibase.change")) {
if (LiquibaseUtil.getBuildVersion().equals(LiquibaseUtil.DEV_VERSION)) {
throw new UnexpectedLiquibaseException(String.format(SUPPORTS_METHOD_REQUIRED_MESSAGE, plugin.getClass().getName()));
}
switch (GlobalConfiguration.SUPPORTS_METHOD_VALIDATION_LEVELS.getCurrentValue()) {
case WARN:
Scope.getCurrentScope().getLog(getClass()).warning(String.format(SUPPORTS_METHOD_REQUIRED_MESSAGE, plugin.getClass().getName()));
plugins.remove(plugin);
break;
case FAIL:
throw new UnexpectedLiquibaseException(String.format(SUPPORTS_METHOD_REQUIRED_MESSAGE, plugin.getClass().getName()));
default:
break;
}
}
} catch (NoSuchMethodException e) {
throw new UnexpectedLiquibaseException(String.format(SUPPORTS_METHOD_REQUIRED_MESSAGE, plugin.getClass().getName()));
StevenMassaro marked this conversation as resolved.
Show resolved Hide resolved
}
}
}

public Map<String, Object> getParameters(Change change) {
Map<String, Object> returnMap = new HashMap<>();
ChangeMetaData changeMetaData = getChangeMetaData(change);
Expand All @@ -141,14 +192,4 @@ public static ChangeFactory getInstance() {
return Scope.getCurrentScope().getSingleton(ChangeFactory.class);
}

/**
* Should the change be checked to see if it supports
* the current database?
* Default is true
*
* @param performSupportsDatabaseValidation
*/
public void setPerformSupportsDatabaseValidation(boolean performSupportsDatabaseValidation) {
this.performSupportsDatabaseValidation = performSupportsDatabaseValidation;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ private void warnForMismatchedXsdVersion(String systemId) {
boolean found = versionMatcher.find();
if (found) {
String buildVersion = LiquibaseUtil.getBuildVersion();
if (!buildVersion.equals("DEV")) {
if (!buildVersion.equals(LiquibaseUtil.DEV_VERSION)) {
String xsdVersion = versionMatcher.group("version");
if (!buildVersion.startsWith(xsdVersion)) {
hasWarnedAboutMismatchedXsdVersion = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@

public class LiquibaseUtil {

public static final String DEV_VERSION = "DEV";

private static Properties liquibaseBuildProperties;

/**
* Return the configured build.version. Will always be "DEV" for non-release builds.
* Return the configured build.version. Will always be DEV_VERSION for non-release builds.
*/
public static String getBuildVersion() {
return getBuildInfo("build.version");
Expand All @@ -26,7 +28,7 @@ public static String getBuildVersion() {
*/
public static String getBuildVersionInfo() {
String version = getBuildInfo("build.version");
if (version.equals("DEV")) {
if (version.equals(DEV_VERSION)) {
final String buildCommit = getBuildInfo("build.commit");
if (buildCommit.equals("unknown")) {
version = "[local build]";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import liquibase.exception.ChangeLogParseException
import liquibase.exception.LiquibaseException
import liquibase.sdk.resource.MockResourceAccessor
import liquibase.test.JUnitResourceAccessor
import liquibase.util.LiquibaseUtil
import spock.lang.Specification
import spock.lang.Unroll

Expand Down Expand Up @@ -173,13 +174,13 @@ class XMLChangeLogSAXParserTest extends Specification {
XMLChangeLogSAXParser.computeSchemaVersion(buildVersion) == expected

where:
buildVersion | expected
"DEV" | "latest"
"4.11.0" | "4.11"
"4.11.1" | "4.11"
"4" | "latest" //weird versions go to latest
"" | "latest" //weird versions go to latest
null | "latest" //weird versions go to latest
buildVersion | expected
LiquibaseUtil.DEV_VERSION | "latest"
"4.11.0" | "4.11"
"4.11.1" | "4.11"
"4" | "latest" //weird versions go to latest
"" | "latest" //weird versions go to latest
null | "latest" //weird versions go to latest
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,13 @@ class LiquibaseUtilTest extends Specification {
LiquibaseUtil.getBuildVersion() == version

where:
version | localBuild | proData | expected
"1.2.3" | false | true | "1.2.3"
"1.2.3" | false | false | "1.2.3"
"DEV" | false | true | "[Core: liquibase/liquibase/test-branch/524/7904bb/2021-09-17 14:06+0000, Pro: other-branch/58/e5195b/2021-09-17T14:04:58Z]"
"DEV" | false | false | "[Core: liquibase/liquibase/test-branch/524/7904bb/2021-09-17 14:06+0000]"
"DEV" | true | true | "[local build]"
"DEV" | true | false | "[local build]"
version | localBuild | proData | expected
"1.2.3" | false | true | "1.2.3"
"1.2.3" | false | false | "1.2.3"
LiquibaseUtil.DEV_VERSION | false | true | "[Core: liquibase/liquibase/test-branch/524/7904bb/2021-09-17 14:06+0000, Pro: other-branch/58/e5195b/2021-09-17T14:04:58Z]"
LiquibaseUtil.DEV_VERSION | false | false | "[Core: liquibase/liquibase/test-branch/524/7904bb/2021-09-17 14:06+0000]"
LiquibaseUtil.DEV_VERSION | true | true | "[local build]"
LiquibaseUtil.DEV_VERSION | true | false | "[local build]"
}

def "getBuildVersionInfo actuallyRead"() {
Expand Down