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.
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.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.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.FileUtil;
import liquibase.util.StringUtil;
import java.io.IOException;
import java.io.InputStream;
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]:");
public static final String SEEN_CHANGELOGS_PATHS_SCOPE_KEY = "SEEN_CHANGELOG_PATHS";
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 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;
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 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)
);
ValidatingVisitor validatingVisitor = new ValidatingVisitor(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 {
setLogicalFilePath(parsedNode.getChildValue(null, "logicalFilePath", String.class));
String context = parsedNode.getChildValue(null, "contextFilter", 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()) {
handleChildNode(childNode, resourceAccessor, new HashMap<>());
}
}
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 {
expandExpressions(node);
String nodeName = node.getName();
switch (nodeName) {
case "changeSet":
if (isDbmsMatch(node.getChildValue(null, "dbms", String.class))) {
this.addChangeSet(createChangeSet(node, resourceAccessor));
} else {
handleSkippedChangeSet(node);
}
break;
case "modifyChangeSets":
ModifyChangeSets modifyChangeSets = createModifyChangeSets(node);
nodeScratch = new HashMap<>();
nodeScratch.put("modifyChangeSets", modifyChangeSets);
for (ParsedNode modifyChildNode : node.getChildren()) {
handleChildNode(modifyChildNode, resourceAccessor, nodeScratch);
}
nodeScratch.remove("modifyChangeSets");
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, "contextFilter", 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, "relativeToChangelogFile", false),
node.getChildValue(null, "errorIfMissing", true),
resourceAccessor,
includeContextFilter,
labels,
ignore,
OnUnknownFileFormat.FAIL,
(ModifyChangeSets) nodeScratch.get("modifyChangeSets"));
} catch (LiquibaseException e) {
throw new SetupException(e);
}
break;
}
case "includeAll": {
String path = node.getChildValue(null, "path", String.class);
String resourceFilterDef = node.getChildValue(null, "filter", String.class);
if (resourceFilterDef == null) {
resourceFilterDef = node.getChildValue(null, "resourceFilter", 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, "resourceComparator", String.class);
Comparator resourceComparator = null;
if (resourceComparatorDef == null) {
resourceComparator = getStandardChangeLogComparator();
} else {
try {
resourceComparator = (Comparator) Class.forName(resourceComparatorDef).getConstructor().newInstance();
} catch (ReflectiveOperationException e) {
//take default comparator
Scope.getCurrentScope().getLog(getClass()).info("no resourceComparator defined - taking default " +
"implementation");
resourceComparator = getStandardChangeLogComparator();
}
}
ContextExpression includeContextFilter = new ContextExpression(node.getChildValue(null, "contextFilter", 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, "relativeToChangelogFile", false), resourceFilter,
node.getChildValue(null, "errorIfMissingOrEmpty", true),
resourceComparator,
resourceAccessor,
includeContextFilter,
labels,
ignore,
node.getChildValue(null, "minDepth", 1),
node.getChildValue(null, "maxDepth", Integer.MAX_VALUE),
node.getChildValue(null, "endsWithFilter", ""),
(ModifyChangeSets) nodeScratch.get("modifyChangeSets"));
break;
}
case "preConditions": {
PreconditionContainer parsedContainer = new PreconditionContainer();
parsedContainer.load(node, resourceAccessor);
this.preconditionContainer.addNestedPrecondition(parsedContainer);
break;
}
case "removeChangeSetProperty": {
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, "contextFilter", String.class);
if (StringUtil.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, "relativeToChangelogFile", Boolean.FALSE);
Boolean errorIfMissing = node.getChildValue(null, "errorIfMissing", 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