liquibase.changelog.ChangeSet Maven / Gradle / Ivy
package liquibase.changelog;
import liquibase.Contexts;
import liquibase.change.Change;
import liquibase.change.ChangeFactory;
import liquibase.change.CheckSum;
import liquibase.change.DbmsTargetedChange;
import liquibase.change.core.EmptyChange;
import liquibase.change.core.RawSQLChange;
import liquibase.changelog.visitor.ChangeExecListener;
import liquibase.database.Database;
import liquibase.database.DatabaseList;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.exception.*;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogFactory;
import liquibase.logging.Logger;
import liquibase.precondition.Conditional;
import liquibase.precondition.core.ErrorPrecondition;
import liquibase.precondition.core.FailedPrecondition;
import liquibase.precondition.core.PreconditionContainer;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.sql.visitor.SqlVisitor;
import liquibase.statement.SqlStatement;
import liquibase.util.StreamUtil;
import liquibase.util.StringUtils;
import java.util.*;
/**
* Encapsulates a changeSet and all its associated changes.
*/
public class ChangeSet implements Conditional, LiquibaseSerializable {
public enum RunStatus {
NOT_RAN, ALREADY_RAN, RUN_AGAIN, MARK_RAN, INVALID_MD5SUM
}
public enum ExecType {
EXECUTED("EXECUTED", false, true),
FAILED("FAILED", false, false),
SKIPPED("SKIPPED", false, false),
RERAN("RERAN", true, true),
MARK_RAN("MARK_RAN", false, true);
ExecType(String value, boolean ranBefore, boolean ran) {
this.value = value;
this.ranBefore = ranBefore;
this.ran = ran;
}
public final String value;
public final boolean ranBefore;
public final boolean ran;
}
public enum ValidationFailOption {
HALT("HALT"),
MARK_RAN("MARK_RAN");
String key;
ValidationFailOption(String key) {
this.key = key;
}
@Override
public String toString() {
return key;
}
}
private ChangeLogParameters changeLogParameters;
/**
* List of change objects defined in this changeset
*/
private List changes;
/**
* "id" specified in changeLog file. Combination of id+author+filePath must be unique
*/
private String id;
/**
* "author" defined in changeLog file. Having each developer use a unique author tag allows duplicates of "id" attributes between developers.
*/
private String author;
/**
* File changeSet is defined in. May be a logical/non-physical string. It is included in the unique identifier to allow duplicate id+author combinations in different files
*/
private String filePath = "UNKNOWN CHANGE LOG";
private Logger log;
/**
* If set to true, the changeSet will be executed on every update. Defaults to false
*/
private boolean alwaysRun;
/**
* If set to true, the changeSet will be executed when the checksum changes. Defaults to false.
*/
private boolean runOnChange;
/**
* Runtime contexts in which the changeSet will be executed. If null or empty, will execute regardless of contexts set
*/
private Contexts contexts;
/**
* Databases for which this changeset should run. The string values should match the value returned from Database.getShortName()
*/
private Set dbmsSet;
/**
* If false, do not stop liquibase update execution if an error is thrown executing the changeSet. Defaults to true
*/
private Boolean failOnError;
/**
* List of checksums that are assumed to be valid besides the one stored in the database. Can include the string "any"
*/
private Set validCheckSums = new HashSet();
/**
* If true, the changeSet will run in a database transaction. Defaults to true
*/
private boolean runInTransaction;
/**
* Behavior if the validation of any of the changeSet changes fails. Does not include checksum validation
*/
private ValidationFailOption onValidationFail = ValidationFailOption.HALT;
/**
* Stores if validation failed on this chhangeSet
*/
private boolean validationFailed;
/**
* Changes defined to roll back this changeSet
*/
private List rollBackChanges = new ArrayList();
/**
* ChangeSet comments defined in changeLog file
*/
private String comments;
/**
* ChangeSet level precondtions defined for this changeSet
*/
private PreconditionContainer preconditions;
/**
* SqlVisitors defined for this changeset.
* SqlVisitors will modify the SQL generated by the changes before sending it to the database.
*/
private List sqlVisitors = new ArrayList();
private ObjectQuotingStrategy objectQuotingStrategy;
private DatabaseChangeLog changeLog;
public boolean shouldAlwaysRun() {
return alwaysRun;
}
public boolean shouldRunOnChange() {
return runOnChange;
}
public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath, String contextList, String dbmsList, DatabaseChangeLog databaseChangeLog) {
this(id, author, alwaysRun, runOnChange, filePath, contextList, dbmsList, true, ObjectQuotingStrategy.LEGACY, databaseChangeLog);
}
public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath, String contextList, String dbmsList, boolean runInTransaction, DatabaseChangeLog databaseChangeLog) {
this(id, author, alwaysRun, runOnChange, filePath, contextList, dbmsList, runInTransaction, ObjectQuotingStrategy.LEGACY, databaseChangeLog);
}
public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath, String contextList, String dbmsList, ObjectQuotingStrategy quotingStrategy, DatabaseChangeLog databaseChangeLog) {
this(id, author, alwaysRun, runOnChange, filePath, contextList, dbmsList, true, quotingStrategy, databaseChangeLog);
}
public ChangeSet(String id, String author, boolean alwaysRun, boolean runOnChange, String filePath, String contextList, String dbmsList,
boolean runInTransaction, ObjectQuotingStrategy quotingStrategy, DatabaseChangeLog databaseChangeLog) {
this.changes = new ArrayList();
log = LogFactory.getLogger();
this.id = id;
this.author = author;
this.filePath = filePath;
this.alwaysRun = alwaysRun;
this.runOnChange = runOnChange;
this.runInTransaction = runInTransaction;
this.objectQuotingStrategy = quotingStrategy;
this.contexts = new Contexts(contextList);
if (StringUtils.trimToNull(dbmsList) != null) {
String[] strings = dbmsList.toLowerCase().split(",");
dbmsSet = new HashSet();
for (String string : strings) {
dbmsSet.add(string.trim().toLowerCase());
}
}
this.changeLog = databaseChangeLog;
}
public String getFilePath() {
return filePath;
}
public CheckSum generateCheckSum() {
StringBuffer stringToMD5 = new StringBuffer();
for (Change change : getChanges()) {
stringToMD5.append(change.generateCheckSum()).append(":");
}
for (SqlVisitor visitor : this.getSqlVisitors()) {
stringToMD5.append(visitor.generateCheckSum()).append(";");
}
return CheckSum.compute(stringToMD5.toString());
}
public ExecType execute(DatabaseChangeLog databaseChangeLog, Database database) throws MigrationFailedException {
return execute(databaseChangeLog, null, database);
}
/**
* This method will actually execute each of the changes in the list against the
* specified database.
*
* @return should change set be marked as ran
*/
public ExecType execute(DatabaseChangeLog databaseChangeLog, ChangeExecListener listener, Database database) throws MigrationFailedException {
if (validationFailed) {
return ExecType.MARK_RAN;
}
long startTime = new Date().getTime();
ExecType execType = null;
boolean skipChange = false;
Executor executor = ExecutorService.getInstance().getExecutor(database);
try {
// set object quoting strategy
database.setObjectQuotingStrategy(objectQuotingStrategy);
// set auto-commit based on runInTransaction if database supports DDL in transactions
if (database.supportsDDLInTransaction()) {
database.setAutoCommit(!runInTransaction);
}
executor.comment("Changeset " + toString(false));
if (StringUtils.trimToNull(getComments()) != null) {
String comments = getComments();
String[] lines = comments.split("\n");
for (int i = 0; i < lines.length; i++) {
if (i > 0) {
lines[i] = database.getLineComment() + " " + lines[i];
}
}
executor.comment(StringUtils.join(Arrays.asList(lines), "\n"));
}
try {
if (preconditions != null) {
preconditions.check(database, databaseChangeLog, this);
}
} catch (PreconditionFailedException e) {
if (listener != null) {
listener.preconditionFailed(e, preconditions.getOnFail());
}
StringBuffer message = new StringBuffer();
message.append(StreamUtil.getLineSeparator());
for (FailedPrecondition invalid : e.getFailedPreconditions()) {
message.append(" ").append(invalid.toString());
message.append(StreamUtil.getLineSeparator());
}
if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.HALT)) {
throw new MigrationFailedException(this, message.toString(), e);
} else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.CONTINUE)) {
skipChange = true;
execType = ExecType.SKIPPED;
LogFactory.getLogger().info("Continuing past: " + toString() + " despite precondition failure due to onFail='CONTINUE': " + message);
} else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.MARK_RAN)) {
execType = ExecType.MARK_RAN;
skipChange = true;
log.info("Marking ChangeSet: " + toString() + " ran despite precondition failure due to onFail='MARK_RAN': " + message);
} else if (preconditions.getOnFail().equals(PreconditionContainer.FailOption.WARN)) {
execType = null; //already warned
} else {
throw new UnexpectedLiquibaseException("Unexpected precondition onFail attribute: " + preconditions.getOnFail(), e);
}
} catch (PreconditionErrorException e) {
if (listener != null) {
listener.preconditionErrored(e, preconditions.getOnError());
}
StringBuffer message = new StringBuffer();
message.append(StreamUtil.getLineSeparator());
for (ErrorPrecondition invalid : e.getErrorPreconditions()) {
message.append(" ").append(invalid.toString());
message.append(StreamUtil.getLineSeparator());
}
if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.HALT)) {
throw new MigrationFailedException(this, message.toString(), e);
} else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.CONTINUE)) {
skipChange = true;
execType = ExecType.SKIPPED;
} else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.MARK_RAN)) {
execType = ExecType.MARK_RAN;
skipChange = true;
log.info("Marking ChangeSet: " + toString() + " ran despite precondition error: " + message);
} else if (preconditions.getOnError().equals(PreconditionContainer.ErrorOption.WARN)) {
execType = null; //already logged
} else {
throw new UnexpectedLiquibaseException("Unexpected precondition onError attribute: " + preconditions.getOnError(), e);
}
database.rollback();
} finally {
database.rollback();
}
if (!skipChange) {
for (Change change : changes) {
try {
change.finishInitialization();
} catch (SetupException se) {
throw new MigrationFailedException(this, se);
}
}
log.debug("Reading ChangeSet: " + toString());
for (Change change : getChanges()) {
if ((!(change instanceof DbmsTargetedChange)) || DatabaseList.definitionMatches(((DbmsTargetedChange) change).getDbms(), database, true)) {
if (listener != null) {
listener.willRun(change, this, changeLog, database);
}
database.executeStatements(change, databaseChangeLog, sqlVisitors);
log.info(change.getConfirmationMessage());
if (listener != null) {
listener.ran(change, this, changeLog, database);
}
} else {
log.debug("Change " + change.getSerializedObjectName() + " not included for database " + database.getShortName());
}
}
if (runInTransaction) {
database.commit();
}
log.info("ChangeSet " + toString(false) + " ran successfully in " + (new Date().getTime() - startTime + "ms"));
if (execType == null) {
execType = ExecType.EXECUTED;
}
} else {
log.debug("Skipping ChangeSet: " + toString());
}
} catch (Exception e) {
try {
database.rollback();
} catch (Exception e1) {
throw new MigrationFailedException(this, e);
}
if (getFailOnError() != null && !getFailOnError()) {
log.info("Change set " + toString(false) + " failed, but failOnError was false. Error: " + e.getMessage());
log.debug("Failure Stacktrace", e);
execType = ExecType.FAILED;
} else {
log.severe("Change Set " + toString(false) + " failed. Error: " + e.getMessage(), e);
if (e instanceof MigrationFailedException) {
throw ((MigrationFailedException) e);
} else {
throw new MigrationFailedException(this, e);
}
}
} finally {
// restore auto-commit to false if this ChangeSet was not run in a transaction,
// but only if the database supports DDL in transactions
if (!runInTransaction && database.supportsDDLInTransaction()) {
try {
database.setAutoCommit(false);
} catch (DatabaseException e) {
throw new MigrationFailedException(this, "Could not resetInternalState autocommit", e);
}
}
}
return execType;
}
public void rollback(Database database) throws RollbackFailedException {
try {
Executor executor = ExecutorService.getInstance().getExecutor(database);
executor.comment("Rolling Back ChangeSet: " + toString());
// set auto-commit based on runInTransaction if database supports DDL in transactions
if (database.supportsDDLInTransaction()) {
database.setAutoCommit(!runInTransaction);
}
RanChangeSet ranChangeSet = database.getRanChangeSet(this);
if (rollBackChanges != null && rollBackChanges.size() > 0) {
for (Change rollback : rollBackChanges) {
if (((rollback instanceof DbmsTargetedChange)) && !DatabaseList.definitionMatches(((DbmsTargetedChange) rollback).getDbms(), database, true)) {
continue;
}
SqlStatement[] statements = rollback.generateStatements(database);
if (statements == null) {
continue;
}
for (SqlStatement statement : statements) {
try {
executor.execute(statement, sqlVisitors);
} catch (DatabaseException e) {
throw new RollbackFailedException("Error executing custom SQL [" + statement + "]", e);
}
}
}
} else {
List changes = getChanges();
for (int i = changes.size() - 1; i >= 0; i--) {
Change change = changes.get(i);
database.executeRollbackStatements(change, sqlVisitors);
}
}
if (runInTransaction) {
database.commit();
}
log.debug("ChangeSet " + toString() + " has been successfully rolled back.");
} catch (Exception e) {
try {
database.rollback();
} catch (DatabaseException e1) {
//ok
}
throw new RollbackFailedException(e);
} finally {
// restore auto-commit to false if this ChangeSet was not run in a transaction,
// but only if the database supports DDL in transactions
if (!runInTransaction && database.supportsDDLInTransaction()) {
try {
database.setAutoCommit(false);
} catch (DatabaseException e) {
throw new RollbackFailedException("Could not resetInternalState autocommit", e);
}
}
}
}
/**
* Returns an unmodifiable list of changes. To add one, use the addRefactoing method.
*/
public List getChanges() {
return Collections.unmodifiableList(changes);
}
public void addChange(Change change) {
changes.add(change);
change.setChangeSet(this);
}
public String getId() {
return id;
}
public String getAuthor() {
return author;
}
public Contexts getContexts() {
return contexts;
}
public Set getDbmsSet() {
return dbmsSet;
}
public DatabaseChangeLog getChangeLog() {
return changeLog;
}
public String toString(boolean includeMD5Sum) {
return filePath + "::" + getId() + "::" + getAuthor() + (includeMD5Sum ? ("::(Checksum: " + generateCheckSum() + ")") : "");
}
@Override
public String toString() {
return toString(false);
}
public String getComments() {
return comments;
}
public void setComments(String comments) {
this.comments = comments;
}
public boolean isAlwaysRun() {
return alwaysRun;
}
public boolean isRunOnChange() {
return runOnChange;
}
public boolean isRunInTransaction() {
return runInTransaction;
}
public Change[] getRollBackChanges() {
return rollBackChanges.toArray(new Change[rollBackChanges.size()]);
}
public void addRollBackSQL(String sql) {
if (StringUtils.trimToNull(sql) == null) {
rollBackChanges.add(new EmptyChange());
return;
}
for (String statment : StringUtils.splitSQL(sql, null)) {
rollBackChanges.add(new RawSQLChange(statment.trim()));
}
}
public void addRollbackChange(Change change) {
rollBackChanges.add(change);
}
public boolean supportsRollback(Database database) {
if (rollBackChanges != null && rollBackChanges.size() > 0) {
return true;
}
for (Change change : getChanges()) {
if (!change.supportsRollback(database)) {
return false;
}
}
return true;
}
public String getDescription() {
List changes = getChanges();
if (changes.size() == 0) {
return "Empty";
}
StringBuffer returnString = new StringBuffer();
Class extends Change> lastChangeClass = null;
int changeCount = 0;
for (Change change : changes) {
if (change.getClass().equals(lastChangeClass)) {
changeCount++;
} else if (changeCount > 1) {
returnString.append(" (x").append(changeCount).append(")");
returnString.append(", ");
returnString.append(ChangeFactory.getInstance().getChangeMetaData(change).getName());
changeCount = 1;
} else {
returnString.append(", ").append(ChangeFactory.getInstance().getChangeMetaData(change).getName());
changeCount = 1;
}
lastChangeClass = change.getClass();
}
if (changeCount > 1) {
returnString.append(" (x").append(changeCount).append(")");
}
return returnString.toString().replaceFirst("^, ", "");
}
public Boolean getFailOnError() {
return failOnError;
}
public void setFailOnError(Boolean failOnError) {
this.failOnError = failOnError;
}
public ValidationFailOption getOnValidationFail() {
return onValidationFail;
}
public void setOnValidationFail(ValidationFailOption onValidationFail) {
this.onValidationFail = onValidationFail;
}
public void setValidationFailed(boolean validationFailed) {
this.validationFailed = validationFailed;
}
public void addValidCheckSum(String text) {
validCheckSums.add(CheckSum.parse(text));
}
public boolean isCheckSumValid(CheckSum storedCheckSum) {
// no need to generate the checksum if any has been set as the valid checksum
for (CheckSum validCheckSum : validCheckSums) {
if (validCheckSum.toString().equalsIgnoreCase("1:any")) {
return true;
}
}
CheckSum currentMd5Sum = generateCheckSum();
if (currentMd5Sum == null) {
return true;
}
if (storedCheckSum == null) {
return true;
}
if (currentMd5Sum.equals(storedCheckSum)) {
return true;
}
for (CheckSum validCheckSum : validCheckSums) {
if (currentMd5Sum.equals(validCheckSum)) {
return true;
}
}
return false;
}
@Override
public PreconditionContainer getPreconditions() {
return preconditions;
}
@Override
public void setPreconditions(PreconditionContainer preconditionContainer) {
this.preconditions = preconditionContainer;
}
public void addSqlVisitor(SqlVisitor sqlVisitor) {
sqlVisitors.add(sqlVisitor);
}
public List getSqlVisitors() {
return sqlVisitors;
}
public ChangeLogParameters getChangeLogParameters() {
return changeLogParameters;
}
/**
* Called by the changelog parsing process to pass the {@link ChangeLogParameters}.
*/
public void setChangeLogParameters(ChangeLogParameters changeLogParameters) {
this.changeLogParameters = changeLogParameters;
}
public ObjectQuotingStrategy getObjectQuotingStrategy() {
return objectQuotingStrategy;
}
@Override
public String getSerializedObjectName() {
return "changeSet";
}
@Override
public Set getSerializableFields() {
return new HashSet(Arrays.asList(
"id",
"author",
"runAlways",
"runOnChange",
"failOnError",
"context",
"dbms",
"comment",
"changes",
"rollback"));
}
@Override
public Object getSerializableFieldValue(String field) {
if (field.equals("id")) {
return this.getId();
}
if (field.equals("author")) {
return this.getAuthor();
}
if (field.equals("runAlways")) {
if (this.isAlwaysRun()) {
return true;
} else {
return null;
}
}
if (field.equals("runOnChange")) {
if (this.isRunOnChange()) {
return true;
} else {
return null;
}
}
if (field.equals("failOnError")) {
return this.getFailOnError();
}
if (field.equals("context")) {
if (this.getContexts() != null && this.getContexts().size() > 0) {
StringBuffer contextString = new StringBuffer();
for (String context : this.getContexts()) {
contextString.append(context).append(",");
}
return contextString.toString().replaceFirst(",$", "");
} else {
return null;
}
}
if (field.equals("dbms")) {
if (this.getDbmsSet() != null && this.getDbmsSet().size() > 0) {
StringBuffer dbmsString = new StringBuffer();
for (String dbms : this.getDbmsSet()) {
dbmsString.append(dbms).append(",");
}
return dbmsString.toString().replaceFirst(",$", "");
} else {
return null;
}
}
if (field.equals("comment")) {
return StringUtils.trimToNull(this.getComments());
}
if (field.equals("changes")) {
return getChanges();
}
if (field.equals("rollback")) {
if (this.getRollBackChanges() != null && this.getRollBackChanges().length > 0) {
return this.getRollBackChanges();
} else {
return null;
}
}
throw new UnexpectedLiquibaseException("Unexpected field request on changeSet: "+field);
}
@Override
public SerializationType getSerializableFieldType(String field) {
if (field.equals("comment") || field.equals("changes") || field.equals("rollback")) {
return SerializationType.NESTED_OBJECT;
// } else if (field.equals()) {
// return SerializationType.DIRECT_VALUE;
} else {
return SerializationType.NAMED_FIELD;
}
}
@Override
public String getSerializedObjectNamespace() {
return STANDARD_CHANGELOG_NAMESPACE;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy