liquibase.changelog.DatabaseChangeLog Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of liquibase-core Show documentation
Show all versions of liquibase-core Show documentation
Liquibase is a tool for managing and executing database changes.
The newest version!
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.xml.XMLChangeLogSAXParser;
import liquibase.parser.core.yaml.YamlParser;
import liquibase.precondition.Conditional;
import liquibase.precondition.Precondition;
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 = "include";
public static final String INCLUDE_ALL = "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();
private String physicalFilePath;
private String logicalFilePath;
private ObjectQuotingStrategy objectQuotingStrategy;
private List changeVisitors = new ArrayList<>();
private final List changeSets = new ArrayList<>();
private final List skippedChangeSets = new ArrayList<>();
private final List skippedBecauseOfLicenseChangeSets = new ArrayList<>();
private ChangeLogParameters changeLogParameters;
private RuntimeEnvironment runtimeEnvironment;
private DatabaseChangeLog rootChangeLog = ROOT_CHANGE_LOG.get();
private DatabaseChangeLog parentChangeLog = PARENT_CHANGE_LOG.get();
private ContextExpression contextFilter;
private ContextExpression includeContextFilter;
private Labels includeLabels;
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 DatabaseChangeLog getParentChangeLog() {
return parentChangeLog;
}
public RuntimeEnvironment getRuntimeEnvironment() {
return runtimeEnvironment;
}
public void setRuntimeEnvironment(RuntimeEnvironment runtimeEnvironment) {
this.runtimeEnvironment = runtimeEnvironment;
}
@Override
public PreconditionContainer getPreconditions() {
return preconditionContainer;
}
@Override
public void setPreconditions(PreconditionContainer precond) {
this.preconditionContainer.addNestedPrecondition(precond);
}
public ChangeLogParameters getChangeLogParameters() {
return changeLogParameters;
}
public void setChangeLogParameters(ChangeLogParameters changeLogParameters) {
this.changeLogParameters = changeLogParameters;
}
public String getPhysicalFilePath() {
return physicalFilePath;
}
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 ObjectQuotingStrategy getObjectQuotingStrategy() {
return objectQuotingStrategy;
}
public void setObjectQuotingStrategy(ObjectQuotingStrategy objectQuotingStrategy) {
this.objectQuotingStrategy = objectQuotingStrategy;
}
/**
* @deprecated use {@link #getContextFilter()}
*/
public ContextExpression getContexts() {
return getContextFilter();
}
/**
* @deprecated use {@link #setContextFilter(ContextExpression)}
*/
public void setContexts(ContextExpression contexts) {
setContextFilter(contexts);
}
public ContextExpression getContextFilter() {
return contextFilter;
}
public void setContextFilter(ContextExpression contextFilter) {
this.contextFilter = contextFilter;
}
public ContextExpression getIncludeContextFilter() {
return includeContextFilter;
}
/**
* @deprecated Correct version is {@link #setIncludeLabels(Labels)}. Kept for backwards compatibility.
*/
public void setIncludeLabels(LabelExpression labels) {
this.includeLabels = new Labels(labels.toString());
}
public void setIncludeLabels(Labels labels) {
this.includeLabels = labels;
}
public Labels getIncludeLabels() {
return includeLabels;
}
public void setIncludeIgnore(boolean ignore) {
this.includeIgnore = ignore;
}
public boolean isIncludeIgnore() {
return this.includeIgnore;
}
/**
* @deprecated use {@link #setIncludeContextFilter(ContextExpression)}
*/
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 List getChangeVisitors(){
return changeVisitors;
}
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 List getChangeSets() {
return changeSets;
}
public List getSkippedBecauseOfLicenseChangeSets() {
return skippedBecauseOfLicenseChangeSets;
}
public List getSkippedChangeSets() {
return skippedChangeSets;
}
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:
if (isDbmsMatch(node.getChildValue(null, DBMS, String.class))) {
this.addChangeSet(createChangeSet(node, resourceAccessor));
} else {
handleSkippedChangeSet(node);
}
break;
case MODIFY_CHANGE_SETS:
ModifyChangeSets modifyChangeSets = createModifyChangeSets(node);
nodeScratch = new HashMap<>();
nodeScratch.put(MODIFY_CHANGE_SETS, modifyChangeSets);
for (ParsedNode modifyChildNode : node.getChildren()) {
handleChildNode(modifyChildNode, resourceAccessor, nodeScratch);
}
nodeScratch.remove(MODIFY_CHANGE_SETS);
break;
case INCLUDE: {
String path = node.getChildValue(null, FILE, String.class);
if (path == null) {
throw new UnexpectedLiquibaseException("No 'file' attribute on 'include'");
}
path = path.replace('\\', '/');
Scope.getCurrentScope().addMdcValue(MdcKey.CHANGELOG_FILE, path);
ContextExpression includeContextFilter = new ContextExpression(node.getChildValue(null, CONTEXT_FILTER, String.class));
if (includeContextFilter.isEmpty()) {
includeContextFilter = new ContextExpression(node.getChildValue(null, CONTEXT, String.class));
}
Labels labels = new Labels(node.getChildValue(null, LABELS, String.class));
Boolean ignore = node.getChildValue(null, IGNORE, Boolean.class);
try {
include(path,
node.getChildValue(null, RELATIVE_TO_CHANGELOG_FILE, false),
node.getChildValue(null, ERROR_IF_MISSING, true),
resourceAccessor,
includeContextFilter,
labels,
ignore,
node.getChildValue(null, LOGICAL_FILE_PATH, String.class),
OnUnknownFileFormat.FAIL,
(ModifyChangeSets) nodeScratch.get(MODIFY_CHANGE_SETS));
} catch (LiquibaseException e) {
throw new SetupException(e);
}
break;
}
case INCLUDE_ALL: {
String path = node.getChildValue(null, PATH, String.class);
String resourceFilterDef = node.getChildValue(null, FILTER, String.class);
if (resourceFilterDef == null) {
resourceFilterDef = node.getChildValue(null, RESOURCE_FILTER, String.class);
}
IncludeAllFilter resourceFilter = null;
if (resourceFilterDef != null) {
try {
resourceFilter = (IncludeAllFilter) Class.forName(resourceFilterDef).getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
throw new SetupException(e);
}
}
String resourceComparatorDef = node.getChildValue(null, RESOURCE_COMPARATOR, String.class);
Comparator resourceComparator = determineResourceComparator(resourceComparatorDef);
ContextExpression includeContextFilter = new ContextExpression(node.getChildValue(null, CONTEXT_FILTER, String.class));
if (includeContextFilter.isEmpty()) {
includeContextFilter = new ContextExpression(node.getChildValue(null, CONTEXT, String.class));
}
Labels labels = new Labels(node.getChildValue(null, LABELS, String.class));
Boolean ignore = node.getChildValue(null, IGNORE, Boolean.class);
if (ignore == null) {
ignore = false;
}
includeAll(path,
node.getChildValue(null, RELATIVE_TO_CHANGELOG_FILE, false), resourceFilter,
node.getChildValue(null, ERROR_IF_MISSING_OR_EMPTY, true),
resourceComparator,
resourceAccessor,
includeContextFilter,
labels,
ignore,
node.getChildValue(null, LOGICAL_FILE_PATH, String.class),
node.getChildValue(null, MIN_DEPTH, 0),
node.getChildValue(null, MAX_DEPTH, Integer.MAX_VALUE),
node.getChildValue(null, ENDS_WITH_FILTER, ""),
(ModifyChangeSets) nodeScratch.get(MODIFY_CHANGE_SETS));
break;
}
case PRE_CONDITIONS: {
PreconditionContainer parsedContainer = new PreconditionContainer();
parsedContainer.load(node, resourceAccessor);
this.preconditionContainer.addNestedPrecondition(parsedContainer);
break;
}
case REMOVE_CHANGE_SET_PROPERTY: {
List childNodes = node.getChildren();
Optional changeNode = childNodes.stream().filter(n -> n.getName().equalsIgnoreCase("change")).findFirst();
if(changeNode.isPresent()){
ChangeVisitor changeVisitor = ChangeVisitorFactory.getInstance().create((String) changeNode.get().getValue());
if(changeVisitor != null){
changeVisitor.load(node, resourceAccessor);
if(DatabaseList.definitionMatches(changeVisitor.getDbms(), changeLogParameters.getDatabase(), false)) {
//add changeVisitor to this changeLog only if the running database matches with one of the removeChangeSetProperty's dbms
getChangeVisitors().add(changeVisitor);
}
}
}
break;
}
case PROPERTY: {
try {
String contextFilter = node.getChildValue(null, CONTEXT_FILTER, String.class);
if (StringUtils.isEmpty(contextFilter)) {
contextFilter = 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, contextFilter, 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