
liquibase.changelog.DatabaseChangeLog Maven / Gradle / Ivy
package liquibase.changelog;
import liquibase.*;
import liquibase.change.visitor.ChangeVisitor;
import liquibase.change.visitor.ChangeVisitorFactory;
import liquibase.changelog.filter.ContextChangeSetFilter;
import liquibase.changelog.filter.DbmsChangeSetFilter;
import liquibase.changelog.filter.LabelChangeSetFilter;
import liquibase.changelog.visitor.ValidatingVisitor;
import liquibase.changelog.visitor.ValidatingVisitorGenerator;
import liquibase.changelog.visitor.ValidatingVisitorGeneratorFactory;
import liquibase.changeset.ChangeSetService;
import liquibase.changeset.ChangeSetServiceFactory;
import liquibase.database.Database;
import liquibase.database.DatabaseList;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.exception.*;
import liquibase.logging.Logger;
import liquibase.logging.mdc.MdcKey;
import liquibase.logging.mdc.MdcValue;
import liquibase.logging.mdc.customobjects.DuplicateChangesets;
import liquibase.logging.mdc.customobjects.MdcChangeset;
import liquibase.parser.ChangeLogParser;
import liquibase.parser.ChangeLogParserConfiguration;
import liquibase.parser.ChangeLogParserFactory;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.parser.core.json.JsonChangeLogParser;
import liquibase.parser.core.sql.SqlChangeLogParser;
import liquibase.parser.core.xml.XMLChangeLogSAXParser;
import liquibase.parser.core.yaml.YamlParser;
import liquibase.precondition.Conditional;
import liquibase.precondition.core.PreconditionContainer;
import liquibase.resource.Resource;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.LiquibaseService;
import liquibase.util.ExceptionUtil;
import liquibase.util.FileUtil;
import liquibase.util.StringUtil;
import lombok.Getter;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.*;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
/**
* Encapsulates the information stored in the change log XML file.
*/
public class DatabaseChangeLog implements Comparable, Conditional {
private static final ThreadLocal ROOT_CHANGE_LOG = new ThreadLocal<>();
private static final ThreadLocal PARENT_CHANGE_LOG = new ThreadLocal<>();
private static final Logger LOG = Scope.getCurrentScope().getLog(DatabaseChangeLog.class);
private static final Pattern SLASH_PATTERN = Pattern.compile("^/");
private static final Pattern DOUBLE_BACK_SLASH_PATTERN = Pattern.compile("\\\\");
private static final Pattern NO_LETTER_PATTERN = Pattern.compile("^[a-zA-Z]:");
private static final String CLASSPATH_PROTOCOL = "classpath:";
public static final String SEEN_CHANGELOGS_PATHS_SCOPE_KEY = "SEEN_CHANGELOG_PATHS";
public static final String FILE = "file";
public static final String CONTEXT_FILTER = "contextFilter";
public static final String CONTEXT = "context";
public static final String LABELS = "labels";
public static final String IGNORE = "ignore";
public static final String RELATIVE_TO_CHANGELOG_FILE = "relativeToChangelogFile";
public static final String LOGICAL_FILE_PATH = "logicalFilePath";
public static final String ERROR_IF_MISSING = "errorIfMissing";
public static final String MODIFY_CHANGE_SETS = "modifyChangeSets";
public static final String PATH = "path";
public static final String FILTER = "filter";
public static final String RESOURCE_FILTER = "resourceFilter";
public static final String RESOURCE_COMPARATOR = "resourceComparator";
public static final String MIN_DEPTH = "minDepth";
public static final String MAX_DEPTH = "maxDepth";
public static final String ENDS_WITH_FILTER = "endsWithFilter";
public static final String ERROR_IF_MISSING_OR_EMPTY = "errorIfMissingOrEmpty";
public static final String CHANGE_SET = "changeSet";
public static final String DBMS = "dbms";
public static final String INCLUDE_CHANGELOG = "include";
public static final String INCLUDE_ALL_CHANGELOGS = "includeAll";
public static final String PRE_CONDITIONS = "preConditions";
public static final String REMOVE_CHANGE_SET_PROPERTY = "removeChangeSetProperty";
public static final String PROPERTY = "property";
public static final String NAME = "name";
public static final String VALUE = "value";
public static final String GLOBAL = "global";
private final PreconditionContainer preconditionContainer = new GlobalPreconditionContainer();
@Getter
private String physicalFilePath;
private String logicalFilePath;
@Getter
private ObjectQuotingStrategy objectQuotingStrategy;
@Getter
private final List changeVisitors = new ArrayList<>();
@Getter
private final List changeSets = new ArrayList<>();
@Getter
private final List skippedChangeSets = new ArrayList<>();
@Getter
private final List skippedBecauseOfLicenseChangeSets = new ArrayList<>();
@Getter
private ChangeLogParameters changeLogParameters;
@Getter
private RuntimeEnvironment runtimeEnvironment;
private DatabaseChangeLog rootChangeLog = ROOT_CHANGE_LOG.get();
@Getter
private DatabaseChangeLog parentChangeLog = PARENT_CHANGE_LOG.get();
@Getter
private ContextExpression contextFilter;
@Getter
private ContextExpression includeContextFilter;
@Getter
private Labels includeLabels;
@Getter
private boolean includeIgnore;
@Getter
private ParsedNode currentlyLoadedChangeSetNode;
public DatabaseChangeLog() {
}
public DatabaseChangeLog(String physicalFilePath) {
this.physicalFilePath = physicalFilePath;
}
public void setRootChangeLog(DatabaseChangeLog rootChangeLog) {
this.rootChangeLog = rootChangeLog;
}
public DatabaseChangeLog getRootChangeLog() {
return (rootChangeLog != null) ? rootChangeLog : this;
}
public void setParentChangeLog(DatabaseChangeLog parentChangeLog) {
this.parentChangeLog = parentChangeLog;
}
public void setRuntimeEnvironment(RuntimeEnvironment runtimeEnvironment) {
this.runtimeEnvironment = runtimeEnvironment;
}
@Override
public PreconditionContainer getPreconditions() {
return preconditionContainer;
}
@Override
public void setPreconditions(PreconditionContainer precondition) {
this.preconditionContainer.addNestedPrecondition(precondition);
}
public void setChangeLogParameters(ChangeLogParameters changeLogParameters) {
this.changeLogParameters = changeLogParameters;
}
public void setPhysicalFilePath(String physicalFilePath) {
this.physicalFilePath = physicalFilePath;
}
public String getLogicalFilePath() {
String returnPath = logicalFilePath;
if (logicalFilePath == null) {
returnPath = physicalFilePath;
}
if (returnPath == null) {
return null;
}
String path = DOUBLE_BACK_SLASH_PATTERN.matcher(returnPath).replaceAll("/");
return SLASH_PATTERN.matcher(path).replaceFirst("");
}
public void setLogicalFilePath(String logicalFilePath) {
this.logicalFilePath = logicalFilePath;
}
public String getFilePath() {
if (logicalFilePath == null) {
return physicalFilePath;
} else {
return getLogicalFilePath();
}
}
public void setObjectQuotingStrategy(ObjectQuotingStrategy objectQuotingStrategy) {
this.objectQuotingStrategy = objectQuotingStrategy;
}
/**
* @deprecated use {@link #getContextFilter()}
*/
@Deprecated
public ContextExpression getContexts() {
return getContextFilter();
}
/**
* @deprecated use {@link #setContextFilter(ContextExpression)}
*/
@Deprecated
public void setContexts(ContextExpression contexts) {
setContextFilter(contexts);
}
public void setContextFilter(ContextExpression contextFilter) {
this.contextFilter = contextFilter;
}
/**
* @deprecated Correct version is {@link #setIncludeLabels(Labels)}. Kept for backwards compatibility.
*/
@Deprecated
public void setIncludeLabels(LabelExpression labels) {
this.includeLabels = new Labels(labels.toString());
}
public void setIncludeLabels(Labels labels) {
this.includeLabels = labels;
}
public void setIncludeIgnore(boolean ignore) {
this.includeIgnore = ignore;
}
/**
* @deprecated use {@link #setIncludeContextFilter(ContextExpression)}
*/
@Deprecated
public void setIncludeContexts(ContextExpression includeContexts) {
setIncludeContextFilter(includeContexts);
}
public void setIncludeContextFilter(ContextExpression includeContextFilter) {
this.includeContextFilter = includeContextFilter;
}
@Override
public String toString() {
return getFilePath();
}
@Override
public int compareTo(DatabaseChangeLog o) {
return getFilePath().compareTo(o.getFilePath());
}
public ChangeSet getChangeSet(String path, String author, String id) {
final List possibleChangeSets = getChangeSets(path, author, id);
if (possibleChangeSets.isEmpty()) {
return null;
}
return possibleChangeSets.get(0);
}
public List getChangeSets(String path, String author, String id) {
final ArrayList changeSetsToReturn = new ArrayList<>();
final String normalizedPath = normalizePath(path);
if (normalizedPath != null) {
for (ChangeSet changeSet : this.changeSets) {
if (changeSet.getAuthor().equalsIgnoreCase(author) && changeSet.getId().equalsIgnoreCase(id) && isDbmsMatch(changeSet.getDbmsSet())) {
final String changesetNormalizedPath = normalizePath(changeSet.getFilePath());
if (changesetNormalizedPath != null && changesetNormalizedPath.equalsIgnoreCase(normalizedPath)) {
changeSetsToReturn.add(changeSet);
}
}
}
}
return changeSetsToReturn;
}
public void addChangeSet(ChangeSet changeSet) {
if (changeSet.getRunOrder() == null) {
ListIterator it = this.changeSets.listIterator(this.changeSets.size());
boolean added = false;
while (it.hasPrevious() && !added) {
if (!"last".equals(it.previous().getRunOrder())) {
it.next();
it.add(changeSet);
added = true;
}
}
if (!added) {
it.add(changeSet);
}
} else if ("first".equals(changeSet.getRunOrder())) {
ListIterator it = this.changeSets.listIterator();
boolean added = false;
while (it.hasNext() && !added) {
if (!"first".equals(it.next().getRunOrder())) {
it.previous();
it.add(changeSet);
added = true;
}
}
if (!added) {
this.changeSets.add(changeSet);
}
} else if ("last".equals(changeSet.getRunOrder())) {
this.changeSets.add(changeSet);
} else {
throw new UnexpectedLiquibaseException("Unknown runOrder: " + changeSet.getRunOrder());
}
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if ((o == null) || (getClass() != o.getClass())) {
return false;
}
DatabaseChangeLog that = (DatabaseChangeLog) o;
return getFilePath().equals(that.getFilePath());
}
@Override
public int hashCode() {
return getFilePath().hashCode();
}
public void validate(Database database, String... contexts) throws LiquibaseException {
this.validate(database, new Contexts(contexts), new LabelExpression());
}
public void validate(Database database, Contexts contexts, LabelExpression labelExpression)
throws LiquibaseException {
database.setObjectQuotingStrategy(objectQuotingStrategy);
ChangeLogIterator logIterator = new ChangeLogIterator(
this,
new DbmsChangeSetFilter(database),
new ContextChangeSetFilter(contexts),
new LabelChangeSetFilter(labelExpression)
);
ValidatingVisitorGeneratorFactory validatingVisitorGeneratorFactory = Scope.getCurrentScope().getSingleton(ValidatingVisitorGeneratorFactory.class);
ValidatingVisitorGenerator generator = validatingVisitorGeneratorFactory.getValidatingVisitorGenerator();
ValidatingVisitor validatingVisitor = generator.generateValidatingVisitor(database.getRanChangeSetList());
validatingVisitor.validate(database, this);
logIterator.run(validatingVisitor, new RuntimeEnvironment(database, contexts, labelExpression));
final Logger log = Scope.getCurrentScope().getLog(getClass());
for (String message : validatingVisitor.getWarnings().getMessages()) {
log.warning(message);
}
if (!validatingVisitor.validationPassed()) {
Scope.getCurrentScope().addMdcValue(MdcKey.DEPLOYMENT_OUTCOME, MdcValue.COMMAND_FAILED);
List duplicateChangesetsMdc = validatingVisitor.getDuplicateChangeSets().stream().map(MdcChangeset::fromChangeset).collect(Collectors.toList());
Scope.getCurrentScope().addMdcValue(MdcKey.DUPLICATE_CHANGESETS, new DuplicateChangesets(duplicateChangesetsMdc));
Scope.getCurrentScope().getLog(getClass()).info("Change failed validation!");
throw new ValidationFailedException(validatingVisitor);
}
}
public ChangeSet getChangeSet(RanChangeSet ranChangeSet) {
final ChangeSet changeSet = getChangeSet(ranChangeSet.getChangeLog(), ranChangeSet.getAuthor(), ranChangeSet.getId());
if (changeSet != null) {
changeSet.setStoredFilePath(ranChangeSet.getStoredChangeLog());
}
return changeSet;
}
public List getChangeSets(RanChangeSet ranChangeSet) {
List changesets = getChangeSets(ranChangeSet.getChangeLog(), ranChangeSet.getAuthor(), ranChangeSet.getId());
changesets.forEach(c -> c.setStoredFilePath(ranChangeSet.getStoredChangeLog()));
return changesets;
}
public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException, SetupException {
ExceptionUtil.doSilently(() -> {
String physicalFilePathLowerCase = this.physicalFilePath.toLowerCase();
if (JsonChangeLogParser.SUPPORTED_EXTENSIONS.stream().anyMatch(ext -> physicalFilePathLowerCase.endsWith(ext))) {
Scope.getCurrentScope().getAnalyticsEvent().incrementJsonChangelogCount();
} else if (XMLChangeLogSAXParser.SUPPORTED_EXTENSIONS.stream().anyMatch(ext -> physicalFilePathLowerCase.endsWith(ext))) {
Scope.getCurrentScope().getAnalyticsEvent().incrementXmlChangelogCount();
} else if (YamlParser.SUPPORTED_EXTENSIONS.stream().anyMatch(ext -> physicalFilePathLowerCase.endsWith(ext))) {
Scope.getCurrentScope().getAnalyticsEvent().incrementYamlChangelogCount();
}
});
setLogicalFilePath(parsedNode.getChildValue(null, LOGICAL_FILE_PATH, String.class));
String context = parsedNode.getChildValue(null, CONTEXT_FILTER, String.class);
if (context == null) {
context = parsedNode.getChildValue(null, CONTEXT, String.class);
}
setContextFilter(new ContextExpression(context));
String nodeObjectQuotingStrategy = parsedNode.getChildValue(null, "objectQuotingStrategy", String.class);
if (nodeObjectQuotingStrategy != null) {
setObjectQuotingStrategy(ObjectQuotingStrategy.valueOf(nodeObjectQuotingStrategy));
}
for (ParsedNode childNode : parsedNode.getChildren()) {
if (childNode.getName().equals((new ChangeSet(null)).getSerializedObjectName())) {
this.currentlyLoadedChangeSetNode = childNode;
}
handleChildNode(childNode, resourceAccessor, new HashMap<>());
}
this.currentlyLoadedChangeSetNode = null;
}
protected void expandExpressions(ParsedNode parsedNode) throws UnknownChangeLogParameterException {
if (changeLogParameters == null) {
return;
}
try {
Object value = parsedNode.getValue();
if ((value instanceof String)) {
parsedNode.setValue(changeLogParameters.expandExpressions(parsedNode.getValue(String.class), this));
}
List children = parsedNode.getChildren();
if (children != null) {
for (ParsedNode child : children) {
expandExpressions(child);
}
}
} catch (ParsedNodeException e) {
throw new UnexpectedLiquibaseException(e);
}
}
protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccessor)
throws ParsedNodeException, SetupException {
handleChildNode(node, resourceAccessor, new HashMap<>());
}
protected void handleChildNode(ParsedNode node, ResourceAccessor resourceAccessor, Map nodeScratch)
throws ParsedNodeException, SetupException {
handleChildNodeHelper(node, resourceAccessor, nodeScratch);
}
public void handleChildNodeHelper(ParsedNode node, ResourceAccessor resourceAccessor, Map nodeScratch)
throws ParsedNodeException, SetupException {
expandExpressions(node);
String nodeName = node.getName();
switch (nodeName) {
case CHANGE_SET:
handleDbmsAttribute(node, resourceAccessor);
break;
case MODIFY_CHANGE_SETS:
handleModifyChangeSets(node, resourceAccessor);
break;
case INCLUDE_CHANGELOG: {
handleInclude(node, resourceAccessor, nodeScratch);
break;
}
case INCLUDE_ALL_CHANGELOGS: {
handleIncludeAll(node, resourceAccessor, nodeScratch);
break;
}
case PRE_CONDITIONS: {
handlePrecondition(node, resourceAccessor);
break;
}
case REMOVE_CHANGE_SET_PROPERTY: {
handleRemoveChangeSet(node, resourceAccessor);
break;
}
case PROPERTY: {
handleProperty(node, resourceAccessor);
break;
}
default:
// we want to exclude child nodes that are not changesets or the other things
// and avoid failing when encountering "child" nodes of the databaseChangeLog which are just
// XML node attributes (like schemaLocation). If you don't understand, remove the if-condition and run the tests
// and look at the error output or review the "node" object here with a debugger.
if (node.getChildren() != null && !node.getChildren().isEmpty()) {
throw new ParsedNodeException("Unexpected node found under databaseChangeLog: " + nodeName);
}
}
}
private void handlePrecondition(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
PreconditionContainer parsedContainer = new PreconditionContainer();
parsedContainer.load(node, resourceAccessor);
this.preconditionContainer.addNestedPrecondition(parsedContainer);
}
private void handleProperty(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
try {
String propertyContextFilter = node.getChildValue(null, CONTEXT_FILTER, String.class);
if (StringUtil.isEmpty(propertyContextFilter)) {
propertyContextFilter = node.getChildValue(null, CONTEXT, String.class);
}
String dbms = node.getChildValue(null, DBMS, String.class);
String labels = node.getChildValue(null, LABELS, String.class);
Boolean global = node.getChildValue(null, GLOBAL, Boolean.class);
if (global == null) {
// okay behave like liquibase < 3.4 and set global == true
global = true;
}
String file = node.getChildValue(null, FILE, String.class);
Boolean relativeToChangelogFile = node.getChildValue(null, RELATIVE_TO_CHANGELOG_FILE, Boolean.FALSE);
Boolean errorIfMissing = node.getChildValue(null, ERROR_IF_MISSING, Boolean.TRUE);
Resource resource;
if (file == null) {
// direct referenced property, no file
String name = node.getChildValue(null, NAME, String.class);
String value = node.getChildValue(null, VALUE, String.class);
this.changeLogParameters.set(name, value, propertyContextFilter, labels, dbms, global, this);
} else {
// get relative path if specified
if (relativeToChangelogFile) {
resource = resourceAccessor.get(this.getPhysicalFilePath()).resolveSibling(file);
} else {
resource = resourceAccessor.get(file);
}
// read properties from the file
Properties props = new Properties();
if (!resource.exists()) {
if (errorIfMissing) {
throw new UnexpectedLiquibaseException(FileUtil.getFileNotFoundMessage(file));
} else {
Scope.getCurrentScope().getLog(getClass()).warning(FileUtil.getFileNotFoundMessage(file));
}
} else {
try (InputStream propertiesStream = resource.openInputStream()) {
props.load(propertiesStream);
for (Map.Entry
© 2015 - 2025 Weber Informatics LLC | Privacy Policy