package liquibase.util;

import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.*;

/**
 * Defines a list of clauses that can be manipulated and rearranged based on keys associated with each.
 * On {@link #toString()}, the clauses will be joined by the separator passed at construction time and prepended/postpended by the start/end strings set at construction time.
 *

 * Note: all keys and values passed in are trimmed.

 * NOTE: All keys are managed in a case INSENSITIVE manner

 * NOTE: Null or empty clauses and subclauses are skipped in {@link #toString()}
 */
public class StringClauses {

    private final String separator;
    private final String start;
    private final String end;
    private final Random random = new SecureRandom();
    private LinkedHashMap<String, Object> clauses = new LinkedHashMap<>();

    /**
     * Creates a new StringClause with no start or end strings and a space separator.
     */
    public StringClauses() {
        this(null);
    }

    /**
     * Creates a new StringClause with no start or end strings and the given separator.
     */
    public StringClauses(String separator) {
        this(null, separator, null);
    }

    /**
     * Creates a new StringClause with the given start, end and separator.
     */
    public StringClauses(String start, String separator, String end) {
        if (start == null) {
            start = "";
        }
        if (separator == null) {
            separator = " ";
        }
        if (end == null) {
            end = "";
        }

        this.start = start;
        this.end = end;
        this.separator = separator;
    }

    /**
     * Returns the key if it is not empty and/or not already used. If it is already used, it generates a random value for the key.
     */
    private String uniqueKey(String key) {
        boolean generateOne = false;

        key = StringUtil.trimToNull(key);
        if (key == null) {
            generateOne = true;
        } else if (clauses.containsKey(key.toLowerCase())) {
            generateOne = true;
        }

        while (generateOne) {
            key = StringUtil.leftPad(new BigInteger(50, random).toString(32), 6).replace(" ", "0").substring(0,6);
            generateOne = clauses.containsKey(key);
        }
        return key;
    }

    /**
     * Adds a new clause at the end of the list with the given key.
     */
    public StringClauses append(String key, String clause) {
        Validate.notNull(StringUtil.trimToNull(key), "key must be a non-null, non-empty value");

        key = StringUtil.trimToEmpty(key).toLowerCase();
        clause = StringUtil.trimToEmpty(clause);

        if (clauses.containsKey(key)) {
            throw new IllegalArgumentException("Cannot add clause with key '" + key + "' because it is already defined");
        }
        clauses.put(key, clause.trim());
        return this;
    }

    /**
     * Adds a new sub-clause at the end of the list with the given key.
     */
    public StringClauses append(String key, StringClauses subclauses) {
        Validate.notNull(StringUtil.trimToNull(key), "key must be a non-null, non-empty value");

        key = StringUtil.trimToEmpty(key).toLowerCase();

        if (clauses.containsKey(key)) {
            throw new IllegalArgumentException("Cannot add clause with key '" + key + "' because it is already defined");
        }

        clauses.put(key, subclauses);
        return this;
    }

    /**
     * Adds a new clause at the end of the list. The key is equal to the passed clause.
     * There is no corresponding append with just a StringClauses because the subclause may not be fully created when appended and so the key may be an unexpected value.
     */
    public StringClauses append(String clause) {
        return this.append(uniqueKey(clause), clause);
    }

    /**
     * Convenience method for {@link #append(String, StringClauses)} when using enums.
     */
    public StringClauses append(Enum name, StringClauses subclauses) {
        return this.append(name.name(), subclauses);
    }

    /**
     * Convenience method for {@link #append(String, String)} when using enums.
     */
    public StringClauses append(Enum name, String clause) {
        return this.append(name.name(), clause);
    }

    public StringClauses append(LiteralClause literal) {
        if (literal != null) {
            clauses.put(literal.getClass().getName().toLowerCase() + " #" + clauses.size(), literal);
        }
        return this;
    }

    public StringClauses prepend(LiteralClause literal) {
        if (literal != null) {
            LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
            newMap.put(uniqueKey(literal.getClass().getName().toLowerCase()), literal);
            newMap.putAll(this.clauses);
            this.clauses = newMap;
        }
        return this;
    }

    /**
     * Adds a clause with the given key to the beginning of the list.
     */
    public StringClauses prepend(String key, String clause) {
        return prependImpl(key, StringUtil.trimToEmpty(clause));
    }

    /**
     * Adds a sub-clause with the given key to the beginning of the list.
     */
    public StringClauses prepend(String key, StringClauses clause) {
        return prependImpl(key, clause);
    }

    /**
     * Convenience method for {@link #prepend(String, String)} that uses the clause as the key.
     */
    public StringClauses prepend(String clause) {
        if (StringUtil.trimToNull(clause) == null) {
            return prepend(new Whitespace(clause));
        }
        return prepend(uniqueKey(clause), clause);
    }

    /**
     * Convenience method for {@link #prepend(String, String)} when using enums
     */
    public StringClauses prepend(Enum key, String clause) {
        return prepend(key.name(), clause);
    }

    /**
     * Convenience method for {@link #prepend(String, liquibase.util.StringClauses)} when using enums
     */
    public StringClauses prepend(Enum key, StringClauses clause) {
        return prepend(key.name(), clause);
    }

    private StringClauses prependImpl(String key, Object clause) throws IllegalArgumentException {
        Validate.notNull(StringUtil.trimToNull(key), "key must be a non-null, non-empty value");

        key = StringUtil.trimToEmpty(key).toLowerCase();

        if (clauses.containsKey(key)) {
            throw new IllegalArgumentException("Cannot add clause with key '" + key + "' because it is already defined");
        }

        LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
        newMap.put(key, clause);
        newMap.putAll(this.clauses);
        this.clauses = newMap;

        return this;
    }

    /**
     * Removes the clause with the given key. Removes case insensitively. If key doesn't exist, operation is a no-op.
     */
    public StringClauses remove(String key) {
        clauses.remove(key.toLowerCase());
        return this;
    }

    /**
     * Convenience method for {@link #remove(String)} when using enums.
     */
    public StringClauses remove(Enum key) {
        return remove(key.name());
    }

    /**
     * Replaces the given key with a new string. If the existing key does not exist, throws IllegalArgumentException
     */
    public StringClauses replace(String key, String newValue) throws IllegalArgumentException {
        return replaceImpl(key, StringUtil.trimToEmpty(newValue));
    }

    /**
     * Replaces the given key with a new string. If the existing key does not exist, throws IllegalArgumentException
     */
    public StringClauses replaceIfExists(String key, String newValue) throws IllegalArgumentException {
        if (contains(key)) {
            return replaceImpl(key, StringUtil.trimToEmpty(newValue));
        } else {
            return this;
        }
    }

    public boolean contains(String key) {
        return clauses.containsKey(key.toLowerCase());
    }

    /**
     * Replaces the given key with a new sub-clause. If the existing key does not exist, throws IllegalArgumentException
     */
    public StringClauses replace(String key, StringClauses newValue) {
        return replaceImpl(key, newValue);
    }

    /**
     * Convenience method for {@link #replace(String, liquibase.util.StringClauses)} when using enums.
     */
    public StringClauses replace(Enum key, StringClauses newValue) {
        return replace(key.name(), newValue);
    }

    /**
     * Convenience method for {@link #replace(String, String)} when using enums.
     */
    public StringClauses replace(Enum key, String newValue) {
        return replace(key.name(), newValue);
    }

    private StringClauses replaceImpl(String key, Object newValue) {
        key = StringUtil.trimToEmpty(key).toLowerCase();
        if (!clauses.containsKey(key)) {
            throw new IllegalArgumentException("Key '" + key + "' is not defined");
        }
        LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : clauses.entrySet()) {
            if (entry.getKey().equals(key)) {
                newMap.put(key, newValue);
            } else {
                newMap.put(entry.getKey(), entry.getValue());
            }
        }
        this.clauses = newMap;

