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

refactor ValidatingVisitor to use factory pattern, error if strict is set and defaults file does not exist (DAT-15920) #5814

Merged
merged 16 commits into from
Apr 29, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,7 @@
import liquibase.configuration.ConfiguredValue;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.configuration.core.DefaultsFileValueProvider;
import liquibase.exception.CommandLineParsingException;
import liquibase.exception.CommandValidationException;
import liquibase.exception.ExitCodeException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.*;
import liquibase.integration.IntegrationDetails;
import liquibase.license.LicenseInfo;
import liquibase.license.LicenseService;
Expand Down Expand Up @@ -47,17 +44,12 @@
import java.security.PrivilegedAction;
import java.time.Duration;
import java.util.*;
import java.util.jar.JarFile;
import java.util.jar.Manifest;
import java.util.logging.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;

import static java.util.ResourceBundle.getBundle;
import static liquibase.configuration.LiquibaseConfiguration.REGISTERED_VALUE_PROVIDERS_KEY;
import static liquibase.integration.commandline.LiquibaseLauncherSettings.LiquibaseLauncherSetting.LIQUIBASE_HOME;
import static liquibase.integration.commandline.LiquibaseLauncherSettings.getSetting;
import static liquibase.integration.commandline.VersionUtils.*;
import static liquibase.util.SystemUtil.isWindows;

Expand Down Expand Up @@ -626,31 +618,36 @@ private List<ConfigurationValueProvider> registerValueProviders(String[] args) t
}

final PathHandlerFactory pathHandlerFactory = Scope.getCurrentScope().getSingleton(PathHandlerFactory.class);
Resource resource = pathHandlerFactory.getResource(defaultsFileConfig.getValue());
String defaultsFileConfigValue = defaultsFileConfig.getValue();
Resource resource = pathHandlerFactory.getResource(defaultsFileConfigValue);
if (resource.exists()) {
try (InputStream defaultsStream = resource.openInputStream()) {
if (defaultsStream != null) {
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(defaultsStream, "File exists at path " + defaultsFileConfig.getValue());
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(defaultsStream, "File exists at path " + defaultsFileConfigValue);
liquibaseConfiguration.registerProvider(fileProvider);
returnList.add(fileProvider);
}
}
} else {
InputStream inputStreamOnClasspath = Thread.currentThread().getContextClassLoader().getResourceAsStream(defaultsFileConfig.getValue());
InputStream inputStreamOnClasspath = Thread.currentThread().getContextClassLoader().getResourceAsStream(defaultsFileConfigValue);
if (inputStreamOnClasspath == null) {
Scope.getCurrentScope().getLog(getClass()).fine("Cannot find defaultsFile " + defaultsFileConfig.getValue());
Scope.getCurrentScope().getLog(getClass()).fine("Cannot find defaultsFile " + defaultsFileConfigValue);
if (!defaultsFileConfig.wasDefaultValueUsed()) {
//can't use UI since it's not configured correctly yet
System.err.println("Could not find defaults file " + defaultsFileConfig.getValue());
if (GlobalConfiguration.STRICT.getCurrentValue()) {
throw new UnexpectedLiquibaseException("ERROR: The file '"+defaultsFileConfigValue+"' was not found. The global argument 'strict' is enabled, which validates the existence of files specified in liquibase files, such as changelogs, flowfiles, checks packages files, and more. To prevent this message, check your configurations, or disable the 'strict' setting.");
} else {
System.err.println("Could not find defaults file " + defaultsFileConfigValue);
}
}
} else {
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(inputStreamOnClasspath, "File in classpath " + defaultsFileConfig.getValue());
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(inputStreamOnClasspath, "File in classpath " + defaultsFileConfigValue);
liquibaseConfiguration.registerProvider(fileProvider);
returnList.add(fileProvider);
}
}

