package liquibase.changelog;

import liquibase.ContextExpression;
import liquibase.Labels;
import liquibase.Scope;
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.LogType;
import liquibase.logging.Logger;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.precondition.Conditional;
import liquibase.precondition.ErrorPrecondition;
import liquibase.precondition.FailedPrecondition;
import liquibase.precondition.core.PreconditionContainer;
import liquibase.resource.ResourceAccessor;
import liquibase.sql.visitor.SqlVisitor;
import liquibase.sql.visitor.SqlVisitorFactory;
import liquibase.statement.SqlStatement;
import liquibase.util.StreamUtil;
import liquibase.util.StringUtil;

import java.util.*;

/**
 * Encapsulates a changeSet and all its associated changes.
 */
public class ChangeSet implements Conditional, ChangeLogChild {

    protected CheckSum checkSum;

    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;
        }
    }

    protected String key;

    private ChangeLogParameters changeLogParameters;

    /**
     * List of change objects defined in this changeset
     */
    private List<Change> 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";

    /**
     * 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 ContextExpression contexts;

    /**
     * "Labels" associated with this changeSet.  If null or empty, will execute regardless of contexts set
     */
    private Labels labels;

    /**
     *
     * If set to true, the changeSet will be ignored (skipped)
     *
     */
    private boolean ignore;

    /**
     * Databases for which this changeset should run.  The string values should match the value returned from Database.getShortName()
     */
    private Set<String> 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<CheckSum> 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 ChangeSet
     */
    private boolean validationFailed;

    /**
     * Changes defined to roll back this changeSet
     */
    private RollbackContainer rollback = new RollbackContainer();

    /**
     * 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<SqlVisitor> sqlVisitors = new ArrayList<>();

    private ObjectQuotingStrategy objectQuotingStrategy;

    private DatabaseChangeLog changeLog;

    private String created;

    /**
     * Allow changeSet to be ran "first" or "last". Multiple changeSets with the same runOrder will preserve their order relative to each other.
     */
    private String runOrder;

    private Map<String, Object> attributes = new HashMap<>();

    public boolean shouldAlwaysRun() {
        return alwaysRun;
    }

    public boolean shouldRunOnChange() {
        return runOnChange;
    }

    public ChangeSet(DatabaseChangeLog databaseChangeLog) {
        this.changes = new ArrayList<>();
        this.changeLog = databaseChangeLog;
    }

    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(databaseChangeLog);
        this.id = id;
        this.author = author;
        this.filePath = filePath;
        this.alwaysRun = alwaysRun;
        this.runOnChange = runOnChange;
        this.runInTransaction = runInTransaction;
        this.objectQuotingStrategy = quotingStrategy;
        this.contexts = new ContextExpression(contextList);
        setDbms(dbmsList);
    }

    protected void setDbms(String dbmsList) {
        this.dbmsSet = DatabaseList.toDbmsSet(dbmsList);
    }

    public String getFilePath() {
        return filePath;
    }

    public void clearCheckSum() {
        this.checkSum = null;
    }

    public CheckSum generateCheckSum() {
        if (checkSum == null) {
            StringBuffer stringToMD5 = new StringBuffer();
            for (Change change : getChanges()) {
                stringToMD5.append(change.generateCheckSum()).append(":");
            }

            for (SqlVisitor visitor : this.getSqlVisitors()) {
                stringToMD5.append(visitor.generateCheckSum()).append(";");
            }

            checkSum = CheckSum.compute(stringToMD5.toString());
        }

        return checkSum;
    }

    @Override
    public void load(ParsedNode node, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        this.id = node.getChildValue(null, "id", String.class);
        this.author = node.getChildValue(null, "author", String.class);
        this.alwaysRun  = node.getChildValue(null, "runAlways", node.getChildValue(null, "alwaysRun", false));
        this.runOnChange  = node.getChildValue(null, "runOnChange", false);
        this.contexts = new ContextExpression(node.getChildValue(null, "context", String.class));
        this.labels = new Labels(StringUtil.trimToNull(node.getChildValue(null, "labels", String.class)));
        setDbms(node.getChildValue(null, "dbms", String.class));
        this.runInTransaction  = node.getChildValue(null, "runInTransaction", true);
        this.created = node.getChildValue(null, "created", String.class);
        this.runOrder = node.getChildValue(null, "runOrder", String.class);
        this.ignore = node.getChildValue(null, "ignore", false);
        this.comments = StringUtil.join(node.getChildren(null, "comment"), "\n", new StringUtil.StringUtilFormatter() {
            @Override
            public String toString(Object obj) {
                if (((ParsedNode) obj).getValue() == null) {
                    return "";
                } else {
                    return ((ParsedNode) obj).getValue().toString();
                }
            }
        });
        this.comments = StringUtil.trimToNull(this.comments);

        String objectQuotingStrategyString = StringUtil.trimToNull(node.getChildValue(null, "objectQuotingStrategy", String.class));
        if (changeLog != null) {
            this.objectQuotingStrategy = changeLog.getObjectQuotingStrategy();
        }
        if (objectQuotingStrategyString != null) {
            this.objectQuotingStrategy = ObjectQuotingStrategy.valueOf(objectQuotingStrategyString);
        }

        if (this.objectQuotingStrategy == null) {
            this.objectQuotingStrategy = ObjectQuotingStrategy.LEGACY;
        }

        this.filePath = StringUtil.trimToNull(node.getChildValue(null, "logicalFilePath", String.class));
        if (filePath == null) {
            filePath = changeLog.getFilePath();
        }

        this.setFailOnError(node.getChildValue(null, "failOnError", Boolean.class));
        String onValidationFailString = node.getChildValue(null, "onValidationFail", "HALT");
        this.setOnValidationFail(ValidationFailOption.valueOf(onValidationFailString));

        for (ParsedNode child : node.getChildren()) {
            handleChildNode(child, resourceAccessor);
        }
    }

    protected void handleChildNode(ParsedNode child, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        switch (child.getName()) {
            case "rollback":
                handleRollbackNode(child, resourceAccessor);
                break;
            case "validCheckSum":
            case "validCheckSums":
                if (child.getValue() == null) {
                    return;
                }

                if (child.getValue() instanceof Collection) {
                    for (Object checksum : (Collection) child.getValue()) {
                        addValidCheckSum((String) checksum);
                    }
                } else {
                    addValidCheckSum(child.getValue(String.class));
                }
                break;
            case "modifySql":
                String dbmsString = StringUtil.trimToNull(child.getChildValue(null, "dbms", String.class));
                String contextString = StringUtil.trimToNull(child.getChildValue(null, "context", String.class));
                String labelsString = StringUtil.trimToNull(child.getChildValue(null, "labels", String.class));
                boolean applyToRollback = child.getChildValue(null, "applyToRollback", false);

                Set<String> dbms = new HashSet<>();
                if (dbmsString != null) {
                    dbms.addAll(StringUtil.splitAndTrim(dbmsString, ","));
                }
                ContextExpression context = null;
                if (contextString != null) {
                    context = new ContextExpression(contextString);
                }

                Labels labels = null;
                if (labelsString != null) {
                    labels = new Labels(labelsString);
                }

                List<ParsedNode> potentialVisitors = child.getChildren();
                for (ParsedNode node : potentialVisitors) {
                    SqlVisitor sqlVisitor = SqlVisitorFactory.getInstance().create(node.getName());
                    if (sqlVisitor != null) {
                        sqlVisitor.setApplyToRollback(applyToRollback);
                        if (!dbms.isEmpty()) {
                            sqlVisitor.setApplicableDbms(dbms);
                        }
                        sqlVisitor.setContexts(context);
                        sqlVisitor.setLabels(labels);
                        sqlVisitor.load(node, resourceAccessor);

                        addSqlVisitor(sqlVisitor);
                    }
                }

                break;
            case "preConditions":
                this.preconditions = new PreconditionContainer();
                try {
                    this.preconditions.load(child, resourceAccessor);
                } catch (ParsedNodeException e) {
                    e.printStackTrace();
                }
                break;
            case "changes":
                for (ParsedNode changeNode : child.getChildren()) {
                    handleChildNode(changeNode, resourceAccessor);
                }
                break;
            default:
                Change change = toChange(child, resourceAccessor);
                if ((change == null) && (child.getValue() instanceof String)) {
                    this.setAttribute(child.getName(), child.getValue());
                } else {
                    addChange(change);
                }
                break;
        }
    }

    protected void handleRollbackNode(ParsedNode rollbackNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        String changeSetId = rollbackNode.getChildValue(null, "changeSetId", String.class);
        if (changeSetId != null) {
            String changeSetAuthor = rollbackNode.getChildValue(null, "changeSetAuthor", String.class);
            String changeSetPath = rollbackNode.getChildValue(null, "changeSetPath", getFilePath());

            DatabaseChangeLog changeLog = this.getChangeLog();
            ChangeSet changeSet = changeLog.getChangeSet(changeSetPath, changeSetAuthor, changeSetId);
            while ((changeSet == null) && (changeLog != null)) {
                changeLog = changeLog.getParentChangeLog();
                if (changeLog != null) {
                    changeSet = changeLog.getChangeSet(changeSetPath, changeSetAuthor, changeSetId);
                }
            }
            if (changeSet == null) {
                throw new ParsedNodeException("Change set " + new ChangeSet(changeSetId, changeSetAuthor, false, false, changeSetPath, null, null, null).toString(false) + " does not exist");
            }
            for (Change change : changeSet.getChanges()) {
                rollback.getChanges().add(change);
            }
            return;
        }

        boolean foundValue = false;
        for (ParsedNode childNode : rollbackNode.getChildren()) {
            Change rollbackChange = toChange(childNode, resourceAccessor);
            if (rollbackChange != null) {
                addRollbackChange(rollbackChange);
                foundValue =  true;
            }
        }

        Object value = rollbackNode.getValue();
        if (value != null) {
            if (value instanceof String) {
                String finalValue = StringUtil.trimToNull((String) value);
                if (finalValue != null) {
                    String[] strings = StringUtil.processMutliLineSQL(finalValue, true, true, ";");
                    for (String string : strings) {
                        addRollbackChange(new RawSQLChange(string));
                        foundValue = true;
                    }
                }
            } else {
                throw new ParsedNodeException("Unexpected object: "+value.getClass().getName()+" '"+value.toString()+"'");
            }
        }
        if (!foundValue) {
            addRollbackChange(new EmptyChange());
        }
    }

    protected Change toChange(ParsedNode value, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        Change change = Scope.getCurrentScope().getSingleton(ChangeFactory.class).create(value.getName());
        if (change == null) {
            return null;
        } else {
            change.load(value, resourceAccessor);

            return change;
        }
    }

    @Override
    public ParsedNode serialize() {
        throw new RuntimeException("TODO");
    }

    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 {
        Logger log = Scope.getCurrentScope().getLog(getClass());

        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);

            if (database.supportsDDLInTransaction()) {
                database.setAutoCommit(!runInTransaction);
            }

            executor.comment("Changeset " + toString(false));
            if (StringUtil.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(StringUtil.join(Arrays.asList(lines), "\n"));
            }

            try {
                if (preconditions != null) {
                    preconditions.check(database, databaseChangeLog, this, listener);
                }
            } 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;

                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "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(LogType.LOG, "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(LogType.LOG, "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.fine(LogType.LOG, "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);
                        }
                        if (change.generateStatementsVolatile(database)) {
                            executor.comment("WARNING The following SQL may change each run and therefore is possibly incorrect and/or invalid:");
                        }

                        database.executeStatements(change, databaseChangeLog, sqlVisitors);
                        log.info(LogType.LOG, change.getConfirmationMessage());
                        if (listener != null) {
                            listener.ran(change, this, changeLog, database);
                        }
                    } else {
                        log.fine(LogType.LOG, "Change " + change.getSerializedObjectName() + " not included for database " + database.getShortName());
                    }
                }

                if (runInTransaction) {
                    database.commit();
                }
                log.info(LogType.LOG, "ChangeSet " + toString(false) + " ran successfully in " + (new Date().getTime() - startTime + "ms"));
                if (execType == null) {
                    execType = ExecType.EXECUTED;
                }
            } else {
                log.fine(LogType.LOG, "Skipping ChangeSet: " + toString());
            }

        } catch (Exception e) {
            try {
                database.rollback();
            } catch (Exception e1) {
                throw new MigrationFailedException(this, e);
            }
            if ((getFailOnError() != null) && !getFailOnError()) {
                log.info(LogType.LOG, "Change set " + toString(false) + " failed, but failOnError was false.  Error: " + e.getMessage());
                log.fine(LogType.LOG, "Failure Stacktrace", e);
                execType = ExecType.FAILED;
            } else {
                // just log the message, dont log the stacktrace by appending exception. Its logged anyway to stdout
                log.severe(LogType.LOG, "Change Set " + toString(false) + " failed.  Error: " + e.getMessage());
                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 {
      rollback(database, null);
    }

    public void rollback(Database database, ChangeExecListener listener) throws RollbackFailedException {
        try {
            Executor executor = ExecutorService.getInstance().getExecutor(database);
            executor.comment("Rolling Back ChangeSet: " + toString());

            database.setObjectQuotingStrategy(objectQuotingStrategy);

            // set auto-commit based on runInTransaction if database supports DDL in transactions
            if (database.supportsDDLInTransaction()) {
                database.setAutoCommit(!runInTransaction);
            }

            RanChangeSet ranChangeSet = database.getRanChangeSet(this);
            if (hasCustomRollbackChanges()) {

                final List<SqlStatement> statements = new LinkedList<>();
                for (Change change : rollback.getChanges()) {
                    if (((change instanceof DbmsTargetedChange)) && !DatabaseList.definitionMatches(((DbmsTargetedChange) change).getDbms(), database, true)) {
                        continue;
                    }
                    if (listener != null) {
                        listener.willRun(change, this, changeLog, database);
                    }
                    ValidationErrors errors = change.validate(database);
                    if (errors.hasErrors()) {
                        throw new RollbackFailedException("Rollback statement failed validation: "+errors.toString());
                    }
                    //
                    SqlStatement[] changeStatements = change.generateStatements(database);
                    if (changeStatements != null) {
                        statements.addAll(Arrays.asList(changeStatements));
                    }
                    if (listener != null) {
                        listener.ran(change, this, changeLog, database);
                    }
                }
                if (!statements.isEmpty()) {
                    database.executeRollbackStatements(statements.toArray(new SqlStatement[]{}), sqlVisitors);
                }

            } else {
                List<Change> changes = getChanges();
                for (int i = changes.size() - 1; i >= 0; i--) {
                    Change change = changes.get(i);
                    database.executeRollbackStatements(change, sqlVisitors);
                }
            }

            if (runInTransaction) {
                database.commit();
            }
            Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, "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 whether custom rollback steps are specified for this changeSet, or whether auto-generated ones should be used
     */
    protected boolean hasCustomRollbackChanges() {
        return (rollback != null) && (rollback.getChanges() != null) && !rollback.getChanges().isEmpty();
    }

    /**
     * Returns an unmodifiable list of changes.  To add one, use the addRefactoing method.
     */
    public List<Change> getChanges() {
        return Collections.unmodifiableList(changes);
    }

    public void addChange(Change change) {
        if (change == null) {
            return;
        }
        changes.add(change);
        change.setChangeSet(this);
    }

    public String getId() {
        return id;
    }

    public String getAuthor() {
        return author;
    }

    public ContextExpression getContexts() {
        return contexts;
    }

    public Labels getLabels() {
        return labels;
    }

    public void setLabels(Labels labels) {
        this.labels = labels;
    }

    public Set<String> getDbmsSet() {
        return dbmsSet;
    }

    public boolean isIgnore() {
        return ignore;
    }

    public void setIgnore(boolean ignore) {
        this.ignore = ignore;
    }

    public Collection<ContextExpression> getInheritableContexts() {
        Collection<ContextExpression> expressions = new ArrayList<>();
        DatabaseChangeLog changeLog = getChangeLog();
        while (changeLog != null) {
            ContextExpression expression = changeLog.getContexts();
            if ((expression != null) && !expression.isEmpty()) {
                expressions.add(expression);
            }
            ContextExpression includeExpression = changeLog.getIncludeContexts();
            if ((includeExpression != null) && !includeExpression.isEmpty()) {
                expressions.add(includeExpression);
            }
            changeLog = changeLog.getParentChangeLog();
        }
        return Collections.unmodifiableCollection(expressions);
    }

    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 RollbackContainer getRollback() {
        return rollback;
    }

    public void addRollBackSQL(String sql) {
        if (StringUtil.trimToNull(sql) == null) {
            if (rollback.getChanges().isEmpty()) {
                rollback.getChanges().add(new EmptyChange());
            }
            return;
        }

        for (String statment : StringUtil.splitSQL(sql, null)) {
            rollback.getChanges().add(new RawSQLChange(statment.trim()));
        }
    }

    public void addRollbackChange(Change change) {
        if (change == null) {
            return;
        }
        rollback.getChanges().add(change);
        change.setChangeSet(this);
    }

    public boolean supportsRollback(Database database) {
        if ((rollback != null) && (rollback.getChanges() != null) && !rollback.getChanges().isEmpty()) {
            return true;
        }

        for (Change change : getChanges()) {
            if (!change.supportsRollback(database)) {
                return false;
            }
        }
        return true;
    }

    public String getDescription() {
        List<Change> changes = getChanges();
        if (changes.isEmpty()) {
            return "empty";
        }

        List<String> messages = new ArrayList<>();
        for (Change change : changes) {
            messages.add(change.getDescription());
        }

        return StringUtil.limitSize(StringUtil.join(messages, "; "), 255);
    }

    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 Set<CheckSum> getValidCheckSums() {
        return Collections.unmodifiableSet(validCheckSums);
    }

    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 ("1:any".equalsIgnoreCase(validCheckSum.toString())
                || "1:all".equalsIgnoreCase(validCheckSum.toString())
                || "1:*".equalsIgnoreCase(validCheckSum.toString())) {
                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) || storedCheckSum.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<SqlVisitor> 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;
    }

    /**
     * Called to update file path from database entry when rolling back and ignoreClasspathPrefix is true.
     */
    public void setFilePath(String filePath) {
        this.filePath = filePath;
    }

    public ObjectQuotingStrategy getObjectQuotingStrategy() {
        return objectQuotingStrategy;
    }

    public String getCreated() {
        return created;
    }

    public void setCreated(String created) {
        this.created = created;
    }

    public String getRunOrder() {
        return runOrder;
    }

    public void setRunOrder(String runOrder) {
        if (runOrder != null) {
            runOrder = runOrder.toLowerCase();
            if (!"first".equals(runOrder) && !"last".equals(runOrder)) {
                throw new UnexpectedLiquibaseException("runOrder must be 'first' or 'last'");
            }
        }
        this.runOrder = runOrder;
    }

    @Override
    public String getSerializedObjectName() {
        return "changeSet";
    }

    @Override
    public Set<String> getSerializableFields() {
        return new LinkedHashSet<>(
            Arrays.asList(
                "id", "author", "runAlways", "runOnChange", "failOnError", "context", "labels", "dbms",
                "objectQuotingStrategy", "comment", "preconditions", "changes", "rollback", "labels",
                "objectQuotingStrategy", "created"
            )
        );
    }

    @Override
    public Object getSerializableFieldValue(String field) {
        if ("id".equals(field)) {
            return this.getId();
        }
        if ("author".equals(field)) {
            return this.getAuthor();
        }

        if ("runAlways".equals(field)) {
            if (this.isAlwaysRun()) {
                return true;
            } else {
                return null;
            }
        }

        if ("runOnChange".equals(field)) {
            if (this.isRunOnChange()) {
                return true;
            } else {
                return null;
            }
        }

        if ("failOnError".equals(field)) {
            return this.getFailOnError();
        }

        if ("context".equals(field)) {
            if (!this.getContexts().isEmpty()) {
                return this.getContexts().toString().replaceFirst("^\\(", "").replaceFirst("\\)$", "");
            } else {
                return null;
            }
        }

        if ("labels".equals(field)) {
            if ((this.getLabels() != null) && !this.getLabels().isEmpty()) {
                return StringUtil.join(this.getLabels().getLabels(), ", ");
            } else {
                return null;
            }
        }

        if ("dbms".equals(field)) {
            if ((this.getDbmsSet() != null) && !this.getDbmsSet().isEmpty()) {
                StringBuffer dbmsString = new StringBuffer();
                for (String dbms : this.getDbmsSet()) {
                    dbmsString.append(dbms).append(",");
                }
                return dbmsString.toString().replaceFirst(",$", "");
            } else {
                return null;
            }
        }

        if ("comment".equals(field)) {
            return StringUtil.trimToNull(this.getComments());
        }

        if ("objectQuotingStrategy".equals(field)) {
            if (this.getObjectQuotingStrategy() == null) {
                return null;
            }
            return this.getObjectQuotingStrategy().toString();
        }

        if ("preconditions".equals(field)) {
            if ((this.getPreconditions() != null) && !this.getPreconditions().getNestedPreconditions().isEmpty()) {
                return this.getPreconditions();
            } else {
                return null;
            }
        }

        if ("changes".equals(field)) {
            return getChanges();
        }

        if ("created".equals(field)) {
            return getCreated();
        }

        if ("rollback".equals(field)) {
            if ((rollback != null) && (rollback.getChanges() != null) && !rollback.getChanges().isEmpty()) {
                return rollback;
            } else {
                return null;
            }
        }

        throw new UnexpectedLiquibaseException("Unexpected field request on changeSet: "+field);
    }

    @Override
    public SerializationType getSerializableFieldType(String field) {
        if ("comment".equals(field) || "preconditions".equals(field) || "changes".equals(field) || "rollback".equals
            (field)) {
            return SerializationType.NESTED_OBJECT;
        } else {
            return SerializationType.NAMED_FIELD;
        }
    }

    @Override
    public String getSerializedObjectNamespace() {
        return STANDARD_CHANGELOG_NAMESPACE;
    }

    @Override
    public String getSerializableFieldNamespace(String field) {
        return getSerializedObjectNamespace();
    }

    @Override
    public boolean equals(Object obj) {
        if (!(obj instanceof ChangeSet)) {
            return false;
        }
        return this.toString(false).equals(((ChangeSet) obj).toString(false));
    }

    @Override
    public int hashCode() {
        return toString(false).hashCode();
    }

    public Object getAttribute(String attribute) {
        return attributes.get(attribute);
    }

    public ChangeSet setAttribute(String attribute, Object value) {
        this.attributes.put(attribute, value);

        return this;
    }
}


package liquibase.database;

import liquibase.Scope;
import liquibase.changelog.ChangeLogHistoryService;
import liquibase.changelog.ChangeLogHistoryServiceFactory;
import liquibase.changelog.OfflineChangeLogHistoryService;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.parser.SnapshotParser;
import liquibase.parser.SnapshotParserFactory;
import liquibase.resource.ResourceAccessor;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.EmptyDatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Catalog;
import liquibase.structure.core.Schema;
import liquibase.util.ObjectUtil;
import liquibase.util.StringUtil;

import java.io.File;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.HashMap;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class OfflineConnection implements DatabaseConnection {
    private final String url;
    private final String databaseShortName;
    private final Map<String, String> databaseParams = new HashMap<>();
    private DatabaseSnapshot snapshot;
    private OutputLiquibaseSql outputLiquibaseSql = OutputLiquibaseSql.NONE;
    private String changeLogFile = "databasechangelog.csv";
    private boolean caseSensitive;
    private String productName;
    private String productVersion;
    private int databaseMajorVersion = 999;
    private int databaseMinorVersion = 999;
    private String catalog;
    private boolean sendsStringParametersAsUnicode = true;
    private String connectionUserName;

    public OfflineConnection(String url, ResourceAccessor resourceAccessor) {
        this.url = url;
        Matcher matcher = Pattern.compile("offline:(\\w+)\\??(.*)").matcher(url);
        if (!matcher.matches()) {
            throw new UnexpectedLiquibaseException("Could not parse offline url " + url);
        }
        this.databaseShortName = matcher.group(1).toLowerCase();
        String params = StringUtil.trimToNull(matcher.group(2));
        try {
            Map<String, String> params1 = new HashMap<String, String>();
            if (params != null) {
                String[] keyValues = params.split("&");
                for (String param : keyValues) {
                    String[] split = param.split("=");
                    params1.put(URLDecoder.decode(split[0], "UTF-8"), URLDecoder.decode(split[1], "UTF-8"));
                }
            }

            this.productName = "Offline " + databaseShortName;
            for (Map.Entry<String, String> paramEntry : params1.entrySet()) {

                if ("version".equals(paramEntry.getKey())) {
                    this.productVersion = paramEntry.getValue();
                    String[] versionParts = productVersion.split("\\.");
                    try {
                        this.databaseMajorVersion = Integer.parseInt(versionParts[0]);
                        if (versionParts.length > 1) {
                            this.databaseMinorVersion = Integer.parseInt(versionParts[1]);
                        }
                    } catch (NumberFormatException e) {
                        Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Cannot parse database version " + productVersion);
                    }
                } else if ("productName".equals(paramEntry.getKey())) {
                    this.productName = paramEntry.getValue();
                } else if ("catalog".equals(paramEntry.getKey())) {
                    this.catalog = params1.get("catalog");
                } else if ("caseSensitive".equals(paramEntry.getKey())) {
                    this.caseSensitive = Boolean.parseBoolean(paramEntry.getValue());
                } else if ("changeLogFile".equals(paramEntry.getKey())) {
                    this.changeLogFile = paramEntry.getValue();
                } else if ("outputLiquibaseSql".equals(paramEntry.getKey())) {
                    this.outputLiquibaseSql = OutputLiquibaseSql.fromString(paramEntry.getValue());
                } else if ("snapshot".equals(paramEntry.getKey())) {
                    String snapshotFile = paramEntry.getValue();
                    try {
                        SnapshotParser parser = SnapshotParserFactory.getInstance()
                                .getParser(snapshotFile, resourceAccessor);
                        this.snapshot = parser.parse(snapshotFile, resourceAccessor);
                        this.productVersion = this.snapshot.getDatabase().getDatabaseProductVersion();
                        this.snapshot.getDatabase().setConnection(this);

                        for (Catalog catalog : this.snapshot.get(Catalog.class)) {
                            if (catalog.isDefault()) {
                                this.catalog = catalog.getName();
                            }
                        }
                    } catch (LiquibaseException e) {
                        throw new UnexpectedLiquibaseException("Cannot parse snapshot " + url, e);
                    }
                } else if ("sendsStringParametersAsUnicode".equals(paramEntry.getKey())) {
                    this.sendsStringParametersAsUnicode = Boolean.parseBoolean(paramEntry.getValue());
                } else {
                    this.databaseParams.put(paramEntry.getKey(), paramEntry.getValue());
                }
            }
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException(e);
        }
    }

    public boolean isCorrectDatabaseImplementation(Database database) {
        return database.getShortName().equalsIgnoreCase(databaseShortName);
    }

    @Override
    public void attached(Database database) {
        for (Map.Entry<String, String> param : this.databaseParams.entrySet()) {
            try {
                ObjectUtil.setProperty(database, param.getKey(), param.getValue());
            } catch (Exception e) {
                Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Error setting database parameter " + param.getKey() + ": " + e.getMessage(), e);
            }
        }
        if (database instanceof AbstractJdbcDatabase) {
            ((AbstractJdbcDatabase) database).setCaseSensitive(this.caseSensitive);
        }

        if (snapshot == null) {
            try {
                snapshot = new EmptyDatabaseSnapshot(database);
            } catch (DatabaseException | InvalidExampleException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }

        ChangeLogHistoryServiceFactory.getInstance().register(createChangeLogHistoryService(database));
    }

    protected ChangeLogHistoryService createChangeLogHistoryService(Database database) {
        return new OfflineChangeLogHistoryService(database, new File(changeLogFile),
                outputLiquibaseSql != OutputLiquibaseSql.NONE, // Output DML
                outputLiquibaseSql == OutputLiquibaseSql.ALL   // Output DDL
        );
    }

    /**
     * Returns a copy of the current simulated content  of the database, filtered by the given
     * array.
     *
     * @param examples the list of objects to clone
     * @return a new DatabaseSnapshot object containing all objects matching examples. If none are found,
     * an empty DatabaseSnapshot is returned.
     */
    public DatabaseSnapshot getSnapshot(DatabaseObject[] examples) {
        return this.snapshot.clone(examples);
    }

    /**
     * For debugging purposes: sets a DatabaseSnapshot object for this connection. Effectively,
     * this simulates the content of the database in this OfflineConnection.
     *
     * @param snapshot the snapshot with the simulated database content
     */
    public void setSnapshot(DatabaseSnapshot snapshot) {
        this.snapshot = snapshot;
        this.snapshot.getDatabase().setConnection(this);

        for (Catalog catalog : this.snapshot.get(Catalog.class)) {
            if (catalog.isDefault()) {
                this.catalog = catalog.getName();
            }
        }
    }

    @Override
    public void close() throws DatabaseException {
        //nothing
    }

    @Override
    public void commit() throws DatabaseException {
        //nothing
    }

    @Override
    public boolean getAutoCommit() throws DatabaseException {
        return false;
    }

    @Override
    public void setAutoCommit(boolean autoCommit) throws DatabaseException {

    }

    @Override
    public String getCatalog() throws DatabaseException {
        return catalog;
    }

    public String getSchema() {
        if (snapshot == null) {
            return null;
        }
        for (Schema schema : snapshot.get(Schema.class)) {
            if (schema.isDefault()) {
                return schema.getName();
            }
        }
        return null;
    }

    @Override
    public String nativeSQL(String sql) throws DatabaseException {
        return sql;
    }

    @Override
    public void rollback() throws DatabaseException {

    }

    @Override
    public String getDatabaseProductName() throws DatabaseException {
        return productName;
    }

    @Override
    public String getDatabaseProductVersion() throws DatabaseException {
        return productVersion;
    }

    @Override
    public int getDatabaseMajorVersion() throws DatabaseException {
        return databaseMajorVersion;
    }

    public void setDatabaseMajorVersion(int databaseMajorVersion) {
        this.databaseMajorVersion = databaseMajorVersion;
    }

    public void setProductVersion(String productVersion) {
        this.productVersion = productVersion;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    @Override
    public int getDatabaseMinorVersion() throws DatabaseException {
        return databaseMinorVersion;
    }

    public void setDatabaseMinorVersion(int databaseMinorVersion) {
        this.databaseMinorVersion = databaseMinorVersion;
    }

    @Override
    public String getURL() {
        return url;
    }

    @Override
    public String getConnectionUserName() {
        return connectionUserName;
    }

    public void setConnectionUserName(String connectionUserName) {
        this.connectionUserName = connectionUserName;
    }

    @Override
    public boolean isClosed() throws DatabaseException {
        return false;
    }

    public boolean getSendsStringParametersAsUnicode() {
        return sendsStringParametersAsUnicode;
    }

    public void setSendsStringParametersAsUnicode(boolean sendsStringParametersAsUnicode) {
        this.sendsStringParametersAsUnicode = sendsStringParametersAsUnicode;
    }

    public boolean isCaseSensitive() {
        return caseSensitive;
    }

    public void setCaseSensitive(boolean caseSensitive) {
        this.caseSensitive = caseSensitive;
    }

    /**
     * Output Liquibase SQL
     */
    private enum OutputLiquibaseSql {
        /**
         * Don't output anything
         */
        NONE,
        /**
         * Output only INSERT/UPDATE/DELETE
         */
        DATA_ONLY,
        /**
         * Output CREATE TABLE as well
         */
        ALL;

        public static OutputLiquibaseSql fromString(String s) {
            if (s == null) {
                return null;
            }
            s = s.toUpperCase();
            // For backward compatibility true is translated in ALL and false in NONE
            switch (s) {
                case "TRUE":
                    return ALL;
                case "FALSE":
                    return NONE;
                default:
                    return valueOf(s);
            }
        }
    }
}


package liquibase.sqlgenerator.core;

import liquibase.database.Database;
import liquibase.database.core.InformixDatabase;
import liquibase.datatype.DataTypeFactory;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.statement.core.InsertOrUpdateStatement;

import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.Map;

@SuppressWarnings("ALL")
public class InsertOrUpdateGeneratorInformix extends InsertOrUpdateGenerator {

  // Table Aliases for Merge references
  private static final String SOURCE_ALIAS = "src";
  private static final String DEST_ALIAS = "dst";

  @Override
  public boolean supports(InsertOrUpdateStatement statement, Database database) {
    return database instanceof InformixDatabase;
  }

  @Override
  protected String getRecordCheck(InsertOrUpdateStatement insertOrUpdateStatement, Database database, String whereClause) {
    StringBuilder sql = new StringBuilder();
    String[] pkFields = insertOrUpdateStatement.getPrimaryKey().split(",");
    String tableReference = database.escapeTableName(insertOrUpdateStatement.getCatalogName(), insertOrUpdateStatement.getSchemaName(), insertOrUpdateStatement.getTableName());

    sql.append("MERGE INTO ").append(tableReference).append(" AS ").append(DEST_ALIAS).append("\n");
    sql.append("USING (\n");
    sql.append(getSelect(insertOrUpdateStatement, database));
    sql.append(") AS ").append(SOURCE_ALIAS).append("\n");
    sql.append("ON ");

    // Merge and match upon the Primary key fields
    for (int i = 0; i < pkFields.length; i++) {
      sql.append(DEST_ALIAS).append(".").append(pkFields[i]).append(" = ").append(SOURCE_ALIAS).append(".").append(pkFields[i]);

      if (i < pkFields.length - 1) {
        sql.append(" AND ");
      }
    }
    sql.append("\nWHEN NOT MATCHED THEN\n");

    return sql.toString();
  }

  @Override
  protected String getInsertStatement(InsertOrUpdateStatement insertOrUpdateStatement, Database database,
                                      SqlGeneratorChain sqlGeneratorChain) {
    StringBuilder columns = new StringBuilder();
    StringBuilder values = new StringBuilder();

    for (String columnKey : insertOrUpdateStatement.getColumnValues().keySet()) {
      columns.append(", ");
      columns.append(DEST_ALIAS).append(".").append(columnKey);
      values.append(SOURCE_ALIAS).append(".").append(columnKey).append(", ");
    }
    columns.delete(0, 2);

    int lastComma = values.lastIndexOf(",");
    if (lastComma > -1) {
      values.delete(lastComma, lastComma + 2);
    }

    return "INSERT (" + columns.toString() + ") VALUES (" + values.toString() + ")\n";
  }

  @Override
  protected String getElse(Database database) {
    return "";
  }

  @Override
  protected String getUpdateStatement(InsertOrUpdateStatement insertOrUpdateStatement, Database database,
                                      String whereClause, SqlGeneratorChain sqlGeneratorChain) {

    Map<String, Object> columnValues = insertOrUpdateStatement.getColumnValues();
    String[] pkFields = insertOrUpdateStatement.getPrimaryKey().split(",");

    // Only generate the update statement if non key columns exist
    if (pkFields.length != columnValues.size()) {
      StringBuilder sql = new StringBuilder("WHEN MATCHED THEN\n");
      sql.append("UPDATE SET ");

      HashSet<String> hashPkFields = new HashSet<String>(Arrays.asList(pkFields));

      for (String columnKey : columnValues.keySet()) {
        // Do not include Primary Key fields within the update
        if (!hashPkFields.contains(columnKey)) {
          sql.append(DEST_ALIAS).append(".").append(columnKey).append(" = ");
          sql.append(SOURCE_ALIAS).append(".").append(columnKey);
          sql.append(", ");
        }
      }

      int lastComma = sql.lastIndexOf(",");
      if (lastComma > -1) {
        sql.delete(lastComma, lastComma + 2);
      }
      return sql.toString();
    }
    else {
      return "";
    }
  }

  // Copied and modified from liquibase.sqlgenerator.core.InsertOrUpdateGeneratorMySQL
  private String convertToString(Object newValue, Database database) {
    String sqlString;
    if (newValue == null || newValue.toString().equals("") || newValue.toString().equalsIgnoreCase("NULL")) {
      sqlString = "NULL::INTEGER";
    } else if (newValue instanceof String && !looksLikeFunctionCall(((String) newValue), database)) {
      sqlString = "'" + database.escapeStringForDatabase(newValue.toString()) + "'";
    } else if (newValue instanceof Date) {
      sqlString = database.getDateLiteral(((Date) newValue));
    } else if (newValue instanceof Boolean) {
      if (((Boolean) newValue)) {
        sqlString = DataTypeFactory.getInstance().getTrueBooleanValue(database);
      } else {
        sqlString = DataTypeFactory.getInstance().getFalseBooleanValue(database);
      }
    } else {
      sqlString = newValue.toString();
    }
    return sqlString;
  }

  private String getSelect(InsertOrUpdateStatement insertOrUpdateStatement, Database database) {
    StringBuilder select = new StringBuilder();
    select.append("\tSELECT ");

    for (String columnKey : insertOrUpdateStatement.getColumnValues().keySet()) {
      select.append(convertToString(insertOrUpdateStatement.getColumnValue(columnKey), database));
      select.append(" AS ");
      select.append(columnKey);
      select.append(", ");
    }

    int lastComma = select.lastIndexOf(", ");
    if (lastComma > -1) {
      select.delete(lastComma, lastComma + 2);
    }

    select.append("\n\tFROM sysmaster:informix.sysdual\n");

    return select.toString();
  }

}


package liquibase.change;

import liquibase.Scope;
import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.exception.*;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.plugin.AbstractPlugin;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.serializer.core.string.StringChangeLogSerializer;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.util.StringUtil;
import liquibase.util.beans.PropertyUtils;

import java.beans.IntrospectionException;
import java.beans.PropertyDescriptor;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
import java.util.*;

/**
 * Standard superclass to simplify {@link Change } implementations. You can implement Change directly, this class is
 * purely for convenience but recommended.
 *

 * By default, this base class relies on annotations such as {@link DatabaseChange} and {@link DatabaseChangeProperty}
 * and delegating logic to the {@link liquibase.sqlgenerator.SqlGenerator} objects created to do the actual change work.
 * Place the @DatabaseChangeProperty annotations on the read "get" methods to control property metadata.
 */
public abstract class AbstractChange extends AbstractPlugin implements Change {

    protected static final String NODENAME_COLUMN = "column";
    private ResourceAccessor resourceAccessor;

    private ChangeSet changeSet;

    public AbstractChange() {
    }

    /**
     * Default implementation is a no-op
     */
    @Override
    public void finishInitialization() throws SetupException {

    }

    /**
     * Generate the ChangeMetaData for this class. Default implementation reads from the @{@link DatabaseChange } annotation
     * and calls out to {@link #createChangeParameterMetadata(String)} for each property.
     *
     * @throws UnexpectedLiquibaseException if no @DatabaseChange annotation on this Change class
     */
    @Override
    public ChangeMetaData createChangeMetaData() {
        try {
            DatabaseChange databaseChange = this.getClass().getAnnotation(DatabaseChange.class);

            if (databaseChange == null) {
                throw new UnexpectedLiquibaseException("No @DatabaseChange annotation for " + getClass().getName());
            }

            Set<ChangeParameterMetaData> params = new HashSet<>();
            for (PropertyDescriptor property : PropertyUtils.getInstance().getDescriptors(getClass())) {
                if (isInvalidProperty(property)) {
                    continue;
                }
                Method readMethod = property.getReadMethod();
                Method writeMethod = property.getWriteMethod();
                if (readMethod == null) {
                    try {
                        readMethod = this.getClass().getMethod(
                            "is" + StringUtil.upperCaseFirst(property.getName())
                        );
                    } catch (NoSuchMethodException|SecurityException ignore) {
                        //it was worth a try
                    }
                }
                if ((readMethod != null) && (writeMethod != null)) {
                    DatabaseChangeProperty annotation = readMethod.getAnnotation(DatabaseChangeProperty.class);
                    if ((annotation == null) || annotation.isChangeProperty()) {
                        params.add(createChangeParameterMetadata(property.getDisplayName()));
                    }
                }

            }

            Map<String, String> notes = new HashMap<>();
            for (DatabaseChangeNote note : databaseChange.databaseNotes()) {
                notes.put(note.database(), note.notes());
            }

            return new ChangeMetaData(databaseChange.name(), databaseChange.description(), databaseChange.priority(),
                databaseChange.appliesTo(), notes, params);
        } catch (UnexpectedLiquibaseException|IntrospectionException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    protected boolean isInvalidProperty(PropertyDescriptor property) {
        return "metaClass".equals(property.getDisplayName());
    }

    /**
     * Called by {@link #createChangeMetaData()} to create metadata for a given parameter. It finds the method that corresponds to the parameter
     * and calls the corresponding create*MetaData methods such as {@link #createRequiredDatabasesMetaData(String, DatabaseChangeProperty)} to determine the
     * correct values for the ChangeParameterMetaData fields.
     *
     * @throws UnexpectedLiquibaseException if the passed parameter does not exist
     */
    protected ChangeParameterMetaData createChangeParameterMetadata(String parameterName) {

        try {
            String displayName = parameterName.replaceAll("([A-Z])", " $1");
            displayName = displayName.substring(0, 1).toUpperCase() + displayName.substring(1);

            PropertyDescriptor property = null;
            for (PropertyDescriptor prop : PropertyUtils.getInstance().getDescriptors(getClass())) {
                if (prop.getDisplayName().equals(parameterName)) {
                    property = prop;
                    break;
                }
            }
            if (property == null) {
                throw new UnexpectedLiquibaseException("Could not find property " + parameterName);
            }

            Method readMethod = property.getReadMethod();
            if (readMethod == null) {
                readMethod = getClass().getMethod("is" + StringUtil.upperCaseFirst(property.getName()));
            }
            Type type = readMethod.getGenericReturnType();

            DatabaseChangeProperty changePropertyAnnotation = readMethod.getAnnotation(DatabaseChangeProperty.class);

            String mustEqualExisting = createMustEqualExistingMetaData(parameterName, changePropertyAnnotation);
            String description = createDescriptionMetaData(parameterName, changePropertyAnnotation);
            Map<String, Object> examples = createExampleValueMetaData(parameterName, changePropertyAnnotation);
            String since = createSinceMetaData(parameterName, changePropertyAnnotation);
            SerializationType serializationType = createSerializationTypeMetaData(
                parameterName, changePropertyAnnotation
            );
            String[] requiredForDatabase = createRequiredDatabasesMetaData(parameterName, changePropertyAnnotation);
            String[] supportsDatabase = createSupportedDatabasesMetaData(parameterName, changePropertyAnnotation);

            return new ChangeParameterMetaData(this, parameterName, displayName, description, examples, since,
                type, requiredForDatabase, supportsDatabase, mustEqualExisting, serializationType);
        } catch (IntrospectionException|UnexpectedLiquibaseException|NoSuchMethodException|SecurityException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    /**
     * Create the {@link ChangeParameterMetaData} "since" value. Uses the value on the DatabaseChangeProperty
     * annotation or returns null as a default.
     */
    @SuppressWarnings("UnusedParameters")
    protected String createSinceMetaData(String parameterName, DatabaseChangeProperty changePropertyAnnotation) {
        if (changePropertyAnnotation == null) {
            return null;
        }
        return StringUtil.trimToNull(changePropertyAnnotation.since());
    }

    /**
     * Create the {@link ChangeParameterMetaData} "description" value. Uses the value on the DatabaseChangeProperty
     * annotation or returns null as a default.
     */
    @SuppressWarnings("UnusedParameters")
    protected String createDescriptionMetaData(String parameterName, DatabaseChangeProperty changePropertyAnnotation) {
        if (changePropertyAnnotation == null) {
            return null;
        }
        return StringUtil.trimToNull(changePropertyAnnotation.description());
    }

    /**
     * Create the {@link ChangeParameterMetaData} "serializationType" value. Uses the value on the
     * DatabaseChangeProperty annotation or returns
     * {@link liquibase.serializer.LiquibaseSerializable.SerializationType#NAMED_FIELD} as a default.
     */
    @SuppressWarnings("UnusedParameters")
    protected liquibase.serializer.LiquibaseSerializable.SerializationType createSerializationTypeMetaData(
        String parameterName, DatabaseChangeProperty changePropertyAnnotation
    ) {
        if (changePropertyAnnotation == null) {
            return SerializationType.NAMED_FIELD;
        }
        return changePropertyAnnotation.serializationType();
    }

    /**
     * Create the {@link ChangeParameterMetaData} "mustEqual" value. Uses the value on the DatabaseChangeProperty
     * annotation or returns null as a default.
     */
    @SuppressWarnings("UnusedParameters")
    protected String createMustEqualExistingMetaData(
        String parameterName, DatabaseChangeProperty changePropertyAnnotation
    ) {
        if (changePropertyAnnotation == null) {
            return null;
        }

        return changePropertyAnnotation.mustEqualExisting();
    }

    /**
     * Create the {@link ChangeParameterMetaData} "example" value. Uses the value on the DatabaseChangeProperty annotation or returns null as a default.
     * Returns map with key=database short name, value=example. Use short-name "all" as the fallback.
     */
    @SuppressWarnings("UnusedParameters")
    protected Map<String, Object> createExampleValueMetaData(
        String parameterName, DatabaseChangeProperty changePropertyAnnotation
    ) {
        if (changePropertyAnnotation == null) {
            return null;
        }

        Map<String, Object> examples = new HashMap<>();
        examples.put("all", StringUtil.trimToNull(changePropertyAnnotation.exampleValue()));

        return examples;
    }

    /**
     * Create the {@link ChangeParameterMetaData} "requiredDatabases" value.
     * Uses the value on the DatabaseChangeProperty annotation or returns an array containing the string "COMPUTE"
     * as a default. "COMPUTE" will cause ChangeParameterMetaData to attempt to determine the required databases based
     * on the generated Statements
     */
    @SuppressWarnings("UnusedParameters")
    protected String[] createRequiredDatabasesMetaData(
        String parameterName, DatabaseChangeProperty changePropertyAnnotation
    ) {
        if (changePropertyAnnotation == null) {
            return new String[]{ChangeParameterMetaData.COMPUTE};
        } else {
            return changePropertyAnnotation.requiredForDatabase();
        }
    }

    /**
     * Create the {@link ChangeParameterMetaData} "supportedDatabase" value.
     * Uses the value on the DatabaseChangeProperty annotation or returns an array containing the string "COMPUTE"
     * as a default. "COMPUTE" will cause ChangeParameterMetaData to attempt to determine the required databases
     * based on the generated Statements
     */
    @SuppressWarnings("UnusedParameters")
    protected String[] createSupportedDatabasesMetaData(
        String parameterName, DatabaseChangeProperty changePropertyAnnotation
    ) {
        if (changePropertyAnnotation == null) {
            return new String[]{ChangeParameterMetaData.COMPUTE};
        } else {
            return changePropertyAnnotation.supportsDatabase();
        }

    }

    /**
     * {@inheritDoc}
     */
    @Override
    @DatabaseChangeProperty(isChangeProperty = false)
    public ChangeSet getChangeSet() {
        return changeSet;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setChangeSet(ChangeSet changeSet) {
        this.changeSet = changeSet;
    }

    /**
     * Implementation delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGenerator#generateStatementsIsVolatile(Database) } method on the
     * {@link SqlStatement} objects returned by {@link #generateStatements }.
     * If zero or null SqlStatements are returned by generateStatements then this method returns false.
     */
    @Override
    public boolean generateStatementsVolatile(Database database) {
        SqlStatement[] statements = generateStatements(database);
        if (statements == null) {
            return false;
        }
        for (SqlStatement statement : statements) {
            if (SqlGeneratorFactory.getInstance().generateStatementsVolatile(statement, database)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Implementation delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGenerator#generateRollbackStatementsIsVolatile(Database) }
     * method on the {@link SqlStatement} objects returned by {@link #generateStatements }
     * If no or null SqlStatements are returned by generateRollbackStatements then this method returns false.
     */
    @Override
    public boolean generateRollbackStatementsVolatile(Database database) {
        if (generateStatementsVolatile(database)) {
            return true;
        }
        SqlStatement[] statements = generateStatements(database);
        if (statements == null) {
            return false;
        }
        for (SqlStatement statement : statements) {
            if (SqlGeneratorFactory.getInstance().generateRollbackStatementsVolatile(statement, database)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Implementation delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGenerator#supports(liquibase.statement.SqlStatement, liquibase.database.Database)}
     * method on the {@link SqlStatement} objects returned by {@link #generateStatements }.
     * If no or null SqlStatements are returned by generateStatements then this method returns true.
     * If {@link #generateStatementsVolatile(liquibase.database.Database)} returns true, we cannot call generateStatements and so assume true.
     */
    @Override
    public boolean supports(Database database) {
        if (generateStatementsVolatile(database)) {
            return true;
        }
        SqlStatement[] statements = generateStatements(database);
        if (statements == null) {
            return true;
        }
        for (SqlStatement statement : statements) {
            if (!SqlGeneratorFactory.getInstance().supports(statement, database)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Implementation delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGenerator#warn(liquibase.statement.SqlStatement, liquibase.database.Database,
     * liquibase.sqlgenerator.SqlGeneratorChain)} method on the {@link SqlStatement} objects returned by
     * {@link #generateStatements }.
     * If a generated statement is not supported for the given database, no warning will be added since that is a
     * validation error. If no or null SqlStatements are returned by generateStatements then this method returns no
     * warnings.
     */
    @Override
    public Warnings warn(Database database) {
        Warnings warnings = new Warnings();
        if (generateStatementsVolatile(database)) {
            return warnings;
        }

        SqlStatement[] statements = generateStatements(database);
        if (statements == null) {
            return warnings;
        }
        for (SqlStatement statement : statements) {
            if (SqlGeneratorFactory.getInstance().supports(statement, database)) {
                warnings.addAll(SqlGeneratorFactory.getInstance().warn(statement, database));
            } else if (statement.skipOnUnsupported()) {
                warnings.addWarning(
                    statement.getClass().getName() + " is not supported on " + database.getShortName() +
                        ", but " + Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName() +
                        " will still execute");
            }
        }

        return warnings;
    }

    /**
     * Implementation checks the ChangeParameterMetaData for declared required fields
     * and also delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGenerator#validate(liquibase.statement.SqlStatement,
     * liquibase.database.Database, liquibase.sqlgenerator.SqlGeneratorChain)}  method on the {@link SqlStatement}
     * objects returned by {@link #generateStatements }.
     * If no or null SqlStatements are returned by generateStatements then this method returns no errors.
     * If there are no parameters than this method returns no errors
     */
    @Override
    public ValidationErrors validate(Database database) {
        ValidationErrors changeValidationErrors = new ValidationErrors();

        // Record an error if a parameter is not set, but that parameter is required by database.
        for (ChangeParameterMetaData param :
            Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().values()) {
            if (param.isRequiredFor(database) && (param.getCurrentValue(this) == null)) {
                changeValidationErrors.addError(param.getParameterName() + " is required for " +
                    Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName() + " on " + database.getShortName());
            }
        }

        // We cannot proceed to the next validation if we have missing parameters
        if (changeValidationErrors.hasErrors()) {
            return changeValidationErrors;
        }

        // Record warnings if statements are unsupported on database
        if (!generateStatementsVolatile(database)) {
            String unsupportedWarning = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName()
                    + " is not supported on " + database.getShortName();
            boolean sawUnsupportedError = false;

            SqlStatement[] statements = generateStatements(database);
            if (statements != null) {
                for (SqlStatement statement : statements) {
                    boolean supported = SqlGeneratorFactory.getInstance().supports(statement, database);
                    if (!supported && !sawUnsupportedError) {
                        if (!statement.skipOnUnsupported()) {
                            changeValidationErrors.addError(unsupportedWarning);
                            sawUnsupportedError = true;
                        }
                    } else {
                        changeValidationErrors.addAll(SqlGeneratorFactory.getInstance().validate(statement, database));
                    }
                }
            }
        }

        return changeValidationErrors;
    }

    @Override
    public ChangeStatus checkStatus(Database database) {
        return new ChangeStatus().unknown("Not implemented");
    }

    /**
     * Implementation relies on value returned from {@link #createInverses()}.
     */
    @Override
    public SqlStatement[] generateRollbackStatements(Database database) throws RollbackImpossibleException {
        return generateRollbackStatementsFromInverse(database);
    }

    /**
     * Implementation returns true if {@link #createInverses()} returns a non-null value.
     */
    @Override
    public boolean supportsRollback(Database database) {
        return createInverses() != null;
    }

    /**
     * Implementation generates checksum by serializing the change with {@link StringChangeLogSerializer}
     */
    @Override
    public CheckSum generateCheckSum() {
        return CheckSum.compute(new StringChangeLogSerializer().serialize(this, false));
    }

    /*
     * Generates rollback statements from the inverse changes returned by createInverses().
     * Throws RollbackImpossibleException if the changes created by createInverses() is not supported for the
     * passed database.
     */
    private SqlStatement[] generateRollbackStatementsFromInverse(Database database) throws RollbackImpossibleException {
        Change[] inverses = createInverses();
        if (inverses == null) {
            throw new RollbackImpossibleException("No inverse to " + getClass().getName() + " created");
        }

        List<SqlStatement> statements = new ArrayList<>();

        try {
            for (Change inverse : inverses) {
                if (!inverse.supports(database)) {
                    throw new RollbackImpossibleException(
                        Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(inverse).getName() + " is not supported on " +
                            database.getShortName()
                    );
                }
                statements.addAll(Arrays.asList(inverse.generateStatements(database)));
            }
        } catch (LiquibaseException e) {
            throw new RollbackImpossibleException(e);
        }

        return statements.toArray(new SqlStatement[statements.size()]);
    }

    /**
     * Create inverse changes that can roll back this change. This method is intended
     * to be overriden by Change implementations that have a logical inverse operation. Default implementation
     * returns null.
     *

     * If {@link #generateRollbackStatements(liquibase.database.Database)} is overridden, this method may not be called.
     *
     * @return Return null if there is no corresponding inverse and therefore automatic rollback is not possible.
     * Return an empty array to have a no-op rollback.
     * @see #generateRollbackStatements #supportsRollback
     */
    protected Change[] createInverses() {
        return null;
    }

    /**
     * @inheritDoc
     */
    @DatabaseChangeProperty(isChangeProperty = false)
    public ResourceAccessor getResourceAccessor() {
        return resourceAccessor;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setResourceAccessor(ResourceAccessor resourceAccessor) {
        this.resourceAccessor = resourceAccessor;
    }

    /**
     * Implementation delegates logic to the
     * {@link liquibase.sqlgenerator.SqlGeneratorFactory#getAffectedDatabaseObjects(liquibase.statement.SqlStatement,
     * liquibase.database.Database)} method on the {@link SqlStatement} objects returned by {@link #generateStatements }
     * Returns empty set if change is not supported for the passed database
     */
    @Override
    public Set<DatabaseObject> getAffectedDatabaseObjects(Database database) {
        if (this.generateStatementsVolatile(database)) {
            return new HashSet<>();
        }
        Set<DatabaseObject> affectedObjects = new HashSet<>();
        SqlStatement[] statements = generateStatements(database);

        if (statements != null) {
            for (SqlStatement statement : statements) {
                affectedObjects.addAll(SqlGeneratorFactory.getInstance()
                    .getAffectedDatabaseObjects(statement, database));
            }
        }

        return affectedObjects;
    }

    /**
     * Returns the fields on this change that are serializable.
     */
    @Override
    public Set<String> getSerializableFields() {
        return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().keySet();
    }

    @Override
    public Object getSerializableFieldValue(String field) {
        ChangeParameterMetaData fieldMetaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this)
            .getParameters().get(field);
        if (fieldMetaData == null) {
            return null;
        }
        return fieldMetaData.getCurrentValue(this);
    }

    @Override
    public String getSerializedObjectName() {
        return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName();
    }

    @Override
    public SerializationType getSerializableFieldType(String field) {
        return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getParameters().get(field).getSerializationType();
    }

    @Override
    public String getSerializedObjectNamespace() {
        return GENERIC_CHANGELOG_EXTENSION_NAMESPACE;
    }

    @Override
    public String getSerializableFieldNamespace(String field) {
        return getSerializedObjectNamespace();
    }

    @Override
    public String toString() {
        return Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this).getName();
    }

    @Override
    public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
        this.setResourceAccessor(resourceAccessor);
        try {
            Collection<ChangeParameterMetaData> changeParameters = metaData.getParameters().values();

            for (ChangeParameterMetaData param : changeParameters) {
                if (Collection.class.isAssignableFrom(param.getDataTypeClass())) {
                    if (param.getDataTypeClassParameters().length == 1) {
                        Class collectionType = (Class) param.getDataTypeClassParameters()[0];
                        if (ColumnConfig.class.isAssignableFrom(collectionType)) {
                            List<ParsedNode> columnNodes = new ArrayList<>(
                                parsedNode.getChildren(null, param.getParameterName())
                            );
                            columnNodes.addAll(parsedNode.getChildren(null, NODENAME_COLUMN));

                            Object nodeValue = parsedNode.getValue();
                            if (nodeValue instanceof ParsedNode) {
                                columnNodes.add((ParsedNode) nodeValue);
                            } else if (nodeValue instanceof Collection) {
                                for (Object nodeValueChild : ((Collection) nodeValue)) {
                                    if (nodeValueChild instanceof ParsedNode) {
                                        columnNodes.add((ParsedNode) nodeValueChild);
                                    }
                                }
                            }

                            for (ParsedNode child : columnNodes) {
                                if (NODENAME_COLUMN.equals(child.getName()) || "columns".equals(child.getName())) {
                                    List<ParsedNode> columnChildren = child.getChildren(null, NODENAME_COLUMN);
                                    if ((columnChildren != null) && !columnChildren.isEmpty()) {
                                        for (ParsedNode columnChild : columnChildren) {
                                            ColumnConfig columnConfig = createEmptyColumnConfig(collectionType);
                                            columnConfig.load(columnChild, resourceAccessor);
                                            ((ChangeWithColumns) this).addColumn(columnConfig);
                                        }
                                    } else {
                                        ColumnConfig columnConfig = createEmptyColumnConfig(collectionType);
                                        columnConfig.load(child, resourceAccessor);
                                        ((ChangeWithColumns) this).addColumn(columnConfig);
                                    }
                                }
                            }
                        } else if (
                            (LiquibaseSerializable.class.isAssignableFrom(collectionType))
                            && (!collectionType.isInterface())
                            && (!Modifier.isAbstract(collectionType.getModifiers()))
                        ) {
                            String elementName = ((LiquibaseSerializable) collectionType.getConstructor().newInstance())
                                .getSerializedObjectName();
                            List<ParsedNode> nodes = new ArrayList<>(
                                 parsedNode.getChildren(null, param.getParameterName())
                            );
                            if (!elementName.equals(param.getParameterName())) {
                                nodes.addAll(parsedNode.getChildren(null, elementName));
                            }

                            Object nodeValue = parsedNode.getValue();
                            if (nodeValue instanceof ParsedNode) {
                                nodes.add((ParsedNode) nodeValue);
                            } else if (nodeValue instanceof Collection) {
                                for (Object nodeValueChild : ((Collection) nodeValue)) {
                                    if (nodeValueChild instanceof ParsedNode) {
                                        nodes.add((ParsedNode) nodeValueChild);
                                    }
                                }
                            }

                            for (ParsedNode node : nodes) {
                                if (node.getName().equals(elementName)
                                   || node.getName().equals(param.getParameterName())) {
                                    List<ParsedNode> childNodes = node.getChildren(null, elementName);
                                    if ((childNodes != null) && !childNodes.isEmpty()) {
                                        for (ParsedNode childNode : childNodes) {
                                            LiquibaseSerializable childObject =
                                                (LiquibaseSerializable)collectionType.getConstructor().newInstance();
                                            childObject.load(childNode, resourceAccessor);
                                            ((Collection) param.getCurrentValue(this)).add(childObject);
                                        }
                                    } else {
                                        LiquibaseSerializable childObject =
                                            (LiquibaseSerializable) collectionType.getConstructor().newInstance();
                                        childObject.load(node, resourceAccessor);
                                        ((Collection) param.getCurrentValue(this)).add(childObject);
                                    }
                               }
                            }
                        }
                    }
                } else if (LiquibaseSerializable.class.isAssignableFrom(param.getDataTypeClass())) {
                    if (!param.getDataTypeClass().isInterface()
                            && !Modifier.isAbstract(param.getDataTypeClass().getModifiers())) {

                        try {
                            ParsedNode child = parsedNode.getChild(null, param.getParameterName());
                            if (child != null) {
                                LiquibaseSerializable serializableChild =
                                    (LiquibaseSerializable) param.getDataTypeClass().getConstructor().newInstance();
                                serializableChild.load(child, resourceAccessor);
                                param.setValue(this, serializableChild);
                            }
                        } catch (ReflectiveOperationException e) {
                            throw new UnexpectedLiquibaseException(e);
                        }
                    }
                } else {
                    Object childValue = parsedNode.getChildValue(
                        null, param.getParameterName(), param.getDataTypeClass()
                    );
                    if ((childValue == null) && (param.getSerializationType() == SerializationType.DIRECT_VALUE)) {
                        childValue = parsedNode.getValue();
                    }
                    param.setValue(this, childValue);
                }
            }
        } catch (ReflectiveOperationException e) {
            throw new UnexpectedLiquibaseException(e);
        }
        customLoadLogic(parsedNode, resourceAccessor);
        try {
            this.finishInitialization();
        } catch (SetupException e) {
            throw new ParsedNodeException(e);
        }
    }

    protected ColumnConfig createEmptyColumnConfig(Class collectionType)
        throws ReflectiveOperationException {
        return (ColumnConfig) collectionType.getConstructor().newInstance();
    }

    protected void customLoadLogic(ParsedNode parsedNode, ResourceAccessor resourceAccessor)
        throws ParsedNodeException {

    }

    @Override
    public ParsedNode serialize() throws ParsedNodeException {
        ParsedNode node = new ParsedNode(null, getSerializedObjectName());
        ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
        for (ChangeParameterMetaData param : metaData.getSetParameters(this).values()) {
            Object currentValue = param.getCurrentValue(this);
            currentValue = serializeValue(currentValue);
            if (currentValue != null) {
                node.addChild(null, param.getParameterName(), currentValue);
            }
        }

        return node;
    }

    protected Object serializeValue(Object value) throws ParsedNodeException {
        if (value instanceof Collection) {
            List returnList = new ArrayList();
            for (Object obj : (Collection) value) {
                Object objValue = serializeValue(obj);
                if (objValue != null) {
                    returnList.add(objValue);
                }
            }
            if (((Collection) value).isEmpty()) {
                return null;
            } else {
                return returnList;
            }
        } else if (value instanceof LiquibaseSerializable) {
            return ((LiquibaseSerializable) value).serialize();
        } else {
            return value;
        }
    }

    @Override
    public String getDescription() {
        ChangeMetaData metaData = Scope.getCurrentScope().getSingleton(ChangeFactory.class).getChangeMetaData(this);
        String description = metaData.getName();

        SortedSet<String> names = new TreeSet<>();
        for (Map.Entry<String, ChangeParameterMetaData> entry : metaData.getParameters().entrySet()) {
            String lowerCaseKey = entry.getKey().toLowerCase();
            if (lowerCaseKey.endsWith("name")
                && !lowerCaseKey.contains("schema")
                && !lowerCaseKey.contains("catalog")) {
                Object currentValue = entry.getValue().getCurrentValue(this);
                if (currentValue != null) {
                    names.add(entry.getKey()+"="+ currentValue);
                }
            }
        }

        if (!names.isEmpty()) {
            description += " "+ StringUtil.join(names, ", ");
        }

        return description;
    }
}