        return this;
    }

    /**
     * Inserts a new clause before the given key. Throws IllegalArgumentException if the existingKey does not exist.
     */
    public StringClauses insertBefore(String existingKey, String newKey, String newValue) throws IllegalArgumentException {
        return insertBeforeImpl(existingKey, newKey, newValue);
    }

    /**
     * Inserts a new sub-clause before the given key. Throws IllegalArgumentException if the existingKey does not exist.
     */
    public StringClauses insertBefore(String existingKey, String newKey, StringClauses newValue) {
        return insertBeforeImpl(existingKey, newKey, newValue);
    }

    /**
     * Convenience method for {@link #insertBefore(String, String, String)} where the new clause key is equal to the newValue.
     */
    public StringClauses insertBefore(String existingKey, String newValue) {
        return insertBefore(existingKey, newValue, StringUtil.trimToNull(newValue));
    }

    /**
     * Convenience method for {@link #insertBefore(String, String)} when using enums.
     */
    public StringClauses insertBefore(Enum existingKey, String newValue) {
        return insertBefore(existingKey.name(), newValue);
    }

    /**
     * Convenience method for {@link #insertBefore(String, String)} when using enums.
     */
    public StringClauses insertBefore(Enum existingKey, Enum newKey, String newValue) {
        return insertBefore(existingKey.name(), newKey.name(), newValue);
    }

    private StringClauses insertBeforeImpl(String existingKey, String newKey, Object newValue) {
        Validate.notNull(StringUtil.trimToNull(newKey), "key must be a non-null, non-empty value");

        existingKey = StringUtil.trimToEmpty(existingKey).toLowerCase();
        newKey = StringUtil.trimToEmpty(newKey).toLowerCase();

        if (!clauses.containsKey(existingKey)) {
            throw new IllegalArgumentException("Existing key '" + existingKey + "' does not exist");
        }
        if ("".equals(newKey)) {
            throw new IllegalArgumentException("New key cannot be null or empty");
        }

        if (clauses.containsKey(newKey)) {
            throw new IllegalArgumentException("Cannot add clause with key '" + newKey + "' because it is already defined");
        }

        LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : clauses.entrySet()) {
            if (entry.getKey().equals(existingKey)) {
                newMap.put(newKey, newValue);
            }
            newMap.put(entry.getKey(), entry.getValue());
        }
        this.clauses = newMap;

        return this;
    }

    /**
     * Inserts a new clause after the given key. Throws IllegalArgumentException if the existingKey does not exist.
     */
    public StringClauses insertAfter(String existingKey, String newKey, String newValue) {
        return insertAfterImpl(existingKey, newKey, newValue);
    }

    /**
     * Inserts a new sub-clause after the given key. Throws IllegalArgumentException if the existingKey does not exist.
     */
    public StringClauses insertAfter(String existingKey, String newKey, StringClauses newValue) {
        return insertAfterImpl(existingKey, newKey, newValue);
    }

    /**
     * Convenience method for {@link #insertAfter(String, String)} when using enums
     */
    public StringClauses insertAfter(Enum existingKey, String newValue) {
        return insertAfter(existingKey.name(), newValue);
    }

    /**
     * Convenience method for {@link #insertAfter(String, String, String)} using the newValue as the newKey.
     */
    public StringClauses insertAfter(String existingKey, String newValue) {
        return insertAfter(existingKey, newValue, StringUtil.trimToEmpty(newValue));
    }

    private StringClauses insertAfterImpl(String existingKey, String newKey, Object newValue) {
        Validate.notNull(StringUtil.trimToNull(existingKey), "key must be a non-null, non-empty value");

        existingKey = StringUtil.trimToEmpty(existingKey).toLowerCase();
        newKey = StringUtil.trimToEmpty(newKey).toLowerCase();

        if (!clauses.containsKey(existingKey)) {
            throw new IllegalArgumentException("Existing key '" + existingKey + "' does not exist");
        }
        if (clauses.containsKey(newKey)) {
            throw new IllegalArgumentException("Cannot add clause with key '" + newKey + "' because it is already defined");
        }

        LinkedHashMap<String, Object> newMap = new LinkedHashMap<>();
        for (Map.Entry<String, Object> entry : clauses.entrySet()) {
            newMap.put(entry.getKey(), entry.getValue());
            if (entry.getKey().equals(existingKey)) {
                newMap.put(newKey, newValue);
            }
        }
        this.clauses = newMap;

        return this;
    }

    /**
     * Retrieves the given key. Returns null if not set.
     * If clause at key is a StringClause, return the string version of it.
     * Will traverse sub-clauses to find the key.
     */
    public String get(String exitingKey) {
        exitingKey = StringUtil.trimToEmpty(exitingKey).toLowerCase();
        Object clauses = getImpl(exitingKey);
        if (clauses == null) {
            return null;
        }
        return clauses.toString();
    }

    private Object getImpl(String exitingKey) {
        Object o = this.clauses.get(exitingKey);
        if (o == null) {
            for (Object obj : this.clauses.values()) {
                if (obj instanceof StringClauses) {
                    Object childObj = ((StringClauses) obj).getImpl(exitingKey);
                    if (childObj != null) {
                        return childObj;
                    }
                }
            }
        }
        return o;
    }

    /**
     * Convenience method for {@link #get(String)} when using enums.
     */
    public String get(Enum exitingKey) {
        return get(exitingKey.name());
    }

    /**
     * Retrieves the given key. Returns null if not set. If clause at key is a String, return a new StringClause version of it. Will traverse sub-clauses to find the key.
     */
    public StringClauses getSubclause(String exitingKey) {
        exitingKey = StringUtil.trimToEmpty(exitingKey).toLowerCase();
        Object clauses = getImpl(exitingKey);
        if (clauses == null) {
            return null;
        }
        if (clauses instanceof String) {
            return new StringClauses().append((String) clauses);
        }
        return (StringClauses) clauses;
    }

    /**
     * Convenience method for {@link #getSubclause(String)} when using enums
     */
    public StringClauses getSubclause(Enum exitingKey) {
        return getSubclause(exitingKey.name());
    }

    public ClauseIterator getClauseIterator() {
        return new ClauseIterator(clauses);
    }

    @Override
    public String toString() {
        if (clauses.isEmpty()) {
            return "";
        }

        List finalList = new ArrayList<>(clauses.values());
        ListIterator iterator = finalList.listIterator();
        while (iterator.hasNext()) {
            Object next = iterator.next();
            if ((next == null) || "".equals(next.toString())) {
                iterator.remove();
            }
        }

        return start
                + StringUtil.join(finalList, separator, new StringUtil.ToStringFormatter())
                + end;
    }

    public Object[] toArray(boolean stringify) {
        Object[] returnArray = new Object[clauses.size()];
        ArrayList<Object> currentValues = new ArrayList<>(clauses.values());

        for (int i=0; i<currentValues.size(); i++) {
            if (stringify) {
                returnArray[i] = currentValues.get(i).toString();
            } else {
                returnArray[i] = currentValues.get(i);
            }
        }

        return returnArray;
    }

    public boolean isEmpty() {
        if (clauses.isEmpty()) {
            return true;
        }
        for (Object clause : clauses.values()) {
            if ((clause != null) && !"".equals(clause.toString().trim())) {
                return false;
            }
        }
        return true;
    }

    public interface LiteralClause {
        String toString();
    }

    public static class Delimiter implements LiteralClause{
        private String value;

        public Delimiter(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value;
        }
    }

    public static class Whitespace implements LiteralClause{
        private String value;

        public Whitespace(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value;
        }
    }

    public static class Comment implements LiteralClause {
        private String value;

        public Comment(String value) {
            this.value = value;
        }

        @Override
        public String toString() {
            return value;
        }
    }

    public static class ClauseIterator implements Iterator {

        private final LinkedHashMap<String, Object> clauses;
        private ListIterator<String> keyIterator;

        public ClauseIterator(LinkedHashMap<String, Object> clauses) {
            this.keyIterator = new ArrayList<String>(clauses.keySet()).listIterator();
            this.clauses = clauses;
        }

        @Override
        public boolean hasNext() {
            return keyIterator.hasNext();
        }

        @Override
        public Object next() {
            return clauses.get(keyIterator.next());
        }

        public Object nextNonWhitespace() {
            Object next;
            while (hasNext()) {
                next = clauses.get(keyIterator.next());
                if (!(next instanceof Whitespace) && !(next instanceof Comment)) {
                    return next;
                }
            }
            return null;
        }

        @Override
        public void remove() {
            clauses.remove(keyIterator.previous());
            keyIterator.remove();
        }

        public void replace(Object newClause) {
            keyIterator.previous();
            String keyToReplace = keyIterator.next();

            clauses.put(keyToReplace, newClause);
        }
    }
}


package liquibase.diff.output.changelog;

import liquibase.Scope;
import liquibase.change.Change;
import liquibase.changelog.ChangeSet;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.OfflineConnection;
import liquibase.database.core.*;
import liquibase.diff.DiffResult;
import liquibase.diff.ObjectDifferences;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.output.DiffOutputControl;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogType;
import liquibase.serializer.ChangeLogSerializer;
import liquibase.serializer.ChangeLogSerializerFactory;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.DatabaseObjectComparator;
import liquibase.structure.core.Column;
import liquibase.util.DependencyUtil;
import liquibase.util.StringUtil;

import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.text.SimpleDateFormat;
import java.util.*;

public class DiffToChangeLog {

    public static final String ORDER_ATTRIBUTE = "order";
    public static final String DATABASE_CHANGE_LOG_CLOSING_XML_TAG = "</databaseChangeLog>";

    private String idRoot = String.valueOf(new Date().getTime());
    private boolean overriddenIdRoot;

    private int changeNumber = 1;

    private String changeSetContext;
    private String changeSetAuthor;
    private String changeSetPath;
    private DiffResult diffResult;
    private DiffOutputControl diffOutputControl;
    private boolean tryDbaDependencies=true;

    private static Set<Class> loggedOrderFor = new HashSet<>();

    public DiffToChangeLog(DiffResult diffResult, DiffOutputControl diffOutputControl) {
        this.diffResult = diffResult;
        this.diffOutputControl = diffOutputControl;
    }

    public DiffToChangeLog(DiffOutputControl diffOutputControl) {
        this.diffOutputControl = diffOutputControl;
    }

    public void setDiffResult(DiffResult diffResult) {
        this.diffResult = diffResult;
    }

    public void setChangeSetContext(String changeSetContext) {
        this.changeSetContext = changeSetContext;
    }

    public void print(String changeLogFile) throws ParserConfigurationException, IOException, DatabaseException {
        this.changeSetPath = changeLogFile;
        ChangeLogSerializer changeLogSerializer = ChangeLogSerializerFactory.getInstance().getSerializer(changeLogFile);
        this.print(changeLogFile, changeLogSerializer);
    }

    public void print(PrintStream out) throws ParserConfigurationException, IOException, DatabaseException {
        this.print(out, ChangeLogSerializerFactory.getInstance().getSerializer("xml"));
    }

    public void print(String changeLogFile, ChangeLogSerializer changeLogSerializer) throws ParserConfigurationException, IOException, DatabaseException {
        this.changeSetPath = changeLogFile;
        File file = new File(changeLogFile);
        if (!file.exists()) {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, file + " does not exist, creating");
            FileOutputStream stream = new FileOutputStream(file);
            print(new PrintStream(stream, true, LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()), changeLogSerializer);
            stream.close();
        } else {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, file + " exists, appending");
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            print(new PrintStream(out, true, LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()), changeLogSerializer);

            String xml = new String(out.toByteArray(), LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding());
            String innerXml = xml.replaceFirst("(?ms).*<databaseChangeLog[^>]*>", "");