final File defaultsFile = new File(defaultsFileConfig.getValue());
final File defaultsFile = new File(defaultsFileConfigValue);
File localDefaultsFile = new File(defaultsFile.getAbsolutePath().replaceFirst(".properties$", ".local.properties"));
if (localDefaultsFile.exists()) {
final DefaultsFileValueProvider fileProvider = new DefaultsFileValueProvider(localDefaultsFile) {
Expand Down
1 change: 1 addition & 0 deletions liquibase-standard/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@
<param>liquibase.changeset.ChangeSetService</param>
<param>liquibase.parser.LiquibaseSqlParser</param>
<param>liquibase.database.LiquibaseTableNames</param>
<param>liquibase.changelog.visitor.ValidatingVisitor</param>
</services>
</configuration>
<executions>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import liquibase.changelog.filter.ChangeSetFilterResult;
import liquibase.changelog.visitor.ChangeSetVisitor;
import liquibase.changelog.visitor.SkippedChangeSetVisitor;
import liquibase.changelog.visitor.ValidatingVisitor;
import liquibase.changelog.visitor.StandardValidatingVisitor;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.exception.ValidationErrors;
Expand Down Expand Up @@ -125,7 +125,7 @@ public void run() throws Exception {
// Go validate any changesets with an Executor if
// we are using a ValidatingVisitor
//
if (visitor instanceof ValidatingVisitor) {
if (visitor instanceof StandardValidatingVisitor) {
validateChangeSetExecutor(changeSet, env);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import liquibase.changelog.filter.DbmsChangeSetFilter;
import liquibase.changelog.filter.LabelChangeSetFilter;
import liquibase.changelog.visitor.ValidatingVisitor;
import liquibase.changelog.visitor.ValidatingVisitorFactory;
import liquibase.changeset.ChangeSetService;
import liquibase.changeset.ChangeSetServiceFactory;
import liquibase.database.Database;
Expand Down Expand Up @@ -341,7 +342,9 @@ public void validate(Database database, Contexts contexts, LabelExpression label
new LabelChangeSetFilter(labelExpression)
);

ValidatingVisitor validatingVisitor = new ValidatingVisitor(database.getRanChangeSetList());
ValidatingVisitorFactory validatingVisitorFactory = Scope.getCurrentScope().getSingleton(ValidatingVisitorFactory.class);
ValidatingVisitor validatingVisitor = validatingVisitorFactory.getValidatingVisitor();
validatingVisitor.setRanChangeSetList(database.getRanChangeSetList());
validatingVisitor.validate(database, this);
logIterator.run(validatingVisitor, new RuntimeEnvironment(database, contexts, labelExpression));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
package liquibase.changelog.visitor;

import liquibase.ChecksumVersion;
import liquibase.GlobalConfiguration;
import liquibase.Scope;
import liquibase.change.Change;
import liquibase.changelog.ChangeSet;
import liquibase.changelog.DatabaseChangeLog;
import liquibase.changelog.RanChangeSet;
import liquibase.changelog.filter.ChangeSetFilterResult;
import liquibase.database.Database;
import liquibase.database.DatabaseList;
import liquibase.exception.*;
import liquibase.plugin.Plugin;
import liquibase.precondition.ErrorPrecondition;
import liquibase.precondition.FailedPrecondition;
import liquibase.precondition.core.PreconditionContainer;
import liquibase.util.StringUtil;
import liquibase.util.ValidatingVisitorUtil;
import lombok.Getter;

import java.util.*;

@Getter
public class StandardValidatingVisitor implements ChangeSetVisitor, ValidatingVisitor {

private final List<String> invalidMD5Sums = new ArrayList<>();
Fixed Show fixed Hide fixed
private String failedPreconditionsMessage = null;
Fixed Show fixed Hide fixed
private String errorPreconditionsMessage = null;
Fixed Show fixed Hide fixed
private final List<FailedPrecondition> failedPreconditions = new ArrayList<>();
Fixed Show fixed Hide fixed
private final List<ErrorPrecondition> errorPreconditions = new ArrayList<>();
Fixed Show fixed Hide fixed
private final Set<ChangeSet> duplicateChangeSets = new LinkedHashSet<>();
Fixed Show fixed Hide fixed
private final List<SetupException> setupExceptions = new ArrayList<>();
Fixed Show fixed Hide fixed
private final List<Throwable> changeValidationExceptions = new ArrayList<>();
Fixed Show fixed Hide fixed
private final ValidationErrors validationErrors = new ValidationErrors();
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
private final Warnings warnings = new Warnings();
Fixed Show fixed Hide fixed

private final Set<String> seenChangeSets = new HashSet<>();

private Map<String, RanChangeSet> ranIndex;
private Database database;

//
// Added for test
//
public StandardValidatingVisitor() {
}

public StandardValidatingVisitor(List<RanChangeSet> ranChangeSets) {
setRanChangeSetList(ranChangeSets);
}

@Override
public void setRanChangeSetList(List<RanChangeSet> ranChangeSetList) {
ranIndex = new HashMap<>();
for(RanChangeSet changeSet: ranChangeSetList) {
ranIndex.put(changeSet.toString(), changeSet);
}
}

@Override
public void validate(Database database, DatabaseChangeLog changeLog) {
this.database = database;
PreconditionContainer preconditions = changeLog.getPreconditions();
try {
if (preconditions == null) {
return;
}
final ValidationErrors foundErrors = preconditions.validate(database);
if (foundErrors.hasErrors()) {
this.validationErrors.addAll(foundErrors);
} else {
preconditions.check(database, changeLog, null, null);
}
} catch (PreconditionFailedException e) {
Scope.getCurrentScope().getLog(getClass()).warning("Precondition Failed: "+e.getMessage(), e);
failedPreconditionsMessage = e.getMessage();
failedPreconditions.addAll(e.getFailedPreconditions());
} catch (PreconditionErrorException e) {
Scope.getCurrentScope().getLog(getClass()).severe("Precondition Error: "+e.getMessage(), e);
errorPreconditionsMessage = e.getMessage();
errorPreconditions.addAll(e.getErrorPreconditions());
} finally {
try {
if (database.getConnection() != null) {
database.rollback();
}
} catch (DatabaseException e) {
Scope.getCurrentScope().getLog(getClass()).warning("Error rolling back after precondition check", e);
}
}
}

@Override
public Direction getDirection() {
return ChangeSetVisitor.Direction.FORWARD;
}

private RanChangeSet findChangeSet(ChangeSet changeSet) {
String key = changeSet.toNormalizedString();
return ranIndex.get(key);
}

@Override
public void visit(ChangeSet changeSet, DatabaseChangeLog databaseChangeLog, Database database, Set<ChangeSetFilterResult> filterResults) throws LiquibaseException {
if (changeSet.isIgnore()) {
Scope.getCurrentScope().getLog(StandardValidatingVisitor.class).info("Not validating ignored change set '" + changeSet.toString() + "'");
return;
}
RanChangeSet ranChangeSet = findChangeSet(changeSet);
boolean ran = ranChangeSet != null;
Set<String> dbmsSet = changeSet.getDbmsSet();
if(dbmsSet != null) {
DatabaseList.validateDefinitions(changeSet.getDbmsSet(), validationErrors);
}
changeSet.setStoredCheckSum(ran?ranChangeSet.getLastCheckSum():null);
changeSet.setStoredFilePath(ran?ranChangeSet.getStoredChangeLog():null);
boolean shouldValidate = isShouldValidate(changeSet, ran);

for (Change change : changeSet.getChanges()) {
validateChange(changeSet, database, change, shouldValidate);
}

additionalValidations(changeSet, database, shouldValidate, ran);

if(ranChangeSet != null) {
if (!changeSet.isCheckSumValid(ranChangeSet.getLastCheckSum()) &&
!ValidatingVisitorUtil.isChecksumIssue(changeSet, ranChangeSet, databaseChangeLog, database) &&
!changeSet.shouldRunOnChange() &&
!changeSet.shouldAlwaysRun()) {
invalidMD5Sums.add(changeSet.toString(false)+" was: "+ranChangeSet.getLastCheckSum().toString()
+" but is now: "+changeSet.generateCheckSum(ChecksumVersion.enumFromChecksumVersion(ranChangeSet.getLastCheckSum().getVersion())).toString());
}
}

// Did we already see this ChangeSet?
String changeSetString = changeSet.toString(false);
if (seenChangeSets.contains(changeSetString)) {
duplicateChangeSets.add(changeSet);
} else {
seenChangeSets.add(changeSetString);
}
}

protected void additionalValidations(ChangeSet changeSet, Database database, boolean shouldValidate, boolean ran) {
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed
Fixed Show fixed Hide fixed

}

private boolean isShouldValidate(ChangeSet changeSet, boolean ran) {
boolean shouldValidate = !ran || changeSet.shouldRunOnChange() || changeSet.shouldAlwaysRun();

if (!areChangeSetAttributesValid(changeSet)) {
changeSet.setValidationFailed(true);
shouldValidate = false;
}
return shouldValidate;
}

protected void validateChange(ChangeSet changeSet, Database database, Change change, boolean shouldValidate) {
try {
change.finishInitialization();
} catch (SetupException se) {
setupExceptions.add(se);
}


if(shouldValidate){
warnings.addAll(change.warn(database));

try {
ValidationErrors foundErrors = change.validate(database);
if ((foundErrors != null)) {
if (foundErrors.hasErrors() && (changeSet.getOnValidationFail().equals
(ChangeSet.ValidationFailOption.MARK_RAN))) {
Scope.getCurrentScope().getLog(getClass()).info(
"Skipping changeset " + changeSet + " due to validation error(s): " +
StringUtil.join(foundErrors.getErrorMessages(), ", "));
changeSet.setValidationFailed(true);
} else {
if (!foundErrors.getWarningMessages().isEmpty())
Scope.getCurrentScope().getLog(getClass()).warning(
"Changeset " + changeSet + ": " +
StringUtil.join(foundErrors.getWarningMessages(), ", "));
validationErrors.addAll(foundErrors, changeSet);
}
}
} catch (Exception e) {
changeValidationExceptions.add(e);
}
}
}

private boolean areChangeSetAttributesValid(ChangeSet changeSet) {
boolean authorEmpty = StringUtil.isEmpty(changeSet.getAuthor());
boolean idEmpty = StringUtil.isEmpty(changeSet.getId());
boolean strictCurrentValue = GlobalConfiguration.STRICT.getCurrentValue();

boolean valid = false;
if (authorEmpty && idEmpty) {
validationErrors.addError("ChangeSet Id and Author are empty", changeSet);
} else if (authorEmpty && strictCurrentValue) {
validationErrors.addError("ChangeSet Author is empty", changeSet);
} else if (idEmpty) {
validationErrors.addError("ChangeSet Id is empty", changeSet);
} else {
valid = true;
}
return valid;
}

@Override
public boolean validationPassed() {
return invalidMD5Sums.isEmpty() && failedPreconditions.isEmpty() && errorPreconditions.isEmpty() &&
duplicateChangeSets.isEmpty() && changeValidationExceptions.isEmpty() && setupExceptions.isEmpty() &&
!validationErrors.hasErrors();
}

@Override
public int getPriority() {
return Plugin.PRIORITY_DEFAULT;
}
}