            innerXml = innerXml.replaceFirst(DATABASE_CHANGE_LOG_CLOSING_XML_TAG, "");
            innerXml = innerXml.trim();
            if ("".equals(innerXml)) {
                Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "No changes found, nothing to do");
                return;
            }

            try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "rw")) {

                String line;
                long offset = 0;
                boolean foundEndTag = false;
                while ((line = randomAccessFile.readLine()) != null) {
                    int index = line.indexOf(DATABASE_CHANGE_LOG_CLOSING_XML_TAG);
                    if (index >= 0) {
                        foundEndTag = true;
                        break;
                    } else {
                        offset = randomAccessFile.getFilePointer();
                    }
                }

                String lineSeparator = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration
                .class).getOutputLineSeparator();

                if (foundEndTag) {
                    randomAccessFile.seek(offset);
                    randomAccessFile.writeBytes("    ");
                    randomAccessFile.write(innerXml.getBytes(LiquibaseConfiguration.getInstance().getConfiguration
                    (GlobalConfiguration.class).getOutputEncoding()));
                    randomAccessFile.writeBytes(lineSeparator);
                    randomAccessFile.writeBytes(DATABASE_CHANGE_LOG_CLOSING_XML_TAG + lineSeparator);
                } else {
                    randomAccessFile.seek(0);
                    randomAccessFile.write(xml.getBytes(LiquibaseConfiguration.getInstance().getConfiguration
                    (GlobalConfiguration.class).getOutputEncoding()));
                }
                randomAccessFile.close();
            }

        }
    }

    /**
     * Prints changeLog that would bring the target database to be the same as
     * the reference database
     */
    public void print(PrintStream out, ChangeLogSerializer changeLogSerializer) throws ParserConfigurationException, IOException, DatabaseException {

        List<ChangeSet> changeSets = generateChangeSets();

        changeLogSerializer.write(changeSets, out);

        out.flush();
    }

    public List<ChangeSet> generateChangeSets() {
        final ChangeGeneratorFactory changeGeneratorFactory = ChangeGeneratorFactory.getInstance();
        DatabaseObjectComparator comparator = new DatabaseObjectComparator();

        String created = null;
        if (LiquibaseConfiguration.getInstance().getProperty(GlobalConfiguration.class, GlobalConfiguration.GENERATE_CHANGESET_CREATED_VALUES).getValue(Boolean.class)) {
            created = new SimpleDateFormat("yyyy-MM-dd HH:mmZ").format(new Date());
        }

        List<Class<? extends DatabaseObject>> types = getOrderedOutputTypes(ChangedObjectChangeGenerator.class);
        List<ChangeSet> updateChangeSets = new ArrayList<ChangeSet>();

        for (Class<? extends DatabaseObject> type : types) {
            ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();
            for (Map.Entry<? extends DatabaseObject, ObjectDifferences> entry : diffResult.getChangedObjects(type, comparator).entrySet()) {
                if (!diffResult.getReferenceSnapshot().getDatabase().isLiquibaseObject(entry.getKey()) && !diffResult.getReferenceSnapshot().getDatabase().isSystemObject(entry.getKey())) {
                    Change[] changes = changeGeneratorFactory.fixChanged(entry.getKey(), entry.getValue(), diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
                    addToChangeSets(changes, updateChangeSets, quotingStrategy, created);
                }
            }
        }

        types = getOrderedOutputTypes(MissingObjectChangeGenerator.class);
        List<DatabaseObject> missingObjects = new ArrayList<DatabaseObject>();
        for (Class<? extends DatabaseObject> type : types) {
            for (DatabaseObject object : diffResult.getMissingObjects(type, new DatabaseObjectComparator() {
                @Override
                public int compare(DatabaseObject o1, DatabaseObject o2) {
                    if ((o1 instanceof Column) && (o1.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null) &&
                        (o2.getAttribute(ORDER_ATTRIBUTE, Integer.class) != null)) {
                        int i = o1.getAttribute(ORDER_ATTRIBUTE, Integer.class).compareTo(o2.getAttribute(ORDER_ATTRIBUTE, Integer.class));
                        if (i != 0) {
                            return i;
                        }
                    }
                    return super.compare(o1, o2);

                }
            })) {
                if (object == null) {
                    continue;
                }
                if (!diffResult.getReferenceSnapshot().getDatabase().isLiquibaseObject(object) && !diffResult.getReferenceSnapshot().getDatabase().isSystemObject(object)) {
                    missingObjects.add(object);
                }
            }
        }

        List<ChangeSet> createChangeSets = new ArrayList<ChangeSet>();

        for (DatabaseObject object : sortMissingObjects(missingObjects, diffResult.getReferenceSnapshot().getDatabase())) {
            ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();

            Change[] changes = changeGeneratorFactory.fixMissing(object, diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
            addToChangeSets(changes, createChangeSets, quotingStrategy, created);
        }

        List<ChangeSet> deleteChangeSets = new ArrayList<ChangeSet>();

        types = getOrderedOutputTypes(UnexpectedObjectChangeGenerator.class);
        for (Class<? extends DatabaseObject> type : types) {
            ObjectQuotingStrategy quotingStrategy = diffOutputControl.getObjectQuotingStrategy();
            for (DatabaseObject object : sortUnexpectedObjects(diffResult.getUnexpectedObjects(type, comparator), diffResult.getReferenceSnapshot().getDatabase())) {
                if (!diffResult.getComparisonSnapshot().getDatabase().isLiquibaseObject(object) && !diffResult.getComparisonSnapshot().getDatabase().isSystemObject(object)) {
                    Change[] changes = changeGeneratorFactory.fixUnexpected(object, diffOutputControl, diffResult.getReferenceSnapshot().getDatabase(), diffResult.getComparisonSnapshot().getDatabase());
                    addToChangeSets(changes, deleteChangeSets, quotingStrategy, created);
                }
            }
        }

        List<ChangeSet> changeSets = new ArrayList<ChangeSet>();
        changeSets.addAll(createChangeSets);
        changeSets.addAll(deleteChangeSets);
        changeSets.addAll(updateChangeSets);
        return changeSets;
    }

    private List<DatabaseObject> sortUnexpectedObjects(Collection<? extends DatabaseObject> unexpectedObjects, Database database) {
        return sortObjects("unexpected", (Collection<DatabaseObject>) unexpectedObjects, database);
    }

    private List<DatabaseObject> sortMissingObjects(Collection<DatabaseObject> missingObjects, Database database) {
        return sortObjects("missing", (Collection<DatabaseObject>) missingObjects, database);
    }

    private List<DatabaseObject> sortObjects(final String type, Collection<DatabaseObject> objects, Database database) {

        if ((diffOutputControl.getSchemaComparisons() != null) && !objects.isEmpty() && supportsSortingObjects
            (database) && (database.getConnection() != null) && !(database.getConnection() instanceof OfflineConnection)) {
            List<String> schemas = new ArrayList<>();
            CompareControl.SchemaComparison[] schemaComparisons = this.diffOutputControl.getSchemaComparisons();
            if (schemaComparisons != null) {
                for (CompareControl.SchemaComparison comparison : schemaComparisons) {
                    String schemaName = comparison.getReferenceSchema().getSchemaName();
                    if (schemaName == null) {
                        schemaName = database.getDefaultSchemaName();
                    }
                    schemas.add(schemaName);
                }
            }

            if (schemas.isEmpty()) {
                schemas.add(database.getDefaultSchemaName());
            }

            try {
                final List<String> dependencyOrder = new ArrayList<>();
                DependencyUtil.NodeValueListener<String> nameListener = new DependencyUtil.NodeValueListener<String>() {
                    @Override
                    public void evaluating(String nodeValue) {
                        dependencyOrder.add(nodeValue);
                    }
                };

                DependencyUtil.DependencyGraph graph = new DependencyUtil.DependencyGraph(nameListener);
                addDependencies(graph, schemas, database);
                graph.computeDependencies();

                if (!dependencyOrder.isEmpty()) {

                    final List<DatabaseObject> toSort = new ArrayList<>();
                    final List<DatabaseObject> toNotSort = new ArrayList<>();

                    for (DatabaseObject obj : objects) {
                        if (!(obj instanceof Column)) {
                            String schemaName = null;
                            if (obj.getSchema() != null) {
                                schemaName = obj.getSchema().getName();
                            }

                            String name = schemaName + "." + obj.getName();
                            if (dependencyOrder.contains(name)) {
                                toSort.add(obj);
                            } else {
                                toNotSort.add(obj);
                            }
                        } else {
                            toNotSort.add(obj);
                        }
                    }

                    Collections.sort(toSort, new Comparator<DatabaseObject>() {
                        @Override
                        public int compare(DatabaseObject o1, DatabaseObject o2) {
                            String o1Schema = null;
                            if (o1.getSchema() != null) {
                                o1Schema = o1.getSchema().getName();
                            }

                            String o2Schema = null;
                            if (o2.getSchema() != null) {
                                o2Schema = o2.getSchema().getName();
                            }

                            Integer o1Order = dependencyOrder.indexOf(o1Schema + "." + o1.getName());
                            int o2Order = dependencyOrder.indexOf(o2Schema + "." + o2.getName());

                            int order = o1Order.compareTo(o2Order);
                            if ("unexpected".equals(type)) {
                                order = order * -1;
                            }
                            return order;
                        }
                    });

                    toSort.addAll(toNotSort);
                    return toSort;
                }
            } catch (DatabaseException e) {
                Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, "Cannot get object dependencies: " + e.getMessage());
            }
        }

        return new ArrayList<>(objects);
    }

    private List<Map<String, ?>> queryForDependencies(Executor executor, List<String> schemas)
        throws DatabaseException {
        List<Map<String, ?>> rs = null;
        try {
            if (tryDbaDependencies) {
                rs = executor.queryForList(new RawSqlStatement("select OWNER, NAME, REFERENCED_OWNER, REFERENCED_NAME from DBA_DEPENDENCIES where REFERENCED_OWNER != 'SYS' AND NOT(NAME LIKE 'BIN$%') AND NOT(OWNER = REFERENCED_OWNER AND NAME = REFERENCED_NAME) AND (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                            @Override
                            public String toString(String obj) {
                                return "OWNER='" + obj + "'";
                            }
                        }
                    ) + ")"));
            }
            else {
                rs = executor.queryForList(new RawSqlStatement("select NAME, REFERENCED_OWNER, REFERENCED_NAME from USER_DEPENDENCIES where REFERENCED_OWNER != 'SYS' AND NOT(NAME LIKE 'BIN$%') AND NOT(NAME = REFERENCED_NAME) AND (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                                @Override
                                public String toString(String obj) {
                                    return "REFERENCED_OWNER='" + obj + "'";
                                }
                            }
                ) + ")"));
            }
        }
        catch (DatabaseException dbe) {
            //
            // If our exception is for something other than a missing table/view
            // then we just re-throw the exception
            // else if we can't see USER_DEPENDENCIES then we also re-throw
            //   to stop the recursion
            //
            String message = dbe.getMessage();
            if (! message.contains("ORA-00942: table or view does not exist")) {
              throw new DatabaseException(dbe);
            }
            else if (! tryDbaDependencies) {
              throw new DatabaseException(dbe);
            }
            Scope.getCurrentScope().getLog(getClass()).warning("Unable to query DBA_DEPENDENCIES table. Switching to USER_DEPENDENCIES");
            tryDbaDependencies = false;
            return queryForDependencies(executor, schemas);
        }
        return rs;
    }

    /**
     * Used by {@link #sortMissingObjects(Collection, Database)} to determine whether to go into the sorting logic.
     */
    protected boolean supportsSortingObjects(Database database) {
        return (database instanceof AbstractDb2Database) || (database instanceof MSSQLDatabase) || (database instanceof
            OracleDatabase);
    }

    /**
     * Adds dependencies to the graph as schema.object_name.
     */
    protected void addDependencies(DependencyUtil.DependencyGraph<String> graph, List<String> schemas, Database database) throws DatabaseException {
        if (database instanceof DB2Database) {
            Executor executor = ExecutorService.getInstance().getExecutor(database);
            List<Map<String, ?>> rs = executor.queryForList(new RawSqlStatement("select TABSCHEMA, TABNAME, BSCHEMA, BNAME from syscat.tabdep where (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                        @Override
                        public String toString(String obj) {
                            return "TABSCHEMA='" + obj + "'";
                        }
                    }
            ) + ")"));
            for (Map<String, ?> row : rs) {
                String tabName = StringUtil.trimToNull((String) row.get("TABSCHEMA")) + "." + StringUtil.trimToNull((String) row.get("TABNAME"));
                String bName = StringUtil.trimToNull((String) row.get("BSCHEMA")) + "." + StringUtil.trimToNull((String) row.get("BNAME"));

                graph.add(bName, tabName);
            }
        } else if (database instanceof Db2zDatabase) {
            Executor executor = ExecutorService.getInstance().getExecutor(database);
            String db2ZosSql = "SELECT DSCHEMA AS TABSCHEMA, DNAME AS TABNAME, BSCHEMA, BNAME FROM SYSIBM.SYSDEPENDENCIES WHERE (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                        @Override
                        public String toString(String obj) {
                            return "DSCHEMA='" + obj + "'";
                        }
                    }
            ) + ")";
            List<Map<String, ?>> rs = executor.queryForList(new RawSqlStatement(db2ZosSql));
            for (Map<String, ?> row : rs) {
                String tabName = StringUtil.trimToNull((String) row.get("TABSCHEMA")) + "." + StringUtil.trimToNull((String) row.get("TABNAME"));
                String bName = StringUtil.trimToNull((String) row.get("BSCHEMA")) + "." + StringUtil.trimToNull((String) row.get("BNAME"));

                graph.add(bName, tabName);
            }
        } else if (database instanceof OracleDatabase) {
            Executor executor = ExecutorService.getInstance().getExecutor(database);
            List<Map<String, ?>> rs = queryForDependencies(executor, schemas);
            for (Map<String, ?> row : rs) {
                String tabName = null;
                if (tryDbaDependencies) {
                    tabName =
                        StringUtil.trimToNull((String) row.get("OWNER")) + "." +
                        StringUtil.trimToNull((String) row.get("NAME"));
                }
                else {
                    tabName =
                        StringUtil.trimToNull((String) row.get("REFERENCED_OWNER")) + "." +
                        StringUtil.trimToNull((String) row.get("NAME"));
                }
                String bName =
                    StringUtil.trimToNull((String) row.get("REFERENCED_OWNER")) + "." +
                    StringUtil.trimToNull((String) row.get("REFERENCED_NAME"));

                graph.add(bName, tabName);
            }
        } else if (database instanceof MSSQLDatabase) {
            Executor executor = ExecutorService.getInstance().getExecutor(database);
            String sql = "select object_schema_name(referencing_id) as referencing_schema_name, object_name(referencing_id) as referencing_name, object_name(referenced_id) as referenced_name, object_schema_name(referenced_id) as referenced_schema_name  from sys.sql_expression_dependencies depz where (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                        @Override
                        public String toString(String obj) {
                            return "object_schema_name(referenced_id)='" + obj + "'";
                        }
                    }
            ) + ")";
            sql += " UNION select object_schema_name(object_id) as referencing_schema_name, object_name(object_id) as referencing_name, object_name(parent_object_id) as referenced_name, object_schema_name(parent_object_id) as referenced_schema_name " +
                    "from sys.objects " +
                    "where parent_object_id > 0 " +
                    "and is_ms_shipped=0 " +
                    "and (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                        @Override
                        public String toString(String obj) {
                            return "object_schema_name(object_id)='" + obj + "'";
                        }
                    }
            ) + ")";

            sql += " UNION select object_schema_name(fk.object_id) as referencing_schema_name, fk.name as referencing_name, i.name as referenced_name, object_schema_name(i.object_id) as referenced_schema_name " +
                    "from sys.foreign_keys fk " +
                    "join sys.indexes i on fk.referenced_object_id=i.object_id and fk.key_index_id=i.index_id " +
                    "where fk.is_ms_shipped=0 " +
                    "and (" + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                        @Override
                        public String toString(String obj) {
                            return "object_schema_name(fk.object_id)='" + obj + "'";
                        }
                    }
            ) + ")";

            sql += " UNION select object_schema_name(i.object_id) as referencing_schema_name, object_name(i.object_id) as referencing_name, s.name as referenced_name, null as referenced_schema_name " +
                    "from sys.indexes i " +
                    "join sys.partition_schemes s on i.data_space_id = s.data_space_id";

            sql += " UNION select null as referencing_schema_name, s.name as referencing_name, f.name as referenced_name, null as referenced_schema_name from sys.partition_functions f " +
                    "join sys.partition_schemes s on s.function_id=f.function_id";

            sql += " UNION select null as referencing_schema_name, s.name as referencing_name, fg.name as referenced_name, null as referenced_schema_name from sys.partition_schemes s " +
                    "join sys.destination_data_spaces ds on s.data_space_id=ds.partition_scheme_id " +
                    "join sys.filegroups fg on ds.data_space_id=fg.data_space_id";

            //get data file -> filegroup dependencies
            sql += " UNION select distinct null as referencing_schema_name, f.name as referencing_name, ds.name as referenced_name, null as referenced_schema_name from sys.database_files f " +
                    "join sys.data_spaces ds on f.data_space_id=ds.data_space_id " +
                    "where f.data_space_id > 1";

            //get table -> filestream dependencies
            sql += " UNION select object_schema_name(t.object_id) as referencing_schema_name, t.name as referencing_name, ds.name as referenced_name, null as referenced_schema_name from sys.tables t " +
                    "join sys.data_spaces ds on t.filestream_data_space_id=ds.data_space_id " +
                    "where t.filestream_data_space_id > 1";

            //get table -> filestream dependencies
            sql += " UNION select object_schema_name(t.object_id) as referencing_schema_name, t.name as referencing_name, ds.name as referenced_name, null as referenced_schema_name from sys.tables t " +
                    "join sys.data_spaces ds on t.lob_data_space_id=ds.data_space_id " +
                    "where t.lob_data_space_id > 1";

            //get index -> filegroup dependencies
            sql += " UNION select object_schema_name(i.object_id) as referencing_schema_name, i.name as referencing_name, ds.name as referenced_name, null as referenced_schema_name from sys.indexes i " +
                    "join sys.data_spaces ds on i.data_space_id=ds.data_space_id " +
                    "where i.data_space_id > 1";

            //get index -> table dependencies
            sql += " UNION select object_schema_name(i.object_id) as referencing_schema_name, i.name as referencing_name, object_name(i.object_id) as referenced_name, object_schema_name(i.object_id) as referenced_schema_name from sys.indexes i " +
                    "where " + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                @Override
                public String toString(String obj) {
                    return "object_schema_name(i.object_id)='" + obj + "'";
                }
            });

            //get schema -> base object dependencies
            sql += " UNION SELECT SCHEMA_NAME(SCHEMA_ID) as referencing_schema_name, name as referencing_name, PARSENAME(BASE_OBJECT_NAME,1) AS referenced_name, (CASE WHEN PARSENAME(BASE_OBJECT_NAME,2) IS NULL THEN schema_name(schema_id) else PARSENAME(BASE_OBJECT_NAME,2) END) AS referenced_schema_name FROM SYS.SYNONYMS WHERE is_ms_shipped='false' AND " + StringUtil.join(schemas, " OR ", new StringUtil.StringUtilFormatter<String>() {
                @Override
                public String toString(String obj) {
                    return "SCHEMA_NAME(SCHEMA_ID)='" + obj + "'";
                }
            });

            //get non-clustered indexes -> unique clustered indexes on views dependencies
            sql += " UNION select object_schema_name(c.object_id) as referencing_schema_name, c.name as referencing_name, object_schema_name(nc.object_id) as referenced_schema_name, nc.name as referenced_name from sys.indexes c join sys.indexes nc on c.object_id=nc.object_id JOIN sys.objects o ON c.object_id = o.object_id where  c.index_id != nc.index_id and c.type_desc='CLUSTERED' and c.is_unique='true' and (not(nc.type_desc='CLUSTERED') OR nc.is_unique='false') AND o.type_desc='VIEW' AND o.name='AR_DETAIL_OPEN'";

            List<Map<String, ?>> rs = executor.queryForList(new RawSqlStatement(sql));
            if (!rs.isEmpty()) {
                for (Map<String, ?> row : rs) {
                    String bName = StringUtil.trimToNull((String) row.get("REFERENCED_SCHEMA_NAME")) + "." + StringUtil.trimToNull((String) row.get("REFERENCED_NAME"));
                    String tabName = StringUtil.trimToNull((String) row.get("REFERENCING_SCHEMA_NAME")) + "." + StringUtil.trimToNull((String) row.get("REFERENCING_NAME"));

                    if (!bName.equals(tabName)) {
                        graph.add(bName, tabName);
                    }
                }
            }
        }
    }

    protected List<Class<? extends DatabaseObject>> getOrderedOutputTypes(Class<? extends ChangeGenerator> generatorType) {

        Database comparisonDatabase = diffResult.getComparisonSnapshot().getDatabase();
        DependencyGraph graph = new DependencyGraph();
        for (Class<? extends DatabaseObject> type : diffResult.getReferenceSnapshot().getSnapshotControl().getTypesToInclude()) {
            graph.addType(type);
        }
        List<Class<? extends DatabaseObject>> types = graph.sort(comparisonDatabase, generatorType);

        if (!loggedOrderFor.contains(generatorType)) {
            String log = generatorType.getSimpleName() + " type order: ";
            for (Class<? extends DatabaseObject> type : types) {
                log += "    " + type.getName();
            }
            Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, log);
            loggedOrderFor.add(generatorType);
        }

        return types;
    }

    private void addToChangeSets(Change[] changes, List<ChangeSet> changeSets, ObjectQuotingStrategy quotingStrategy, String created) {
        if (changes != null) {
            String csContext = this.changeSetContext;

            if (diffOutputControl.getContext() != null) {
                csContext = diffOutputControl.getContext().toString().replaceFirst("^\\(", "")
                .replaceFirst("\\)$", "");
            }
            ChangeSet changeSet = new ChangeSet(generateId(changes), getChangeSetAuthor(), false, false, this.changeSetPath, csContext,
                    null, false, quotingStrategy, null);
            changeSet.setCreated(created);
            if (diffOutputControl.getLabels() != null) {
                changeSet.setLabels(diffOutputControl.getLabels());
            }
            for (Change change : changes) {
                changeSet.addChange(change);
            }
            changeSets.add(changeSet);
        }
    }

    protected String getChangeSetAuthor() {
        if (changeSetAuthor != null) {
            return changeSetAuthor;
        }
        String author = System.getProperty("user.name");
        if (StringUtil.trimToNull(author) == null) {
            return "diff-generated";
        } else {
            return author + " (generated)";
        }
    }

    public void setChangeSetAuthor(String changeSetAuthor) {
        this.changeSetAuthor = changeSetAuthor;
    }

    public String getChangeSetPath() {
        return changeSetPath;
    }

    public void setChangeSetPath(String changeSetPath) {
        this.changeSetPath = changeSetPath;
    }

    public void setIdRoot(String idRoot) {
        this.idRoot = idRoot;
        this.overriddenIdRoot = true;
    }

    protected String generateId(Change[] changes) {
        String desc = "";

        if (LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getGeneratedChangeSetIdsContainDescription()) {
            if (!overriddenIdRoot) { //switch timestamp to a shorter string (last 4 digits in base 36 format). Still mostly unique, but shorter since we also now have mostly-unique descriptions of the changes
                this.idRoot = Long.toString(Long.decode(idRoot), 36);
                idRoot = idRoot.substring(idRoot.length() - 4);
                this.overriddenIdRoot = true;
            }

             if ((changes != null) && (changes.length > 0)) {
                 desc = " ("+ StringUtil.join(changes, " :: ", new StringUtil.StringUtilFormatter<Change>() {
                     @Override
                     public String toString(Change obj) {
                         return obj.getDescription();
                     }
                 })+")";
             }

            if (desc.length() > 150) {
                desc = desc.substring(0, 146) + "...)";
            }
        }

        return idRoot + "-" + changeNumber++ + desc;
    }

    private static class DependencyGraph {

        private Map<Class<? extends DatabaseObject>, Node> allNodes = new HashMap<>();

        private void addType(Class<? extends DatabaseObject> type) {
            allNodes.put(type, new Node(type));
        }

        public List<Class<? extends DatabaseObject>> sort(Database database, Class<? extends ChangeGenerator> generatorType) {
            ChangeGeneratorFactory changeGeneratorFactory = ChangeGeneratorFactory.getInstance();
            for (Class<? extends DatabaseObject> type : allNodes.keySet()) {
                for (Class<? extends DatabaseObject> afterType : changeGeneratorFactory.runBeforeTypes(type, database, generatorType)) {
                    getNode(type).addEdge(getNode(afterType));
                }

                for (Class<? extends DatabaseObject> beforeType : changeGeneratorFactory.runAfterTypes(type, database, generatorType)) {
                    getNode(beforeType).addEdge(getNode(type));
                }
            }

            ArrayList<Node> returnNodes = new ArrayList<>();

            SortedSet<Node> nodesWithNoIncomingEdges = new TreeSet<>(new Comparator<Node>() {
                @Override
                public int compare(Node o1, Node o2) {
                    return o1.type.getName().compareTo(o2.type.getName());
                }
            });
            for (Node n : allNodes.values()) {
                if (n.inEdges.isEmpty()) {
                    nodesWithNoIncomingEdges.add(n);
                }
            }

            while (!nodesWithNoIncomingEdges.isEmpty()) {
                Node node = nodesWithNoIncomingEdges.iterator().next();
                nodesWithNoIncomingEdges.remove(node);

                returnNodes.add(node);

                for (Iterator<Edge> it = node.outEdges.iterator(); it.hasNext(); ) {
                    //remove edge e from the graph
                    Edge edge = it.next();
                    Node nodePointedTo = edge.to;
                    it.remove();//Remove edge from node
                    nodePointedTo.inEdges.remove(edge);//Remove edge from nodePointedTo

                    //if nodePointedTo has no other incoming edges then insert nodePointedTo into nodesWithNoIncomingEdges
                    if (nodePointedTo.inEdges.isEmpty()) {
                        nodesWithNoIncomingEdges.add(nodePointedTo);
                    }
                }
            }
            checkForCycleInDependencies(generatorType);

            List<Class<? extends DatabaseObject>> returnList = new ArrayList<>();
            for (Node node : returnNodes) {
                returnList.add(node.type);
            }
            return returnList;
        }

        private void checkForCycleInDependencies(Class<? extends ChangeGenerator> generatorType) {
            //Check to see if all edges are removed
            for (Node n : allNodes.values()) {
                if (!n.inEdges.isEmpty()) {
                    String message = "Could not resolve " + generatorType.getSimpleName() + " dependencies due " +
                     "to dependency cycle. Dependencies: \n";

                    for (Node node : allNodes.values()) {
                        SortedSet<String> fromTypes = new TreeSet<>();
                        SortedSet<String> toTypes = new TreeSet<>();
                        for (Edge edge : node.inEdges) {
                            fromTypes.add(edge.from.type.getSimpleName());
                        }
                        for (Edge edge : node.outEdges) {
                            toTypes.add(edge.to.type.getSimpleName());
                        }
                        String from = StringUtil.join(fromTypes, ",");
                        String to = StringUtil.join(toTypes, ",");
                        message += "    [" + from + "] -> " + node.type.getSimpleName() + " -> [" + to + "]\n";
                    }

                    throw new UnexpectedLiquibaseException(message);
                }
            }
        }

        private Node getNode(Class<? extends DatabaseObject> type) {
            Node node = allNodes.get(type);
            if (node == null) {
                node = new Node(type);
            }
            return node;
        }

        static class Node {
            public final Class<? extends DatabaseObject> type;
            public final HashSet<Edge> inEdges;
            public final HashSet<Edge> outEdges;

            public Node(Class<? extends DatabaseObject> type) {
                this.type = type;
                inEdges = new HashSet<>();
                outEdges = new HashSet<>();
            }

            public Node addEdge(Node node) {
                Edge e = new Edge(this, node);
                outEdges.add(e);
                node.inEdges.add(e);
                return this;
            }

            @Override
            public String toString() {
                return type.getName();
            }
        }

        static class Edge {
            public final Node from;
            public final Node to;

            public Edge(Node from, Node to) {
                this.from = from;
                this.to = to;
            }

            @Override
            public boolean equals(Object obj) {
                if (obj == null) {
                    return false;
                }
                if (!(obj instanceof Edge)) {
                    return false;
                }
                Edge e = (Edge) obj;
                return (e.from == from) && (e.to == to);
            }

            @Override
            public int hashCode() {
                return (this.from.toString() + "." + this.to.toString()).hashCode();
            }
        }
    }
}


package liquibase.database;

import liquibase.Scope;
import liquibase.database.core.UnsupportedDatabase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.resource.ResourceAccessor;
import liquibase.util.StringUtil;

import java.io.File;
import java.io.FileInputStream;
import java.sql.Connection;
import java.sql.Driver;
import java.util.*;

public class DatabaseFactory {
    private static final Logger LOG = Scope.getCurrentScope().getLog(DatabaseFactory.class);
    private static DatabaseFactory instance;
    private Map<String, SortedSet<Database>> implementedDatabases = new HashMap<>();
    private Map<String, SortedSet<Database>> internalDatabases = new HashMap<>();

    private DatabaseFactory() {
        try {
            for (Database database : Scope.getCurrentScope().getServiceLocator().findInstances(Database.class)) {
                register(database);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

    }

    public static synchronized DatabaseFactory getInstance() {
        if (instance == null) {
            instance = new DatabaseFactory();
        }
        return instance;
    }

    /**
     * Set singleton instance. Primarily used in testing
     */
    public static synchronized void setInstance(DatabaseFactory databaseFactory) {
        instance = databaseFactory;
    }

    public static synchronized void reset() {
        instance = new DatabaseFactory();
    }

    /**
     * Returns instances of all implemented database types.
     */
    public List<Database> getImplementedDatabases() {
        List<Database> returnList = new ArrayList<>();
        for (SortedSet<Database> set : implementedDatabases.values()) {
            returnList.add(set.iterator().next());
        }
        return returnList;
    }

    /**
     * Returns instances of all "internal" database types.
     */
    public List<Database> getInternalDatabases() {
        List<Database> returnList = new ArrayList<>();
        for (SortedSet<Database> set : internalDatabases.values()) {
            returnList.add(set.iterator().next());
        }
        return returnList;
    }

    public void register(Database database) {
        Map<String, SortedSet<Database>> map = null;
        if (database instanceof InternalDatabase) {
            map = internalDatabases;
        } else {
            map = implementedDatabases;

        }

        if (!map.containsKey(database.getShortName())) {
            map.put(database.getShortName(), new TreeSet<>(new TreeSet<>(new DatabaseComparator())));
        }
        map.get(database.getShortName()).add(database);
    }

    public Database findCorrectDatabaseImplementation(DatabaseConnection connection) throws DatabaseException {

        SortedSet<Database> foundDatabases = new TreeSet<>(new DatabaseComparator());

        for (Database implementedDatabase : getImplementedDatabases()) {
            if (connection instanceof OfflineConnection) {
                if (((OfflineConnection) connection).isCorrectDatabaseImplementation(implementedDatabase)) {
                    foundDatabases.add(implementedDatabase);
                }
            } else {
                if (implementedDatabase.isCorrectDatabaseImplementation(connection)) {
                    foundDatabases.add(implementedDatabase);
                }
            }
        }

        if (foundDatabases.isEmpty()) {
            LOG.warning(LogType.LOG, "Unknown database: " + connection.getDatabaseProductName());
            UnsupportedDatabase unsupportedDB = new UnsupportedDatabase();
            unsupportedDB.setConnection(connection);
            return unsupportedDB;
        }

        Database returnDatabase;
        try {
            returnDatabase = foundDatabases.iterator().next().getClass().getConstructor().newInstance();
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }

        returnDatabase.setConnection(connection);
        return returnDatabase;
    }

    public Database openDatabase(String url,
                            String username,
                            String password,
                            String propertyProviderClass,
                            ResourceAccessor resourceAccessor) throws DatabaseException {
        return openDatabase(url, username, password, null, null, null, propertyProviderClass, resourceAccessor);
    }

    public Database openDatabase(String url,
                            String username,
                            String password,
                            String driver,
                            String databaseClass,
                            String driverPropertiesFile,
                            String propertyProviderClass,
                            ResourceAccessor resourceAccessor) throws DatabaseException {
        return this.findCorrectDatabaseImplementation(openConnection(url, username, password, driver, databaseClass, driverPropertiesFile, propertyProviderClass, resourceAccessor));
    }

    public DatabaseConnection openConnection(String url,
                                             String username,
                                             String password,
                                             String propertyProvider,
                                             ResourceAccessor resourceAccessor) throws DatabaseException {

        return openConnection(url, username, password, null, null, null, propertyProvider, resourceAccessor);
    }

    public DatabaseConnection openConnection(String url,
                                             String username,
                                             String password,
                                             String driver,
                                             String databaseClass,
                                             String driverPropertiesFile,
                                             String propertyProviderClass,
                                             ResourceAccessor resourceAccessor) throws DatabaseException {

        if (url.startsWith("offline:")) {
            OfflineConnection offlineConnection = new OfflineConnection(url, resourceAccessor);
            offlineConnection.setConnectionUserName(username);
            return offlineConnection;
        }

        driver = StringUtil.trimToNull(driver);
        if (driver == null) {
            driver = DatabaseFactory.getInstance().findDefaultDriver(url);
        }

        try {
            Driver driverObject;
            DatabaseFactory databaseFactory = DatabaseFactory.getInstance();
            if (databaseClass != null) {
                databaseFactory.clearRegistry();
                databaseFactory.register((Database) Class.forName(databaseClass, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance());
            }

            try {
                if (driver == null) {
                    driver = databaseFactory.findDefaultDriver(url);
                }

                if (driver == null) {
                    throw new RuntimeException("Driver class was not specified and could not be determined from the url (" + url + ")");
                }

                driverObject = (Driver) Class.forName(driver, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance();
            } catch (Exception e) {
                throw new RuntimeException("Cannot find database driver: " + e.getMessage());
            }

            if (driverObject instanceof LiquibaseExtDriver) {
                ((LiquibaseExtDriver)driverObject).setResourceAccessor(resourceAccessor);
            }

            Properties driverProperties;
            if (propertyProviderClass == null) {
                driverProperties = new Properties();
            } else {
                driverProperties = (Properties) Class.forName(propertyProviderClass, true, Scope.getCurrentScope().getClassLoader()).getConstructor().newInstance();
            }

            if (username != null) {
                driverProperties.put("user", username);
            }
            if (password != null) {
                driverProperties.put("password", password);
            }
            if (null != driverPropertiesFile) {
                File propertiesFile = new File(driverPropertiesFile);
                if (propertiesFile.exists()) {
                    LOG.fine(
                            LogType.LOG, "Loading properties from the file:'" + driverPropertiesFile + "'"
                    );
                    FileInputStream inputStream = new FileInputStream(propertiesFile);
                    try {
                        driverProperties.load(inputStream);
                    } finally {
                        inputStream.close();
                    }
                } else {
                    throw new RuntimeException("Can't open JDBC Driver specific properties from the file: '"
                            + driverPropertiesFile + "'");
                }
            }

            LOG.fine(LogType.LOG, "Properties:");
            for (Map.Entry entry : driverProperties.entrySet()) {
                LOG.fine(LogType.LOG, "Key:'" + entry.getKey().toString() + "' Value:'" + entry.getValue().toString() + "'");
            }

            LOG.fine(LogType.LOG, "Connecting to the URL:'" + url + "' using driver:'" + driverObject.getClass().getName() + "'");
            Connection connection = driverObject.connect(url, driverProperties);
            LOG.fine(LogType.LOG, "Connection has been created");
            if (connection == null) {
                throw new DatabaseException("Connection could not be created to " + url + " with driver " + driverObject.getClass().getName() + ".  Possibly the wrong driver for the given database URL");
            }

            return new JdbcConnection(connection);
        } catch (Exception e) {
            throw new DatabaseException(e);
        }
    }

    /**
     * Returns the Java class name of the JDBC driver class (e.g. "org.mariadb.jdbc.Driver")
     * for the specified JDBC URL, if any Database class supports that URL.
     *
     * @param url the JDBC URL to analyse
     * @return a Database object supporting the URL. May also return null if the JDBC URL is unknown to all handlers.
     */
    public String findDefaultDriver(String url) {
        for (Database database : this.getImplementedDatabases()) {
            String defaultDriver = database.getDefaultDriver(url);
            if (defaultDriver != null) {
                return defaultDriver;
            }
        }

        return null;
    }

    /**
     * Removes all registered databases, even built in ones.  Useful for forcing a particular database implementation
     */
    public void clearRegistry() {
        implementedDatabases.clear();
    }

    public Database getDatabase(String shortName) {
        if (!implementedDatabases.containsKey(shortName)) {
            return null;
        }
        return implementedDatabases.get(shortName).iterator().next();

    }

    private static class DatabaseComparator implements Comparator<Database> {
        @Override
        public int compare(Database o1, Database o2) {
            return -1 * Integer.valueOf(o1.getPriority()).compareTo(o2.getPriority());
        }
    }
}


package liquibase.datatype;

import liquibase.change.core.LoadDataChange;
import liquibase.database.Database;
import liquibase.database.core.MSSQLDatabase;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.servicelocator.PrioritizedService;
import liquibase.statement.DatabaseFunction;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;

/**
 * Object representing a data type, instead of a plain string. It will be returned by
 * the getXXXType in the Database interface.
 */
public abstract class LiquibaseDataType implements PrioritizedService {

    private String name;
    private String[] aliases;
    private int priority;
    private int minParameters;
    private int maxParameters;

    private List<Object> parameters = new ArrayList<>();
    private String additionalInformation;
    private String rawDefinition;

    protected LiquibaseDataType(LiquibaseDataType originalType) {
    	name = originalType.name;
    	this.minParameters = originalType.minParameters;
        this.maxParameters = originalType.maxParameters;
        this.aliases = originalType.aliases;
        this.priority = originalType.priority;
    }

    public LiquibaseDataType() {
        DataTypeInfo dataTypeAnnotation = this.getClass().getAnnotation(DataTypeInfo.class);
        this.name = dataTypeAnnotation.name();
        this.minParameters = dataTypeAnnotation.minParameters();
        this.maxParameters = dataTypeAnnotation.maxParameters();
        this.aliases = dataTypeAnnotation.aliases();
        this.priority = dataTypeAnnotation.priority();
    }

    protected LiquibaseDataType(String name, int minParameters, int maxParameters) {
        this.name = name;
        this.minParameters = minParameters;
        this.maxParameters = maxParameters;
        this.aliases = new String[0];
        this.priority = 0;
    }

    public String getName() {
        return name;
    }

    public String[] getAliases() {
        return aliases;
    }

    @Override
    public int getPriority() {
        return priority;
    }

    public boolean supports(Database database) {
        return true;
    }

    public int getMinParameters(Database database) {
        return minParameters;
    }

    public int getMaxParameters(Database database) {
        return maxParameters;
    }

    /**
     * Returns an array with the parameters to the data type, e.g. NUMBER(10, 2) would return
     * an array with the items 10 and 2.
     * @return An array with the parameters. May contain 0 items.
     */
    public Object[] getParameters() {
        return parameters.toArray();
    }

    /**
     * Adds an object to the list of this data type's parameters. Note that it is possible to temporarily exceed the
     * allowed number of allowed parameters until {@link #validate(Database)} is called, because the number of
     * allowed parameters might differ between DBMS.
     * @param value the new value to add as parameter.
     */
    public void addParameter(Object value) {
        this.parameters.add(value);
    }

    /**
     * Returns additional information that was stored during {@link DataTypeFactory#fromDescription(String, Database)}
     * or other parsers.
     * @return the additional information. Might be null.
     */
    public String getAdditionalInformation() {
        return additionalInformation;
    }

    public void setAdditionalInformation(String additionalInformation) {
        this.additionalInformation = additionalInformation;
    }

    /**
     * Obtains the "raw" data type definition if one was used to create this object as a result of parsing
     * @return the raw definition, or null.
     */
    public String getRawDefinition() {
        return rawDefinition;
    }

    /**
     * Validates the correct state of this data type against a given database.
     * @param database the database to validate against
     * @return true if the current settings for this data type can be implemented on the given database, false otherwise
     */
    public boolean validate(Database database) {
        int maxParameters = this.getMaxParameters(database);
        int minParameters = this.getMinParameters(database);

        if (parameters.size() > maxParameters) {
            throw new UnexpectedLiquibaseException("Type "+getClass()+" doesn't support "+ maxParameters+" parameters");
        }
        if (parameters.size() < minParameters) {
            throw new UnexpectedLiquibaseException("Type "+getClass()+" requires "+ minParameters+" parameters");
        }

        return true;
    }

    /**
     * Transforms this data type into the native data type of the target DBMS.
     * @param database the {@link Database} for which the native data type is to be constructed
     * @return the new, native data type
     */
    public DatabaseDataType toDatabaseDataType(Database database) {
        if (database instanceof MSSQLDatabase) {
            String name = database.escapeDataTypeName(getName());
            int dataTypeMaxParameters = database.getDataTypeMaxParameters(getName());
            Object[] parameters = getParameters();
            if (dataTypeMaxParameters < parameters.length) {
                parameters = Arrays.copyOfRange(parameters, 0, dataTypeMaxParameters);
            }
            return new DatabaseDataType(name, parameters);
        }

        DatabaseDataType type = new DatabaseDataType(name.toUpperCase(Locale.US), getParameters());
        type.addAdditionalInformation(additionalInformation);

        return type;
    }

    /**
     * Returns the value object in a format to include in SQL. Quote if necessary.
     */
    public String objectToSql(Object value, Database database) {
        if ((value == null) || "null".equals(value.toString().toLowerCase(Locale.US))) {
            return null;
        } else if (value instanceof DatabaseFunction) {
            return functionToSql((DatabaseFunction) value, database);
        } else if (value instanceof Number) {
            return numberToSql((Number) value, database);
        }
        return otherToSql(value, database);
    }

    protected String functionToSql(DatabaseFunction function, Database database) {
        return (function == null) ? null : database.generateDatabaseFunctionValue(function);
    }

    protected String numberToSql(Number number, Database database) {
        if (number == null) {
            return null;
        }
        if (number instanceof BigDecimal) {
            return formatNumber(((BigDecimal) number).toPlainString());
        }
        return formatNumber(number.toString());
    }

    protected String otherToSql(Object value, Database database) {
        return (value == null) ? null : value.toString();
    }

    public Object sqlToObject(String value, Database database) {
        return value;
    }

    @Override
    public String toString() {
        String returnString = getName();
        if ((parameters != null) && !parameters.isEmpty() && (maxParameters > 0)) {
            returnString += "(";
            for (Object param : parameters) {
                if (returnString == null) {
                    returnString += "NULL,";
                }
                returnString += param.toString()+",";
            }
            returnString = returnString.replaceFirst(",$", "");

            returnString += ")";
        }

        if (additionalInformation != null) {
            returnString += " "+additionalInformation;
        }

        return returnString.trim();
    }

    @Override
    public boolean equals(final Object o) {
        return (o instanceof LiquibaseDataType) && toString().equals(o.toString());
    }

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

    /**
     * Determines if the given function name refers to the function that returns the current
     * time and date for a specific DBMS. Also returns true if the name returns the Liquibase wildcard
     * CURRENT_DATE_TIME_PLACE_HOLDER, which will later be translated into the appropriate function.
     * @param string The database function name to test
     * @param database A database object to test against
     * @return see above
     */
    protected boolean isCurrentDateTimeFunction(String string, Database database) {
        return string.toLowerCase(Locale.US).startsWith("current_timestamp")
                || string.toLowerCase(Locale.US).startsWith(DatabaseFunction.CURRENT_DATE_TIME_PLACE_HOLDER)
                || database.getCurrentDateTimeFunction().toLowerCase(Locale.US).equals(string.toLowerCase(Locale.US));
    }

    public void finishInitialization(String originalDefinition) {
        this.rawDefinition = originalDefinition;
    }

    /**
     * Removes any trailing ".0[...]0" from the end of a number
     * @param value the number (in String form) to format
     * @return the formatted number
     */
    protected String formatNumber(String value) {
        if (value == null) {
            return null;
        }
        return value.replaceFirst("\\.0+$", "");
    }

    /**
     * Returns one of the four basic data types for use in LoadData: BOOLEAN, NUMERIC, DATE or STRING
     * @return one of the above Strings
     */
    public abstract LoadDataChange.LOAD_DATA_TYPE getLoadTypeName();

}


package liquibase.database.core;

import liquibase.Scope;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.exception.DatabaseException;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.statement.core.RawSqlStatement;
import liquibase.util.StringUtil;

public class DB2Database extends AbstractDb2Database {

	@Override
	public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
		return conn.getDatabaseProductName().startsWith("DB2")
				&& !StringUtil.startsWith(conn.getDatabaseProductVersion(), "DSN");
	}

	@Override
	public String getShortName() {
		return "db2";
	}

	/**
	 * boolean data type column are allowed for versions >= 11.1.1.1
	 * @return
	 */
	public boolean supportsBooleanDataType() {
		if (getConnection() == null)
			return false; /// assume not;
		try {

			final Integer fixPack = getDb2FixPack();

			if (fixPack == null)
				throw new DatabaseException("Error getting fix pack number");

			return getDatabaseMajorVersion() > 11
					|| getDatabaseMajorVersion() == 11 && getDatabaseMinorVersion() >= 1 && fixPack.intValue() >= 1;

		} catch (final DatabaseException e) {
			return false; // assume not
		}
	}

	private Integer getDb2FixPack() {
		if (getConnection() == null || getConnection() instanceof OfflineConnection)
			return null;
		try {
			return ExecutorService.getInstance().getExecutor(this).queryForObject(
					new RawSqlStatement("SELECT fixpack_num FROM TABLE (sysproc.env_get_inst_info()) as INSTANCEINFO"),
					Integer.class);
		} catch (final Exception e) {
			Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Error getting fix pack number", e);
		}
		return null;
	}

}


package liquibase.database.core;

import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.DateParseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogFactory;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.statement.DatabaseFunction;
import liquibase.util.ISODateFormat;
import liquibase.util.JdbcUtils;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.text.ParseException;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class H2Database extends AbstractJdbcDatabase {

    private static String START_CONCAT = "CONCAT(";
    private static String END_CONCAT = ")";
    private static String SEP_CONCAT = ", ";
    private static List keywords = Arrays.asList(
            "CROSS",
            "CURRENT_DATE",
            "CURRENT_TIME",
            "CURRENT_TIMESTAMP",
            "DISTINCT",
            "EXCEPT",
            "EXISTS",
            "FALSE",
            "FETCH",
            "FOR",
            "FROM",
            "FULL",
            "GROUP",
            "HAVING",
            "INNER",
            "INTERSECT",
            "IS",
            "JOIN",
            "LIKE",
            "LIMIT",
            "MINUS",
            "NATURAL",
            "NOT",
            "NULL",
            "OFFSET",
            "ON",
            "ORDER",
            "PRIMARY",
            "ROWNUM",
            "SELECT",
            "SYSDATE",
            "SYSTIME",
            "SYSTIMESTAMP",
            "TODAY",
            "TRUE",
            "UNION",
            "UNIQUE",
            "WHERE");
    private String connectionSchemaName = "PUBLIC";

    private static final int MAJOR_VERSION_FOR_MINMAX_IN_SEQUENCES = 1;
    private static final int MINOR_VERSION_FOR_MINMAX_IN_SEQUENCES = 3;
    private static final int BUILD_VERSION_FOR_MINMAX_IN_SEQUENCES = 175;

    public H2Database() {
        super.unquotedObjectsAreUppercased=true;
        super.setCurrentDateTimeFunction("NOW()");
        // for current date
        this.dateFunctions.add(new DatabaseFunction("CURRENT_DATE"));
        this.dateFunctions.add(new DatabaseFunction("CURDATE"));
        this.dateFunctions.add(new DatabaseFunction("SYSDATE"));
        this.dateFunctions.add(new DatabaseFunction("TODAY"));
        // for current time
        this.dateFunctions.add(new DatabaseFunction("CURRENT_TIME"));
        this.dateFunctions.add(new DatabaseFunction("CURTIME"));
        // for current timestamp
        this.dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
        this.dateFunctions.add(new DatabaseFunction("NOW"));

        super.sequenceNextValueFunction = "NEXTVAL('%s')";
        super.sequenceCurrentValueFunction = "CURRVAL('%s')";
        // According to http://www.h2database.com/html/datatypes.html, retrieved on 2017-06-05
        super.unmodifiableDataTypes.addAll(Arrays.asList("int", "integer", "mediumint", "int4", "signed", "boolean",
                "bit", "bool", "tinyint", "smallint", "int2", "year", "bigint", "int8", "identity", "float", "float8",
                "real", "float4", "time", "date", "timestamp", "datetime", "smalldatetime", "timestamp with time zone",
                "other", "uuid", "array", "geometry"));
    }

    @Override
    public String getShortName() {
        return "h2";
    }

    @Override
    public Integer getDefaultPort() {
        return 8082;
    }

    @Override
    protected String getDefaultDatabaseProductName() {
        return "H2";
    }

    @Override
    public String getDefaultDriver(String url) {
        if (url.startsWith("jdbc:h2")) {
            return "org.h2.Driver";
        }
        return null;
    }

    @Override
    public int getPriority() {
        return PRIORITY_DATABASE;
    }

    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        return "H2".equals(conn.getDatabaseProductName());
    }

    @Override
    public boolean supportsTablespaces() {
        return false;
    }

    @Override
    public String getViewDefinition(CatalogAndSchema schema, String name) throws DatabaseException {
        String definition = super.getViewDefinition(schema, name);
        if (!definition.startsWith("SELECT")) {
            definition = definition.replaceFirst(".*?\\n", ""); //some h2 versions return "create view....as\nselect
        }

        definition = definition.replaceFirst("/\\*.*",""); //sometimes includes comments at the end
        return definition;
    }

    @Override
    public Date parseDate(String dateAsString) throws DateParseException {
        try {
            return new ISODateFormat().parse(dateAsString);
        } catch (ParseException e) {
            throw new DateParseException(dateAsString);
        }
    }

    @Override
    public boolean isSafeToRunUpdate() throws DatabaseException {
        if (getConnection() == null) {
            return true;
        }
        String url = getConnection().getURL();
        boolean isLocalURL = (
                super.isSafeToRunUpdate()
                        || url.startsWith("jdbc:h2:file:")
                        || url.startsWith("jdbc:h2:mem:")
                        || url.startsWith("jdbc:h2:zip:")
                        || url.startsWith("jdbc:h2:~")
        );
        return isLocalURL;
    }

    @Override
    public boolean supportsSequences() {
        return true;
    }

    @Override
    protected String getConnectionSchemaName() {
        return connectionSchemaName;
    }

    @Override
    public String getConcatSql(String... values) {
        if (values == null) {
            return null;
        }

        return getConcatSql(Arrays.asList(values));
    }

    /**
     * Recursive way of building CONCAT instruction
     *
     * @param values a non null List of String
     * @return a String containing the CONCAT instruction with all elements, or only a value if there is only one element in the list
     */
    private String getConcatSql(List<String> values) {
        if (values.size() == 1) {
            return values.get(0);
        } else {
            return START_CONCAT + values.get(0) + SEP_CONCAT + getConcatSql(values.subList(1, values.size())) + END_CONCAT;
        }
    }

    @Override
    public String getDateLiteral(String isoDate) {
        return "'" + isoDate.replace('T', ' ') + "'";
    }

    @Override
    public boolean isReservedWord(String objectName) {
        return keywords.contains(objectName.toUpperCase(Locale.US));
    }

    @Override
    public boolean supportsInitiallyDeferrableColumns() {
        return false;
    }

    @Override
    protected String getAutoIncrementClause() {
        return "AUTO_INCREMENT";
    }

    @Override
    protected String getAutoIncrementStartWithClause() {
	return "%d";
    }

    @Override
    protected String getAutoIncrementByClause() {
	return "%d";
    }

    @Override
    public boolean createsIndexesForForeignKeys() {
        return true;
    }

    @Override
    public boolean supportsDropTableCascadeConstraints() {
        return true;
    }

    @Override
    public void setConnection(DatabaseConnection conn) {
        Connection sqlConn = null;

        if (!(conn instanceof OfflineConnection)) {
            try {
                if (conn instanceof JdbcConnection) {
                    Method wrappedConn = conn.getClass().getMethod("getWrappedConnection");
                    wrappedConn.setAccessible(true);
                    sqlConn = (Connection) wrappedConn.invoke(conn);
                }
            } catch (Exception e) {
                throw new UnexpectedLiquibaseException(e);
            }

            if (sqlConn != null) {
                Statement statement = null;
                ResultSet resultSet = null;
                try {
                    statement = sqlConn.createStatement();
                    resultSet = statement.executeQuery("SELECT SCHEMA()");
                    String schemaName = null;
                    if (resultSet.next()) {
                        schemaName = resultSet.getString(1);
                    }
                    if (schemaName != null) {
                        this.connectionSchemaName = schemaName;
                    }
                } catch (SQLException e) {
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Could not read current schema name: "+e.getMessage());
                } finally {
                    JdbcUtils.close(resultSet, statement);
                }
            }
        }
        super.setConnection(conn);
    }

    public boolean supportsMinMaxForSequences() {

        try {
            if (getDatabaseMajorVersion() > MAJOR_VERSION_FOR_MINMAX_IN_SEQUENCES) {

                return true;
            } else if (getDatabaseMajorVersion() == MAJOR_VERSION_FOR_MINMAX_IN_SEQUENCES
                       && getDatabaseMinorVersion() > MINOR_VERSION_FOR_MINMAX_IN_SEQUENCES) {

                return true;
            } else if (getDatabaseMajorVersion() == MAJOR_VERSION_FOR_MINMAX_IN_SEQUENCES
                       && getDatabaseMinorVersion() == MINOR_VERSION_FOR_MINMAX_IN_SEQUENCES
                       && getBuildVersion() >= BUILD_VERSION_FOR_MINMAX_IN_SEQUENCES) {
                return true;
            }

        } catch (DatabaseException e) {
            Scope.getCurrentScope().getLog(getClass()).warning("Failed to determine database version, reported error: " + e.getMessage());
        }
        return false;
    }

    private int getBuildVersion() throws DatabaseException {

        Pattern patchVersionPattern = Pattern.compile("^(?:\\d+\\.)(?:\\d+\\.)(\\d+).*$");
        Matcher matcher = patchVersionPattern.matcher(getDatabaseProductVersion());

        if (matcher.matches()) {
            return Integer.parseInt(matcher.group(1));
        }
        else {
            Scope.getCurrentScope().getLog(getClass()).warning("Failed to determine H2 build number from product version: " + getDatabaseProductVersion());
            return -1;
        }

    }

    @Override
    public int getMaxFractionalDigitsForTimestamp() {
        // http://www.h2database.com/html/datatypes.html seems to imply 9 digits
        return 9;
    }
}


package liquibase.executor.jvm;

import liquibase.util.JdbcUtils;

import java.sql.ResultSet;
import java.sql.ResultSetMetaData;
import java.sql.SQLException;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

/**
 * {@link RowMapper} implementation that creates a java.util.Map
 * for each row, representing all columns as key-value pairs: one
 * entry for each column, with the column name as key.
 *

 *
The Map implementation to use and the key to use for each column
 * in the column Map can be customized through overriding
 * {@link #createColumnMap} and {@link #getColumnKey}, respectively.
 *

 *
 * @author Spring Framework
 */
@SuppressWarnings({"unchecked"})
public class ColumnMapRowMapper implements RowMapper {

    @Override
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        ResultSetMetaData rsmd = rs.getMetaData();
        int columnCount = rsmd.getColumnCount();
        Map mapOfColValues = createColumnMap(columnCount);
        for (int i = 1; i <= columnCount; i++) {
            String key = getColumnKey(rsmd.getColumnLabel(i));
            Object obj = getColumnValue(rs, i);
            mapOfColValues.put(key, obj);
        }
        return mapOfColValues;
    }

    /**
     * Create a Map instance to be used as column map.
     * @param columnCount the column count, to be used as initial
     *                    capacity for the Map
     * @return the new Map instance
     */
    protected Map createColumnMap(int columnCount) {
        return new LinkedHashMap(columnCount);
    }

    /**
     * Determine the key to use for the given column in the column Map.
     *
     * @param columnName the column name (uppercase) as returned by the ResultSet
     * @return the column key to use
     * @see java.sql.ResultSetMetaData#getColumnName
     */
    protected String getColumnKey(String columnName) {
        return columnName.toUpperCase(Locale.US);
    }

    /**
     * Retrieve a JDBC object value for the specified column.
     *
The default implementation uses the getObject method.
     * Additionally, this implementation includes a "hack" to get around Oracle
     * returning a non standard object for their TIMESTAMP datatype.
     *
     * @param rs    is the ResultSet holding the data
     * @param index is the column index
     * @return the Object returned
     */
    protected Object getColumnValue(ResultSet rs, int index) throws SQLException {
        return JdbcUtils.getResultSetValue(rs, index);
    }

}