package liquibase.util;

import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.statement.DatabaseFunction;
import liquibase.statement.SequenceCurrentValueFunction;
import liquibase.statement.SequenceNextValueFunction;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.*;

/**
 * Various methods that make it easier to read and write object properties using the propertyName, instead of having
 * to look up the correct reader/writer methods manually first. All methods in this class are static by nature.
 */
public class ObjectUtil {

    /**
     * Cache for the methods of classes that we have been queried about so far.
     */
    private static Map<Class<?>, Method[]> methodCache = new HashMap<>();

    /**
     * For a given object, try to find the appropriate reader method and return the value, if set
     * for the object. If the property is currently not set for the object, an
     * {@link UnexpectedLiquibaseException} run-time exception occurs.
     *
     * @param object                        the object to examine
     * @param propertyName                  the property name for which the value should be read
     * @return                              the stored value
     */
    public static Object getProperty(Object object, String propertyName)
        throws UnexpectedLiquibaseException {
        Method readMethod = getReadMethod(object, propertyName);
        if (readMethod == null) {
            throw new UnexpectedLiquibaseException(
                String.format("Property [%s] was not found for object type [%s]", propertyName,
                    object.getClass().getName()
                ));
        }

        try {
            return readMethod.invoke(object);
        } catch (IllegalAccessException|InvocationTargetException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    /**
     * Tried to determine the appropriate reader method for a given propertyName of a given object and, if found,
     * returns the class of its return type.
     * @param object the object to examine
     * @param propertyName the property name whose reading method should be searched
     * @return the class name of the return type if the reading method is found, null if it is not found.
     */
    public static Class getPropertyType(Object object, String propertyName) {
        Method readMethod = getReadMethod(object, propertyName);
        if (readMethod == null) {
            return null;
        }
        return readMethod.getReturnType();
    }

    /**
     * Examines the given object's class and returns true if reader and writer methods both exist for the
     * given property name.
     * @param object the object for which the class should be examined
     * @param propertyName the property name to search
     * @return true if both reader and writer methods exist
     */
    public static boolean hasProperty(Object object, String propertyName) {
        return hasReadProperty(object, propertyName) && hasWriteProperty(object, propertyName);
    }

    /**
     * Examines the given object's class and returns true if a reader method exists for the
     * given property name.
     * @param object the object for which the class should be examined
     * @param propertyName the property name to search
     * @return true if a reader method exists
     */
    public static boolean hasReadProperty(Object object, String propertyName) {
        return getReadMethod(object, propertyName) != null;
    }

    /**
     * Examines the given object's class and returns true if a writer method exists for the
     * given property name.
     * @param object the object for which the class should be examined
     * @param propertyName the property name to search
     * @return true if a writer method exists
     */
    public static boolean hasWriteProperty(Object object, String propertyName) {
        return getWriteMethod(object, propertyName) != null;
    }

    /**
     * Tries to guess the "real" data type of propertyValue by the given propertyName, then sets the
     * selected property of the given object to that value.
     * @param object                        the object whose property should be set
     * @param propertyName                  name of the property to set
     * @param propertyValue                 new value of the property, as String
     */
    public static void setProperty(Object object, String propertyName, String propertyValue)  {
        Method method = getWriteMethod(object, propertyName);
        if (method == null) {
            throw new UnexpectedLiquibaseException (
                String.format("Property [%s] was not found for object type [%s]", propertyName,
                    object.getClass().getName()
                ));
        }

        Class<?> parameterType = method.getParameterTypes()[0];
        Object finalValue = propertyValue;
        if (parameterType.equals(Boolean.class) || parameterType.equals(boolean.class)) {
            finalValue = Boolean.valueOf(propertyValue);
        } else if (parameterType.equals(Integer.class)) {
            finalValue = Integer.valueOf(propertyValue);
        } else if (parameterType.equals(Long.class)) {
            finalValue = Long.valueOf(propertyValue);
        } else if (parameterType.equals(BigInteger.class)) {
            finalValue = new BigInteger(propertyValue);
        } else if (parameterType.equals(BigDecimal.class)) {
            finalValue = new BigDecimal(propertyValue);
        } else if (parameterType.equals(DatabaseFunction.class)) {
            finalValue = new DatabaseFunction(propertyValue);
        } else if (parameterType.equals(SequenceNextValueFunction.class)) {
            finalValue = new SequenceNextValueFunction(propertyValue);
        } else if (parameterType.equals(SequenceCurrentValueFunction.class)) {
            finalValue = new SequenceCurrentValueFunction(propertyValue);
        } else if (Enum.class.isAssignableFrom(parameterType)) {
            finalValue = Enum.valueOf((Class<Enum>) parameterType, propertyValue);
        }
        try {
            method.invoke(object, finalValue);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new UnexpectedLiquibaseException(e);
        } catch (IllegalArgumentException e) {
            throw new UnexpectedLiquibaseException("Cannot call " + method.toString()
                + " with value of type " + finalValue.getClass().getName());
        }
    }

    /**
     * Sets the selected property of the given object to propertyValue. A run-time exception will occur if the
     * type of value is incompatible with the reader/writer method signatures of the given propertyName.
     * @param object                        the object whose property should be set
     * @param propertyName                  name of the property to set
     * @param propertyValue                 new value of the property
     */
    public static void setProperty(Object object, String propertyName, Object propertyValue) {
        Method method = getWriteMethod(object, propertyName);
        if (method == null) {
            throw new UnexpectedLiquibaseException (
                String.format("Property [%s] was not found for object type [%s]", propertyName,
                    object.getClass().getName()
                ));
        }

        try {
            if (propertyValue == null) {
                setProperty(object, propertyName, null);
                return;
            }
            if (!method.getParameterTypes()[0].isAssignableFrom(propertyValue.getClass())) {
                setProperty(object, propertyName, propertyValue.toString());
                return;
            }

            method.invoke(object, propertyValue);
        } catch (IllegalAccessException | InvocationTargetException e) {
            throw new UnexpectedLiquibaseException(e);
        } catch (IllegalArgumentException e) {
            throw new UnexpectedLiquibaseException("Cannot call " + method.toString() + " with value of type "
                + (propertyValue == null ? "null" : propertyValue.getClass().getName()));
        }
    }

    /**
     * Tries to find the Java method to read a given propertyName for the given object.
     * @param object the object whose class will be examined
     * @param propertyName the property name for which the read method should be searched
     * @return the {@link Method} if found, null in all other cases.
     */
    private static Method getReadMethod(Object object, String propertyName) {
        String getMethodName = "get" + propertyName.substring(0, 1).toUpperCase(Locale.ENGLISH)
            + propertyName.substring(1);
        String isMethodName = "is" + propertyName.substring(0, 1).toUpperCase(Locale.ENGLISH)
            + propertyName.substring(1);

        Method[] methods = getMethods(object);

        for (Method method : methods) {
            if ((method.getName().equals(getMethodName) || method.getName().equals(isMethodName)) && (method
                .getParameterTypes().length == 0)) {
                return method;
            }
        }
        return null;
    }

    /**
     * Tries to find the Java method to write a new value for a given propertyName to the given object.
     * @param object the object whose class will be examined
     * @param propertyName the property name for which the write method is to be searched
     * @return the {@link Method} if found, null in all other cases.
     */
    private static Method getWriteMethod(Object object, String propertyName) {
        String methodName = "set"
            + propertyName.substring(0, 1).toUpperCase(Locale.ENGLISH) + propertyName.substring(1);
        Method[] methods = getMethods(object);

        for (Method method : methods) {
            if (method.getName().equals(methodName) && (method.getParameterTypes().length == 1)) {
                return method;
            }
        }
        return null;
    }

    /**
     * Determines the class of a given object and returns an array of that class's methods. The information might come
     * from a cache.
     * @param object the object to examine
     * @return a list of methods belonging to the class of the object
     */
    private static Method[] getMethods(Object object) {
        Method[] methods = methodCache.get(object.getClass());

        if (methods == null) {
            methods = object.getClass().getMethods();
            methodCache.put(object.getClass(), methods);
        }
        return methods;
    }

      /**
     * Converts the given object to the targetClass
     */
    public static <T> T convert(Object object, Class<T> targetClass) throws IllegalArgumentException {
        if (object == null) {
            return null;
        }
        if (targetClass.isAssignableFrom(object.getClass())) {
            return (T) object;
        }

        try {
            if (Enum.class.isAssignableFrom(targetClass)) {
                return (T) Enum.valueOf((Class<Enum>) targetClass, object.toString());
            } else if (Number.class.isAssignableFrom(targetClass)) {
                if (object instanceof Number) {
                    Number number = (Number) object;
                    String numberAsString = number.toString();
                    numberAsString = numberAsString.replaceFirst("\\.0+$", ""); //remove zero decimal so int/long/etc. can parse it correctly.

                    if (targetClass.equals(Byte.class)) {
                        long value = Long.valueOf(numberAsString);
                        if (value < Byte.MIN_VALUE || value > Byte.MAX_VALUE) {
                            raiseOverflowException(number, targetClass);
                        }
                        return (T) (Byte) number.byteValue();
                    } else if (targetClass.equals(Short.class)) {
                        long value = Long.valueOf(numberAsString);
                        if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) {
                            raiseOverflowException(number, targetClass);
                        }
                        return (T) (Short) number.shortValue();
                    } else if (targetClass.equals(Integer.class)) {
                        long value = Long.valueOf(numberAsString);
                        if (value < Integer.MIN_VALUE || value > Integer.MAX_VALUE) {
                            raiseOverflowException(number, targetClass);
                        }
                        return (T) (Integer) number.intValue();
                    } else if (targetClass.equals(Long.class)) {
                        return (T) Long.valueOf(numberAsString);
                    } else if (targetClass.equals(Float.class)) {
                        return (T) (Float) number.floatValue();
                    } else if (targetClass.equals(Double.class)) {
                        return (T) (Double) number.doubleValue();
                    } else if (targetClass.equals(BigInteger.class)) {
                        return (T) new BigInteger(numberAsString);
                    } else if (targetClass.equals(BigDecimal.class)) {
                        // using BigDecimal(String) here, to avoid unpredictability of BigDecimal(double)
                        // (see BigDecimal javadoc for details)
                        return (T) new BigDecimal(numberAsString);
                    } else {
                        return raiseUnknownConversionException(object, targetClass);
                    }
                } else if (object instanceof String) {
                    String string = (String) object;
                    if (string.contains(".")) {
                        string = string.replaceFirst("\\.0+$", "");
                    }
                    if (string.equals("")) {
                        string = "0";
                    }
                    if (targetClass.equals(Byte.class)) {
                        return (T) Byte.decode(string);
                    } else if (targetClass.equals(Short.class)) {
                        return (T) Short.decode(string);
                    } else if (targetClass.equals(Integer.class)) {
                        return (T) Integer.decode(string);
                    } else if (targetClass.equals(Long.class)) {
                        return (T) Long.decode(string);
                    } else if (targetClass.equals(Float.class)) {
                        return (T) Float.valueOf(string);
                    } else if (targetClass.equals(Double.class)) {
                        return (T) Double.valueOf(string);
                    } else if (targetClass.equals(BigInteger.class)) {
                        return (T) new BigInteger(string);
                    } else if (targetClass.equals(BigDecimal.class)) {
                        return (T) new BigDecimal(string);
                    } else {
                        return raiseUnknownConversionException(object, targetClass);
                    }
                } else {
                    return raiseUnknownConversionException(object, targetClass);
                }
            } else if (targetClass.isAssignableFrom(Boolean.class)) {
                String lowerCase = object.toString().toLowerCase();
                return (T) (Boolean) (lowerCase.equals("true") || lowerCase.equals("t") || lowerCase.equals("1") || lowerCase.equals("1.0") || lowerCase.equals("yes"));
            } else if (targetClass.isAssignableFrom(String.class)) {
                return (T) object.toString();
            } else if (targetClass.isAssignableFrom(List.class)) {
                if (object instanceof List) {
                    return (T) object;
                } else if (object instanceof Collection) {
                    return (T) new ArrayList((Collection) object);
                } else if (object instanceof Object[]) {
                    return (T) new ArrayList(Arrays.asList((Object[]) object));
                } else {
                    return (T) object;
                }
            } else if (targetClass.isAssignableFrom(StringClauses.class)) {
                return (T) new StringClauses().append(object.toString());
            } else if (targetClass.isAssignableFrom(Class.class)) {
                try {
                    return (T) Class.forName(object.toString());
                } catch (ClassNotFoundException e) {
                    throw new IllegalArgumentException(e);
                }
            }
            return (T) object;
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException(e);
        }
    }

    private static <T> T raiseUnknownConversionException(Object object, Class<T> targetClass) {
        throw new IllegalArgumentException("Could not convert '" + object + "' of type " + object.getClass().getName() + " to unknown target class " + targetClass.getName());
    }

    private static void raiseOverflowException(Number number, Class targetClass) {
        throw new IllegalArgumentException("Could not convert '" + number + "' of type " + number.getClass().getName() + " to target class " + targetClass.getName() + ": overflow");
    }

    /**
     * Return the defaultValue if the passed value is null. Otherwise, return the original value.
     */
    public static <T> T defaultIfNull(T value, T defaultValue) {
        if (value == null) {
            return defaultValue;
        } else {
            return value;
        }
    }
}


package liquibase.diff;

import liquibase.database.Database;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.DataType;

import java.math.BigDecimal;
import java.util.*;

public class ObjectDifferences {

    private CompareControl compareControl;
    private HashMap<String, Difference> differences = new HashMap<>();

    public ObjectDifferences(CompareControl compareControl) {
        this.compareControl = compareControl;
    }

    public Set<Difference> getDifferences() {
        return Collections.unmodifiableSet(new TreeSet<>(differences.values()));
    }

    public Difference getDifference(String field) {
        return differences.get(field);
    }

    public boolean isDifferent(String field) {
        return differences.containsKey(field);
    }

    public ObjectDifferences addDifference(String changedField, Object referenceValue, Object compareToValue) {
        this.differences.put(changedField, new Difference(changedField, referenceValue, compareToValue));

        return this;
    }

    public ObjectDifferences addDifference(String message, String changedField, Object referenceValue, Object compareToValue) {
        this.differences.put(changedField, new Difference(message, changedField, referenceValue, compareToValue));

        return this;
    }

    public boolean hasDifferences() {
        return !differences.isEmpty();
    }

    public void compare(String attribute, DatabaseObject referenceObject, DatabaseObject compareToObject, CompareFunction compareFunction) {
        compare(null, attribute, referenceObject, compareToObject, compareFunction);
    }
    public void compare(String message, String attribute, DatabaseObject referenceObject, DatabaseObject compareToObject, CompareFunction compareFunction) {
        if (compareControl.isSuppressedField(referenceObject.getClass(), attribute)) {
            return;
        }

        Object referenceValue = referenceObject.getAttribute(attribute, Object.class);
        Object compareValue = compareToObject.getAttribute(attribute, Object.class);

        referenceValue = undoCollection(referenceValue, compareValue);
        compareValue = undoCollection(compareValue, referenceValue);

        boolean different;
        if ((referenceValue == null) && (compareValue == null)) {
            different = false;
        } else if (((referenceValue == null) && (compareValue != null)) || ((referenceValue != null) && (compareValue
            == null))) {
            different = true;
        } else {
            different = !compareFunction.areEqual(referenceValue, compareValue);
        }

        if (different) {
            addDifference(message, attribute, referenceValue, compareValue);
        }

    }

    /**
     * Sometimes an attribute in one object is a single-entity collection and on the other it is just the object.
     * Check the passed potentialCollection and if it is a single-entry collection of the same type as the otherObject, return just the collection element.
     * Otherwise, return the original collection.
     */
    protected Object undoCollection(Object potentialCollection, Object otherObject) {
        if ((potentialCollection != null) && (otherObject != null) && (potentialCollection instanceof Collection) &&
            !(otherObject instanceof Collection)) {
            if ((((Collection) potentialCollection).size() == 1) && ((Collection) potentialCollection).iterator()
                .next().getClass().equals(otherObject.getClass())) {
                potentialCollection = ((Collection) potentialCollection).iterator().next();
            }
        }
        return potentialCollection;
    }

    public boolean removeDifference(String attribute) {
        return differences.remove(attribute) != null;
    }

    public CompareControl.SchemaComparison[] getSchemaComparisons() {
        return compareControl.getSchemaComparisons();
    }

    public interface CompareFunction {
        boolean areEqual(Object referenceValue, Object compareToValue);
    }

    public static class StandardCompareFunction implements CompareFunction {

        private final CompareControl.SchemaComparison[] schemaComparisons;
        private Database accordingTo;

        public StandardCompareFunction(CompareControl.SchemaComparison[] schemaComparisons, Database accordingTo) {
            this.schemaComparisons = schemaComparisons;
            this.accordingTo = accordingTo;
        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            if ((referenceValue instanceof DatabaseObject) && (compareToValue instanceof DatabaseObject)) {
                return DatabaseObjectComparatorFactory.getInstance().isSameObject((DatabaseObject) referenceValue, (DatabaseObject) compareToValue, schemaComparisons, accordingTo);
            } else {
                if ((referenceValue instanceof Number) && (compareToValue instanceof Number)
                        && !referenceValue.getClass().equals(compareToValue.getClass())) { //standardize on a common number type
                    referenceValue = new BigDecimal(referenceValue.toString());
                    compareToValue = new BigDecimal(compareToValue.toString());
                }
                if ((referenceValue instanceof Number) && (referenceValue instanceof Comparable)) {
                    return (compareToValue instanceof Number) && (((Comparable) referenceValue).compareTo
                        (compareToValue) == 0);
                } else {
                    return referenceValue.equals(compareToValue);
                }
            }
        }
    }

    public static class ToStringCompareFunction implements CompareFunction {

        private boolean caseSensitive;

        public ToStringCompareFunction(boolean caseSensitive) {
            this.caseSensitive = caseSensitive;
        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            if (caseSensitive) {
                return referenceValue.toString().equals(compareToValue.toString());
            } else {
                return referenceValue.toString().equalsIgnoreCase(compareToValue.toString());
            }

        }
    }

    public static class DatabaseObjectNameCompareFunction implements CompareFunction {

        private final Database accordingTo;
        private Class<? extends DatabaseObject> type;

        public DatabaseObjectNameCompareFunction(Class<? extends DatabaseObject> type, Database accordingTo) {
            this.type = type;
            this.accordingTo = accordingTo;

        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if (referenceValue instanceof Collection) {
                if (!(compareToValue instanceof Collection)) {
                    return false;
                }
                if (((Collection) referenceValue).size() != ((Collection) compareToValue).size()) {
                    return false;
                } else {
                    Iterator referenceIterator = ((Collection) referenceValue).iterator();
                    Iterator compareToIterator = ((Collection) compareToValue).iterator();

                    while (referenceIterator.hasNext()) {
                        if (!areEqual(referenceIterator.next(), compareToIterator.next())) {
                            return false;
                        }
                    }
                    return true;
                }
            }

            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            String object1Name;
            if (referenceValue instanceof DatabaseObject) {
                object1Name = accordingTo.correctObjectName(((DatabaseObject) referenceValue).getAttribute("name", String.class), type);
            } else {
                object1Name = referenceValue.toString();
            }

            String object2Name;
            if (compareToValue instanceof DatabaseObject) {
                object2Name = accordingTo.correctObjectName(((DatabaseObject) compareToValue).getAttribute("name", String.class), type);
            } else {
                object2Name = compareToValue.toString();
            }

            if ((object1Name == null) && (object2Name == null)) {
                return true;
            }
            if ((object1Name == null) || (object2Name == null)) {
                return false;
            }
            if (accordingTo.isCaseSensitive()) {
                return object1Name.equals(object2Name);
            } else {
                return object1Name.equalsIgnoreCase(object2Name);
            }

        }
    }

    public static class DataTypeCompareFunction implements CompareFunction {

        private final Database accordingTo;

        public DataTypeCompareFunction(Database accordingTo) {
            this.accordingTo = accordingTo;

        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            DataType referenceType = (DataType) referenceValue;
            DataType compareToType = (DataType) compareToValue;

            if (!referenceType.getTypeName().equalsIgnoreCase(compareToType.getTypeName())) {
                return false;
            }

            if (compareToType.toString().contains("(") && referenceType.toString().contains("(")) {
                return compareToType.toString().equalsIgnoreCase(referenceType.toString());
            } else {
                return true;
            }

        }
    }

    public static class OrderedCollectionCompareFunction implements CompareFunction {

        private StandardCompareFunction compareFunction;

        public OrderedCollectionCompareFunction(StandardCompareFunction compareFunction) {
            this.compareFunction = compareFunction;
        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            if (!(referenceValue instanceof Collection) || (!(compareToValue instanceof Collection))) {
                return false;
            }

            if (((Collection) referenceValue).size() != ((Collection) compareToValue).size()) {
                return false;
            }

            Iterator referenceIterator = ((Collection) referenceValue).iterator();
            Iterator compareIterator = ((Collection) compareToValue).iterator();

            if (((Collection) referenceValue).size() != ((Collection) compareToValue).size()) {
                return false;
            }

            while (referenceIterator.hasNext()) {
                Object referenceObj = referenceIterator.next();
                Object compareObj = compareIterator.next();

                if (!compareFunction.areEqual(referenceObj, compareObj)) {
                    return false;
                }
            }

            return true;
        }
    }

    public static class UnOrderedCollectionCompareFunction implements CompareFunction {

        private StandardCompareFunction compareFunction;

        public UnOrderedCollectionCompareFunction(StandardCompareFunction compareFunction) {
            this.compareFunction = compareFunction;
        }

        @Override
        public boolean areEqual(Object referenceValue, Object compareToValue) {
            if ((referenceValue == null) && (compareToValue == null)) {
                return true;
            }
            if ((referenceValue == null) || (compareToValue == null)) {
                return false;
            }

            if (!(referenceValue instanceof Collection) || (!(compareToValue instanceof Collection))) {
                return false;
            }

            if (((Collection) referenceValue).size() != ((Collection) compareToValue).size()) {
                return false;
            }

            if (((Collection) referenceValue).size() != ((Collection) compareToValue).size()) {
                return false;
            }

            for (Object referenceObj : ((Collection) referenceValue)) {
                Object foundMatch = null;
                List<Object> unmatchedCompareToValues = new ArrayList<>(((Collection<?>) compareToValue));
                for (Object compareObj : unmatchedCompareToValues) {
                    if (compareFunction.areEqual(referenceObj, compareObj)) {
                        foundMatch = compareObj;
                        break;
                    }
                }
                if (foundMatch == null) {
                    return false;
                } else {
                    unmatchedCompareToValues.remove(foundMatch);
                }
            }

            return true;
        }
    }

}


package liquibase.structure.core;

import liquibase.statement.NotNullConstraint;
import liquibase.util.StringUtil;

import java.util.ArrayList;
import java.util.List;

public class Table extends Relation {

    public Table() {
        setAttribute("outgoingForeignKeys", new ArrayList<ForeignKey>());
        setAttribute("indexes", new ArrayList<Index>());
        setAttribute("uniqueConstraints", new ArrayList<UniqueConstraint>());
        setAttribute("notNullConstraints", new ArrayList<UniqueConstraint>());
    }

    public Table(String catalogName, String schemaName, String tableName) {
        this.setSchema(new Schema(catalogName, schemaName));
        setName(tableName);
    }

    public PrimaryKey getPrimaryKey() {
        return getAttribute("primaryKey", PrimaryKey.class);
    }

    public void setPrimaryKey(PrimaryKey primaryKey) {
        this.setAttribute("primaryKey", primaryKey);
    }

    /**
     * Returns the list of all outgoing FOREIGN KEYS from this table
     */
    public List<ForeignKey> getOutgoingForeignKeys() {
        List<ForeignKey> fkList = getAttribute("outgoingForeignKeys", List.class);
        return ((fkList == null) ? new ArrayList<ForeignKey>(0) : fkList);
    }

    public List<Index> getIndexes() {
        return getAttribute("indexes", List.class);
    }

    public List<UniqueConstraint> getUniqueConstraints() {
        return getAttribute("uniqueConstraints", List.class);
    }
    public List<NotNullConstraint> getNotNullConstraints() {
        return getAttribute("notNullConstraints", List.class);
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if ((o == null) || (getClass() != o.getClass())) return false;

        Table that = (Table) o;

        if ((this.getSchema() != null) && (that.getSchema() != null)) {
            boolean schemasTheSame = StringUtil.trimToEmpty(this.getSchema().getName()).equalsIgnoreCase(StringUtil.trimToEmpty(that.getSchema().getName()));
            if (!schemasTheSame) {
                return false;
            }
        }

        return getName().equalsIgnoreCase(that.getName());

    }

    @Override
    public int hashCode() {
        return StringUtil.trimToEmpty(getName()).toUpperCase().hashCode();
    }

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

    @Override
    public Table setName(String name) {
        return (Table) super.setName(name);
    }

    public String getTablespace() {
        return getAttribute("tablespace",String.class);
    }

    public Table setTablespace(String tablespace) {
        setAttribute("tablespace", tablespace);
        return this;
    }

}


package liquibase.integration.cdi;

import liquibase.Scope;
import liquibase.integration.cdi.annotations.Liquibase;
import liquibase.integration.cdi.annotations.LiquibaseSchema;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.resource.FileSystemResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.util.FileUtil;
import liquibase.util.StreamUtil;

import javax.enterprise.inject.spi.Bean;
import javax.enterprise.inject.spi.BeanManager;
import javax.enterprise.util.AnnotationLiteral;
import javax.inject.Inject;
import javax.inject.Singleton;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.annotation.Annotation;
import java.nio.channels.FileChannel;
import java.nio.channels.FileLock;
import java.nio.channels.OverlappingFileLockException;
import java.util.*;
import java.util.concurrent.Callable;

/**
 * @author Nikita Lipatov (https://github.com/islonik)
 * @since 27/5/17.
 */
@Singleton
public class SchemesCDIConfigBuilder {

    private static final Logger log = Scope.getCurrentScope().getLog(SchemesCDIConfigBuilder.class);

    private static final String ROOT_PATH = System.getProperty("java.io.tmpdir");

    private static final String SCHEMA_NAME = "/schema.template.xml";
    private static final String TEMPLATE_NAME = "liquibase.cdi.schema.xml";
    private static final String INCLUDE_TPL = "\t<include file=\"%s\"/>%n";

    private static final Long FILE_LOCK_TIMEOUT = 50L;

    private final BeanManager bm;
    private final SchemesTreeBuilder treeBuilder;

    @Inject
    public SchemesCDIConfigBuilder(BeanManager bm, SchemesTreeBuilder treeBuilder) {
        this.bm = bm;
        this.treeBuilder = treeBuilder;
    }

    /**
     * API method.
     */
    public ResourceAccessor createResourceAccessor() {
        return new FileSystemResourceAccessor(new File(ROOT_PATH));
    }

    /**
     * API method.
     */
    public CDILiquibaseConfig createCDILiquibaseConfig() {
        final String id = UUID.randomUUID().toString();
        log.fine(LogType.LOG, String.format("[id = %s] createConfig(). Date: '%s'", id, new Date()));

        final InputStream is = SchemesCDIConfigBuilder.class.getResourceAsStream(SCHEMA_NAME);
        try {
            return jvmLocked(id, new Callable<CDILiquibaseConfig>() {
                public CDILiquibaseConfig call() throws Exception {
                    return createCDILiquibaseConfig(id, is);
                }
            });
        } catch (Exception ex) {
            log.warning(LogType.LOG, String.format("[id = %s] Unable to initialize liquibase where '%s'.", id, ex.getMessage()), ex);
            return null;
        } finally {
            try {
                is.close();
            } catch (IOException ioe) {
                log.warning(LogType.LOG, String.format("[id = %s] IOException during closing an input stream '%s'.", id, ioe.getMessage()), ioe);
            }
        }
    }

    private CDILiquibaseConfig createCDILiquibaseConfig(final String id, final InputStream is) throws IOException {
        File liquibaseDir = new File(String.format("%s/liquibase/schemes", ROOT_PATH));
        if (!liquibaseDir.exists() && (!liquibaseDir.mkdirs())) {
            throw new RuntimeException(String.format("[id = %s] Cannot create [%s] dirs.", id, liquibaseDir));
        }
        log.fine(LogType.LOG, String.format("[id = %s] Includes directory: [path='%s']", id, liquibaseDir.getAbsolutePath()));

        String path = String.format("%s/%s", ROOT_PATH, TEMPLATE_NAME);
        File output = new File(path);

        if (output.exists()) {
            log.fine(LogType.LOG, String.format("[id = %s] File [path='%s'] already exists, deleting", id, path));
            if (output.delete()) {
                log.fine(LogType.LOG, String.format("[id = %s] File [path='%s'] already exists, deleted successfully.", id, path));
            } else {
                log.fine(LogType.LOG, String.format("[id = %s] File [path='%s'] already exists, failed to delete.", id, path));
            }
        }
        if (!output.createNewFile()) {
            throw new RuntimeException(String.format("[id = %s] Cannot create [%s] file.", id, output));
        }
        log.info(LogType.LOG, String.format("[id = %s] File %s was created.", id, output));
        log.fine(LogType.LOG, String.format("[id = %s] Root liquibase file [path='%s'] ready.", id, path));

        long start = System.currentTimeMillis();
        log.info(LogType.LOG, String.format("[id = %s] Scanning application for liquibase schemes.", id));

        Set<Bean<?>> beans = bm.getBeans(Object.class, new AnnotationLiteralDefault());

        Set<Class<?>> classesSet = new LinkedHashSet<>();
        for (Bean<?> bean : beans) {
            classesSet.add(bean.getBeanClass());
        }
        Set<Annotation> annotationsSet = new LinkedHashSet<>();
        for (Class clazz : classesSet) {
            annotationsSet.add(clazz.getAnnotation(LiquibaseSchema.class));
        }
        List<LiquibaseSchema> liquibaseSchemaList = new ArrayList<>();
        for (Annotation ann : annotationsSet) {
            liquibaseSchemaList.add((LiquibaseSchema) ann);
        }

        List<LiquibaseSchema> treeList = treeBuilder.build(id, liquibaseSchemaList);

        List<String[]> resourceList = new ArrayList<>();
        for (LiquibaseSchema liquibaseSchema : treeList) {
            resourceList.add(liquibaseSchema.resource());
        }

        List<String> schemaPaths = new ArrayList<>();
        for (String[] resources : resourceList) {
            for (String resource : resources) {
                schemaPaths.add(copyToFile(id, liquibaseDir.getAbsolutePath(), resource));
            }
        }

        StringBuilder schemes = new StringBuilder();
        for (String schema : schemaPaths) {
            schemes.append(String.format(INCLUDE_TPL, schema)).append("\n");
        }

        log.info(LogType.LOG, String.format("[id = %s] Scan complete [took=%s milliseconds].", id, System.currentTimeMillis() - start));
        log.fine(LogType.LOG, String.format("[id = %s] Resolved schemes: %n%s%n", id, schemes));
        log.fine(LogType.LOG, String.format("[id = %s] Generating root liquibase file...", id));

        String template = StreamUtil.readStreamAsString(is); // schema.template.xml

        String xml = String.format(template, schemes);

        FileUtil.write(xml, output);

        log.info(LogType.LOG, String.format("[id = %s] File %s was written.", id, output));
        log.fine(LogType.LOG, String.format("[id = %s] Generation complete.", id));
        log.fine(LogType.LOG, String.format("[id = %s] Root liquibase xml: %n %s %n", id, xml));

        CDILiquibaseConfig config = new CDILiquibaseConfig();
        config.setChangeLog(TEMPLATE_NAME);
        return config;
    }

    synchronized CDILiquibaseConfig jvmLocked(final String id, Callable<CDILiquibaseConfig> action) throws Exception {
        return fileLocked(id, action);
    }

    /**
     * Synchronization among multiple JVM's.
     */
    CDILiquibaseConfig fileLocked(final String id, Callable<CDILiquibaseConfig> action) throws Exception {
        log.info(LogType.LOG, String.format("[id = %s] JVM lock acquired, acquiring file lock", id));
        String lockPath = String.format("%s/schema.liquibase.lock", ROOT_PATH);

        File lockFile = new File(lockPath);
        if (!lockFile.exists() && lockFile.createNewFile()) {
            log.info(LogType.LOG, String.format("[id = %s] Created lock file [path='%s'].", id, lockPath));
        }

        log.info(LogType.LOG, String.format("[id = %s] Trying to acquire the file lock [file='%s']...", id, lockPath));

        CDILiquibaseConfig actionResult = null;
        FileLock lock = null;
        try (
            FileOutputStream fileStream = new FileOutputStream(lockPath);
            FileChannel fileChannel = fileStream.getChannel();
        )
        {
            while (null == lock) {
                try {
                    lock = fileChannel.tryLock();
                } catch (OverlappingFileLockException e) {
                    log.fine(LogType.LOG, String.format("[id = %s] Lock already acquired, waiting for the lock...", id));
                }
                if (null == lock) {
                    log.fine(LogType.LOG, String.format("[id = %s] Waiting for the lock...", id));
                    Thread.sleep(FILE_LOCK_TIMEOUT);
                }
            }
            log.info(LogType.LOG, String.format("[id = %s] File lock acquired, running liquibase...", id));
            actionResult = action.call();
            lock.release();
        } catch (Exception e) {
            log.warning(LogType.LOG, e.getMessage(), e);
        }
        return actionResult;
    }

    private String copyToFile(final String id, final String liquibase, final String schema) {
        log.info(LogType.LOG, String.format("[id = %s] copyToFile(%s, %s)", id, liquibase, schema));

        InputStream is = null;
        try {
            is = this.getClass().getClassLoader().getResourceAsStream(schema);

            log.info(LogType.LOG, String.format("[id = %s] Transferring schema [resource=%s] to directory [path=%s]...", id, schema, liquibase));
            String path = schema.startsWith("/") ? schema.substring(1) : schema;
            log.fine(LogType.LOG, String.format("[id = %s] LiquibaseSchema path is [path='%s'].", id, path));

            if (path.contains("/")) {

                String dirPath = String.format("%s/%s", liquibase, path.substring(0, path.lastIndexOf('/')));
                log.fine(LogType.LOG, String.format("[id = %s] LiquibaseSchema path contains intermediate directories [path='%s'], preparing its...", id, dirPath));

                File file = new File(dirPath);
                if (!file.exists() && file.mkdirs()) {
                    log.info(LogType.LOG, String.format("[id = %s] Directories for [path='%s'] file created.", id, file.getAbsolutePath()));
                }
            }

            File file = new File(String.format("%s/%s", liquibase, path));
            if (file.exists()) {
                log.info(LogType.LOG, String.format("[id = %s] LiquibaseSchema file [path='%s'] already exists, deleting...", id, file.getAbsolutePath()));
                if (file.delete()) {
                    log.info(LogType.LOG, String.format("[id = %s] File [path='%s'] deleted.", id, file.getAbsolutePath()));
                }
            }
            if (file.createNewFile()) {
                log.info(LogType.LOG, String.format("[id = %s] File [path='%s'] created.", id, file.getAbsolutePath()));
            }
            log.fine(LogType.LOG, String.format("[id = %s] LiquibaseSchema file [path='%s'] is ready, copying data...", id, file.getAbsolutePath()));

            FileUtil.write(StreamUtil.readStreamAsString(is), file);

            String schemaPath = file.getAbsolutePath().replace(ROOT_PATH, "");
            log.info(LogType.LOG, String.format("[id = %s] Data copied, schema path is [path='%s'].", id, schemaPath));
            return schemaPath;
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            try {
                if (is != null) {
                    is.close();
                }
            } catch (IOException ioe) {
                log.warning(LogType.LOG, String.format("IOException during closing an input stream '%s'.", ioe.getMessage()), ioe);
            }
        }
    }

    static class AnnotationLiteralDefault extends AnnotationLiteral<Liquibase> {
        private static final long serialVersionUID = -2878951947483191L;
    }
}


package liquibase.sdk;

import liquibase.command.LiquibaseCommand;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.sdk.convert.ConvertCommand;
import liquibase.util.StringUtil;
import org.apache.commons.cli.*;

import java.io.File;
import java.io.FilenameFilter;
import java.util.*;

public class Main {

    private boolean debug;

    private CommandLine globalArguments;
    private String command;
    private List<String> commandArgs = new ArrayList<>();

    private Options globalOptions;

    public Main() {
        globalOptions = new Options();
    }

    public static void main(String[] args) {
        Main main = new Main();

        main.header("Liquibase SDK");

        try {
            main.init(args);

            if (main.command == null) {
                throw new UserError("No command passed");
            }

            if ("help".equals(main.command)) {
                main.printHelp();
                return;
            }

            LiquibaseCommand command;
            CommandLineParser commandParser = new GnuParser();
            if ("convert".equals(main.command)) {
                command = new ConvertCommand();

                Options options = new Options();
                options.addOption(OptionBuilder.hasArg().withDescription("Original changelog").isRequired().create("src"));
                options.addOption(OptionBuilder.hasArg().withDescription("Output changelog").isRequired().create("out"));
                options.addOption(OptionBuilder.hasArg().withDescription("Classpath").create("classpath"));

                CommandLine commandArguments = commandParser.parse(options, main.commandArgs.toArray(new String[main.commandArgs.size()]));
                ((ConvertCommand) command).setSrc(commandArguments.getOptionValue("src"));
                ((ConvertCommand) command).setOut(commandArguments.getOptionValue("out"));
                ((ConvertCommand) command).setClasspath(commandArguments.getOptionValue("classpath"));
            } else {
                throw new UserError("Unknown command: "+main.command);
            }

            command.execute();

            main.divider();
            main.out("Command executed successfully");

        } catch (UserError userError) {
            main.out("");
            main.header("ERROR EXECUTING COMMAND");
            main.out(userError.getMessage());
            main.out("");
            main.out("");
            return;
        } catch (Throwable exception) {
            System.out.println("Unexpected error: "+exception.getMessage());
            exception.printStackTrace();
        }
    }

    public void init(String[] args) throws UserError {
        CommandLineParser globalParser = new GnuParser();

        List<String> globalArgs = new ArrayList<>();

        boolean inGlobal = true;
        for (String arg : args) {
            if (inGlobal) {
                if (arg.startsWith("--")) {
                    globalArgs.add(arg);
                } else {
                    this.command = arg;
                    inGlobal = false;
                }
            } else {
                commandArgs.add(arg);
            }
        }

        try {
            this.globalArguments = globalParser.parse(globalOptions, globalArgs.toArray(new String[globalArgs.size()]));
        } catch (ParseException e) {
            throw new UserError("Error parsing global command line argument: " + e.getMessage());
        }
    }

//        Context context = Context.getInstance();
//        if (context.getSeenExtensionClasses().size() == 0) {
//            System.out.println("No extension classes found in "+StringUtil.join(context.getPackages(), ","));
//            return;
//        }
//
//        System.out.println("Extension classes found:");
//        for (Map.Entry<Class, Set<Class>> entry : context.getSeenExtensionClasses().entrySet()) {
//            System.out.println(StringUtil.indent(entry.getKey().getName()+" extensions:", 4));
//
//            System.out.println(StringUtil.indent(StringUtil.join(entry.getValue(), "\n", new StringUtil.StringUtilFormatter() {
//                @Override
//                public String toString(Object obj) {
//                    return ((Class) obj).getName();
//                }
//            }), 8));
//        }

//        header("Running Tests");
//
//        JUnitCore junit = new JUnitCore();
//        junit.addListener(new TextListener(System.out));
//        Result result = junit.run(new Computer(), StandardChangeTests.class);

    public File getSdkRoot() {
        File dir = new File(".").getAbsoluteFile();
        while (dir != null) {
            if (dir.listFiles(new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                    return "liquibase-sdk.bat".equals(name);
                }
            }).length > 0) {
                return dir;
            }

            dir = dir.getParentFile();
        }

        throw new UnexpectedLiquibaseException("Could not find Liquibase SDK home. Please run liquibase-sdk from the " +
            "liquibase/sdk directory or one of it's sub directories");
    }

    public String getCommand() {
        return command;
    }

    public void header(String... header) {
        divider();
        for (String line : header) {
            System.out.println(line);
        }
        divider();
    }

    public void divider() {
        System.out.println("---------------------------------------------------");
    }

    public void out(String message) {
        System.out.println(message);
    }

    public void debug(String message) {
        if (debug) {
            System.out.println("DEBUG: "+message);
        }
    }

    public void fatal(String error) throws UserError {
        throw new UserError(error);
    }

    public void fatal(Throwable exception) {
        fatal(exception.getMessage(), exception);
    }

    public void fatal(String error, Throwable exception) throws UserError {
        throw new UserError(error, exception);
    }

    public String getPath() {
        Map<String, String> environment = new ProcessBuilder().environment();

        String path = environment.get("Path");
        if (path == null) {
            path = environment.get("PATH");
        }
        if (path == null) {
            path = environment.get("path");
        }
        if (path == null) {
            throw new UnexpectedLiquibaseException("Cannot find path variable in environment. Possible variables are " + StringUtil.join(environment.keySet(), ","));
        }

        return path;
    }

    public String getPath(String... possibleFileNames) {
        Set<String> fileNames = new HashSet<>();

        for (String dir : getPath().split("[:;]")) {
            for (String fileName : possibleFileNames) {
                File file = new File(dir, fileName);
                if (file.exists()) {
                    return file.getAbsolutePath();
                }
            }
        }
        return null;

    }

    public void printHelp() {
        HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("liquibase-sdk [global options] [command] [command options]", globalOptions);
    }

    private static class UserError extends RuntimeException {

        private static final long serialVersionUID = 6926190469964122370L;

        public UserError(String message) {
            super(message);
        }

        UserError(String message, Throwable cause) {
            super(message, cause);
        }
    }
}


package liquibase.util;

public class SpringBootFatJar {

    public static String getPathForResource(String path) {
        String[] components = path.split("!");
        if (components.length == 3) {
            if (components[1].endsWith(".jar")){
                return components[1].substring(1);
            }
            return components[1].substring(1) + components[2];
        } else if (components.length == 2) {
            return components[1].substring(1);
        }
        return path;
    }

    /**
     * Method used to simplify an entryName
     *
     * Ex: with path jar:/some/jar.jar!/BOOT-INF/classes!/db/changelog and entryName /BOOT-INF/classes/db/changelog
     * The simple entry name for Spring is db/changelog
     * (/BOOT-INF/classes/ is not needed and break the liquibase alphabetical sort order)
     *
     * @param entryName the entryName to simplify
     * @param path file path
     * @return the simple path
     */
    public static String getSimplePathForResources(String entryName, String path) {
        String[] components = path.split("!");
        if (components.length == 3) {
            if (components[1].endsWith(".jar")) {
                return components[2].substring(1);
            } else {
                return entryName.replaceFirst(components[1], "").substring(1);
            }
        }
        return entryName;
    }
}


package liquibase.changelog;

import liquibase.change.CheckSum;
import liquibase.changelog.filter.ChangeSetFilter;
import liquibase.changelog.filter.ChangeSetFilterResult;

import java.util.Date;
import java.util.Set;

/**
 * Contains the current status of a ChangeSet. Normally returned by {@link liquibase.changelog.visitor.StatusVisitor}.
 * Contains information on whether the changeSet has ran before and will run next time.
 */
public class ChangeSetStatus {

    private final ChangeSet changeSet;
    private final CheckSum currentCheckSum;
    private String description;
    private String comments;

    private boolean willRun;
    private Set<ChangeSetFilterResult> filterResults;

    private CheckSum storedCheckSum;
    private Date dateLastExecuted;
    private boolean previouslyRan;

    private RanChangeSet ranChangeSet;

    public ChangeSetStatus(ChangeSet changeSet) {
        this.changeSet = changeSet;

        this.currentCheckSum = changeSet.generateCheckSum();
        this.description = changeSet.getDescription();
        this.comments = changeSet.getComments();
    }

    public ChangeSet getChangeSet() {
        return changeSet;
    }

    public CheckSum getCurrentCheckSum() {
        return currentCheckSum;
    }

    /**
     * ChangeSet description
     */
    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    /**
     * ChangeSet comments
     */
    public String getComments() {
        return comments;
    }

    public void setComments(String comments) {
        this.comments = comments;
    }

    /**
     * Will the change set run next time.
     */
    public boolean getWillRun() {
        return willRun;
    }

    public void setWillRun(boolean willRun) {
        this.willRun = willRun;
    }

    /**
     * Reasons the change set will or will not run next time. Returns empty set if no reasons were given
     */
    public Set<ChangeSetFilterResult> getFilterResults() {
        return filterResults;
    }

    public void setFilterResults(Set<ChangeSetFilterResult> filterResults) {
        this.filterResults = filterResults;
    }

    /**
     * Convenience method to check wither a given ChangeSetFilter type is a reason for running the change set or not.
     */
    public boolean isFilteredBy(Class<? extends ChangeSetFilter> filterType) {
        if (!willRun) {
            return false;
        }

        if (filterResults == null) {
            return false;
        }

        for (ChangeSetFilterResult result : filterResults) {
            if (result.getFilter().equals(filterType)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Return the checksum stored from the last execution of the change set. Returns null if it has not ran before
     */
    public CheckSum getStoredCheckSum() {
        return storedCheckSum;
    }

    public void setStoredCheckSum(CheckSum storedCheckSum) {
        this.storedCheckSum = storedCheckSum;
    }

    /**
     * Return the date the change set was last executed. Returns null if it has not ran before
     */
    public Date getDateLastExecuted() {
        return dateLastExecuted;
    }

    public void setDateLastExecuted(Date dateLastExecuted) {
        this.dateLastExecuted = dateLastExecuted;
    }

    /**
     * Returns true if the change set was ran previously.
     */
    public boolean getPreviouslyRan() {
        return previouslyRan;
    }

    public void setPreviouslyRan(boolean previouslyRan) {
        this.previouslyRan = previouslyRan;
    }

    public RanChangeSet getRanChangeSet() {
        return ranChangeSet;
    }

    public void setRanChangeSet(RanChangeSet ranChangeSet) {
        this.ranChangeSet = ranChangeSet;
    }
}


package liquibase.util;

import liquibase.database.Database;
import liquibase.exception.DatabaseException;
import liquibase.structure.core.Column;

import java.sql.*;
import java.util.Collection;

public abstract class JdbcUtils {

    /**
     * Constant that indicates an unknown (or unspecified) SQL type.
     *
     * @see java.sql.Types
     */
    public static final int TYPE_UNKNOWN = Integer.MIN_VALUE;

    /**
     * Close the given JDBC Statement and ignore any thrown exception.
     * This is useful for typical finally blocks in manual JDBC code.
     *
     * @param stmt the JDBC Statement to close (may be null)
     */
    public static void closeStatement(Statement stmt) {
        if (stmt != null) {
            try {
                stmt.close();
            } catch (Throwable ex) {
                // We don't trust the JDBC driver: It might throw RuntimeException or Error.
//                logger.debug("Unexpected exception on closing JDBC Statement", ex);
            }
        }
    }

    /**
     * Close the given JDBC ResultSet and ignore any thrown exception.
     * This is useful for typical finally blocks in manual JDBC code.
     *
     * @param rs the JDBC ResultSet to close (may be null)
     */
    public static void closeResultSet(ResultSet rs) {
        if (rs != null) {
            try {
                rs.close();
            } catch (Throwable ex) {
                // We don't trust the JDBC driver: It might throw RuntimeException or Error.
//                logger.debug("Unexpected exception on closing JDBC ResultSet", ex);
            }
        }
    }

    public static void close(ResultSet rs, Statement stmt) {
        closeResultSet(rs);
        closeStatement(stmt);
    }

    /**
     * Retrieve a JDBC column value from a ResultSet, using the most appropriate
     * value type. The returned value should be a detached value object, not having
     * any ties to the active ResultSet: in particular, it should not be a Blob or
     * Clob object but rather a byte array respectively String representation.
     *
Uses the getObject(index) method, but includes additional "hacks"
     * to get around Oracle 10g returning a non-standard object for its TIMESTAMP
     * datatype and a java.sql.Date for DATE columns leaving out the
     * time portion: These columns will explicitly be extracted as standard
     * java.sql.Timestamp object.
     *
     * @param rs    is the ResultSet holding the data
     * @param index is the column index
     * @return the value object
     * @throws SQLException if thrown by the JDBC API
     * @see java.sql.Blob
     * @see java.sql.Clob
     * @see java.sql.Timestamp
     */
    public static Object getResultSetValue(ResultSet rs, int index) throws SQLException {
        Object obj = null;
        try {
            obj = rs.getObject(index);
        } catch (SQLException e) {
            if ("The conversion from char to SMALLINT is unsupported.".equals(e.getMessage())) {
                //issue with sqlserver jdbc 3.0 http://social.msdn.microsoft.com/Forums/sqlserver/en-US/2c908b45-6f75-484a-a891-5e8206f8844f/conversion-error-in-the-jdbc-30-driver-when-accessing-metadata
                obj = rs.getString(index);
            } else {
                throw e;
            }
        }
        if (obj instanceof Blob) {
            obj = rs.getBytes(index);
        } else if (obj instanceof Clob) {
            obj = rs.getString(index);
        } else if ((obj != null) && obj.getClass().getName().startsWith("oracle.sql.TIMESTAMP")) {
            obj = rs.getTimestamp(index);
        } else if ((obj != null) && obj.getClass().getName().startsWith("oracle.sql.DATE")) {
            String metaDataClassName = rs.getMetaData().getColumnClassName(index);
            if ("java.sql.Timestamp".equals(metaDataClassName) ||
                    "oracle.sql.TIMESTAMP".equals(metaDataClassName)) {
                obj = rs.getTimestamp(index);
            } else {
                obj = rs.getDate(index);
            }
        } else if ((obj != null) && (obj instanceof Date)) {
            if ("java.sql.Timestamp".equals(rs.getMetaData().getColumnClassName(index))) {
                obj = rs.getTimestamp(index);
            }
        }
        return obj;
    }

    /**
     * Check whether the given SQL type is numeric.
     *
     * @param sqlType the SQL type to be checked
     * @return whether the type is numeric
     */
    public static boolean isNumeric(int sqlType) {
        return (Types.BIT == sqlType) || (Types.BIGINT == sqlType) || (Types.DECIMAL == sqlType) || (Types.DOUBLE ==
            sqlType) || (Types.FLOAT == sqlType) || (Types.INTEGER == sqlType) || (Types.NUMERIC == sqlType) ||
            (Types.REAL == sqlType) || (Types.SMALLINT == sqlType) || (Types.TINYINT == sqlType);
    }

    /**
     * Return a single result object from the given Collection.
     *
Throws an exception if 0 or more than 1 element found.
     * @param results the result Collection (can be null)
     * @return the single result object
     */
    public static Object requiredSingleResult(Collection results) throws DatabaseException {
        int size = ((results != null) ? results.size() : 0);
        if (size == 0) {
            throw new DatabaseException("Empty result set, expected one row");
        }
        if (results.size() > 1) {
            throw new DatabaseException("Result set larger than one row");
        }
        return results.iterator().next();
    }

    /**
     * Checks whether a result set has a column matching the specified column name.
     * The column name is first changed to match the database format.
     * E.g. an unquoted columnName in h2 will be converted to uppercase so the column name that is
     * being checked for is really the uppercase version of the column name,
     *
     * @param rs result set to check
     * @param columnNameToCheck column name to check
     * @return The value if found, null if not found
     */
    public static String getValueForColumn(ResultSet rs, String columnNameToCheck, Database database) throws SQLException {
        ResultSetMetaData metadata = rs.getMetaData();
        int numberOfColumns = metadata.getColumnCount();
        String correctedColumnName = database.correctObjectName(columnNameToCheck, Column.class);
        // get the column names; column indexes start from 1
        for (int i = 1; i < (numberOfColumns + 1); i++) {
            String columnName = metadata.getColumnLabel(i);
            // Get the name of the column's table name
            if (correctedColumnName.equalsIgnoreCase(columnName)) {
                return rs.getString(columnName);
            }
        }
        return null;
    }

}


package liquibase.configuration;

import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.util.StringUtil;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * Provides unified management of configuration properties within Liquibase core and in extensions.
 *

 * This class is the top level container used to access {@link ConfigurationContainer} implementations which contain
 * the actual configuration properties.
 * Normal use is to call
 * LiquibaseConfiguration.getInstance().getConfiguration(NEEDED_CONFIGURATION.class).getYOUR_PROPERTY()
 *

 * This class is implemented as a singleton with a single global set of configuration objects, but the
 * {@link #setInstance(LiquibaseConfiguration)} method can be used to replace
 * the singleton with an alternate implementation that uses ThreadLocal objects or any other way of managing
 * configurations.
 */
public class LiquibaseConfiguration {

    private Map<Class, ConfigurationContainer> configurations;

    private ConfigurationValueProvider[] configurationValueProviders;

    private static LiquibaseConfiguration instance;

    /**
     * Returns the singleton instance, creating it if necessary. On creation, the configuration is initialized with {@link liquibase.configuration.SystemPropertyProvider}
     */
    public static synchronized LiquibaseConfiguration getInstance() {
        if (instance == null) {
            instance = new LiquibaseConfiguration();
            instance.init(new SystemPropertyProvider());
        }

        return instance;
    }

    /**
     * Overrides the standard singleton instance created by getInstance().
     * Useful for alternate implementations with more complex AbstractConfigurationContainer lookup logic such
     * as different configurations per thread.
     */
    public static synchronized void setInstance(LiquibaseConfiguration instance) {
        LiquibaseConfiguration.instance = instance;
    }

    /**
     * Constructor protected to prevent construction outside getInstance()
     */
    protected LiquibaseConfiguration() {
    }

    /**
     * Re-initialize the configuration with the given ConfigurationProviders. Any existing
     * AbstractConfigurationContainer instances are reset to defaults.
     */
    public void init(ConfigurationValueProvider... configurationValueProviders) {
        if (configurationValueProviders == null) {
            configurationValueProviders = new ConfigurationValueProvider[0];
        }
        this.configurationValueProviders = configurationValueProviders;

        this.reset();
    }

    /**
     * Resets existing AbstractConfigurationContainer instances to their default values.
     */
    public void reset() {
        this.configurations = new HashMap<>();
    }

    /**
     * Return an instance of the passed AbstractConfigurationContainer type.
     * The same instance is returned from every call to getConfiguration()
     */
    public <T extends ConfigurationContainer> T getConfiguration(Class<T> type) {
        if (!configurations.containsKey(type)) {
            configurations.put(type, createConfiguration(type));
        }

        return (T) configurations.get(type);
    }

    public ConfigurationContainer getConfiguration(String typeName) {
        for (Map.Entry<Class, ConfigurationContainer> entry : configurations.entrySet()) {
            if (entry.getKey().getName().equals(typeName)) {
                return entry.getValue();
            }
        }
        try {
            Class typeClass = Class.forName(typeName);
            configurations.put(typeClass, createConfiguration(typeClass));
            return configurations.get(typeClass);
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    /**
     * Convenience method for liquibaseConfiguration.getConfiguration(type).getProperty(property)
     */
    public ConfigurationProperty getProperty(Class<? extends ConfigurationContainer> type, String property) {
        ConfigurationContainer configuration = getConfiguration(type);
        return configuration.getProperty(property);
    }

    protected  <T extends ConfigurationContainer> T createConfiguration(Class<T> type) {
        try {
            T configuration = type.getConstructor().newInstance();
            configuration.init(configurationValueProviders);
            return configuration;
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException("Cannot create default configuration "+type.getName(), e);
        }
    }

    /**
     * Convenience method for {@link #describeValueLookupLogic(ConfigurationProperty)}
     */
    public String describeValueLookupLogic(Class<? extends ConfigurationContainer> config, String property) {
        return describeValueLookupLogic(getProperty(config, property));
    }

    /**
     * Generates a human consumable description of how the configured ConfigurationValueProvider(s) will
     * attempt to set a default value.
     */
    public String describeValueLookupLogic(ConfigurationProperty property) {
        List<String> reasons = new ArrayList<>();
        for (ConfigurationValueProvider container : configurationValueProviders) {
            reasons.add(container.describeValueLookupLogic(property));
        }

        return StringUtil.join(reasons, " AND ");
    }
}


package liquibase.integration.commandline;

import liquibase.CatalogAndSchema;
import liquibase.command.CommandExecutionException;
import liquibase.command.CommandFactory;
import liquibase.command.core.DiffCommand;
import liquibase.command.core.DiffToChangeLogCommand;
import liquibase.command.core.GenerateChangeLogCommand;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.OfflineConnection;
import liquibase.database.core.*;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.output.DiffOutputControl;
import liquibase.diff.output.ObjectChangeFilter;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.ExecutorService;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.core.Schema;
import liquibase.util.StringUtil;

import javax.xml.parsers.ParserConfigurationException;
import java.io.IOException;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.ResourceBundle;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

import static java.util.ResourceBundle.getBundle;

/**
 * Common Utility methods used in the CommandLine application and the Maven plugin.
 * These methods were originally moved from {@link Main} so they could be shared.
 *
 * @author Peter Murray
 */
public class CommandLineUtils {
    private static ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");

    /**
     * @deprecated Use ResourceAccessor version
     */
    public static Database createDatabaseObject(ClassLoader classLoader,
                                                String url,
                                                String username,
                                                String password,
                                                String driver,
                                                String defaultCatalogName,
                                                String defaultSchemaName,
                                                boolean outputDefaultCatalog,
                                                boolean outputDefaultSchema,
                                                String databaseClass,
                                                String driverPropertiesFile,
                                                String propertyProviderClass,
                                                String liquibaseCatalogName,
                                                String liquibaseSchemaName,
                                                String databaseChangeLogTableName,
                                                String databaseChangeLogLockTableName) throws DatabaseException {

        return createDatabaseObject(new ClassLoaderResourceAccessor(classLoader), url, username, password, driver,
                defaultCatalogName, defaultSchemaName, outputDefaultCatalog, outputDefaultSchema, databaseClass,
                driverPropertiesFile, propertyProviderClass, liquibaseCatalogName, liquibaseSchemaName,
                databaseChangeLogTableName, databaseChangeLogLockTableName);
    }

    public static Database createDatabaseObject(ResourceAccessor resourceAccessor,
                                                String url,
                                                String username,
                                                String password,
                                                String driver,
                                                String defaultCatalogName,
                                                String defaultSchemaName,
                                                boolean outputDefaultCatalog,
                                                boolean outputDefaultSchema,
                                                String databaseClass,
                                                String driverPropertiesFile,
                                                String propertyProviderClass,
                                                String liquibaseCatalogName,
                                                String liquibaseSchemaName,
                                                String databaseChangeLogTableName,
                                                String databaseChangeLogLockTableName) throws DatabaseException {
        try {
            liquibaseCatalogName = StringUtil.trimToNull(liquibaseCatalogName);
            liquibaseSchemaName = StringUtil.trimToNull(liquibaseSchemaName);
            defaultCatalogName = StringUtil.trimToNull(defaultCatalogName);
            defaultSchemaName = StringUtil.trimToNull(defaultSchemaName);
            databaseChangeLogTableName = StringUtil.trimToNull(databaseChangeLogTableName);
            databaseChangeLogLockTableName = StringUtil.trimToNull(databaseChangeLogLockTableName);

            Database database = DatabaseFactory.getInstance().openDatabase(url, username, password, driver,
                    databaseClass, driverPropertiesFile, propertyProviderClass, resourceAccessor);

            if (!database.supportsSchemas()) {
                if ((defaultSchemaName != null) && (defaultCatalogName == null)) {
                    defaultCatalogName = defaultSchemaName;
                }
                if ((liquibaseSchemaName != null) && (liquibaseCatalogName == null)) {
                    liquibaseCatalogName = liquibaseSchemaName;
                }
            }

            defaultCatalogName = StringUtil.trimToNull(defaultCatalogName);
            defaultSchemaName = StringUtil.trimToNull(defaultSchemaName);

            database.setDefaultCatalogName(defaultCatalogName);
            database.setDefaultSchemaName(defaultSchemaName);
            database.setOutputDefaultCatalog(outputDefaultCatalog);
            database.setOutputDefaultSchema(outputDefaultSchema);
            database.setLiquibaseCatalogName(liquibaseCatalogName);
            database.setLiquibaseSchemaName(liquibaseSchemaName);
            if (databaseChangeLogTableName != null) {
                database.setDatabaseChangeLogTableName(databaseChangeLogTableName);
                if (databaseChangeLogLockTableName != null) {
                    database.setDatabaseChangeLogLockTableName(databaseChangeLogLockTableName);
                } else {
                    database.setDatabaseChangeLogLockTableName(databaseChangeLogTableName + "LOCK");
                }
            }

            //Todo: move to database object methods in 4.0
            initializeDatabase(username, defaultCatalogName, defaultSchemaName, database);

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

    /**
     * Executes RawSqlStatements particular to each database engine to set the default schema for the given Database
     *
     * @param username           The username used for the connection. Used with MSSQL databases
     * @param defaultCatalogName Catalog name and schema name are similar concepts.
     *                           Used if defaultCatalogName is null.
     * @param defaultSchemaName  Catalog name and schema name are similar concepts.
     *                           Catalog is used with Oracle, DB2 and MySQL, and takes
     *                           precedence over the schema name.
     * @param database           Which Database object is affected by the initialization.
     * @throws DatabaseException
     */
    public static void initializeDatabase(String username, String defaultCatalogName, String defaultSchemaName,
                                          Database database) throws DatabaseException {
        if (((defaultCatalogName != null) || (defaultSchemaName != null)) && !(database.getConnection() instanceof
            OfflineConnection)) {
            if (database instanceof OracleDatabase) {
                String schema = defaultCatalogName;
                if (schema == null) {
                    schema = defaultSchemaName;
                }
                ExecutorService.getInstance().getExecutor(database).execute(
                    new RawSqlStatement("ALTER SESSION SET CURRENT_SCHEMA=" +
                        database.escapeObjectName(schema, Schema.class)));
            } else if (database instanceof PostgresDatabase && defaultSchemaName != null) {
                    ExecutorService.getInstance().getExecutor(database).execute(new RawSqlStatement("SET SEARCH_PATH TO " + database.escapeObjectName(defaultSchemaName, Schema.class)));
            } else if (database instanceof AbstractDb2Database) {
                String schema = defaultCatalogName;
                if (schema == null) {
                    schema = defaultSchemaName;
                }
                ExecutorService.getInstance().getExecutor(database).execute(new RawSqlStatement("SET CURRENT SCHEMA "
                        + schema));
            } else if (database instanceof MySQLDatabase) {
                String schema = defaultCatalogName;
                if (schema == null) {
                    schema = defaultSchemaName;
                }
                ExecutorService.getInstance().getExecutor(database).execute(new RawSqlStatement("USE " + schema));
            }

        }
    }

    public static void doDiff(Database referenceDatabase, Database targetDatabase, String snapshotTypes) throws
            LiquibaseException {
        doDiff(referenceDatabase, targetDatabase, snapshotTypes, null);
    }

    public static void doDiff(Database referenceDatabase, Database targetDatabase, String snapshotTypes,
                              CompareControl.SchemaComparison[] schemaComparisons) throws LiquibaseException {
        DiffCommand diffCommand = (DiffCommand) CommandFactory.getInstance().getCommand("diff");

        diffCommand
                .setReferenceDatabase(referenceDatabase)
                .setTargetDatabase(targetDatabase)
                .setCompareControl(new CompareControl(schemaComparisons, snapshotTypes))
                .setSnapshotTypes(snapshotTypes)
                .setOutputStream(System.out);

        System.out.println("");
        System.out.println(coreBundle.getString("diff.results"));
        try {
            diffCommand.execute();
        } catch (CommandExecutionException e) {
            throw new LiquibaseException(e);
        }
    }

    public static void doDiffToChangeLog(String changeLogFile,
                                         Database referenceDatabase,
                                         Database targetDatabase,
                                         DiffOutputControl diffOutputControl,
                                         ObjectChangeFilter objectChangeFilter,
                                         String snapshotTypes)
            throws LiquibaseException, IOException, ParserConfigurationException {
        doDiffToChangeLog(changeLogFile, referenceDatabase, targetDatabase, diffOutputControl, objectChangeFilter,
                snapshotTypes, null);
    }

    public static void doDiffToChangeLog(String changeLogFile,
                                         Database referenceDatabase,
                                         Database targetDatabase,
                                         DiffOutputControl diffOutputControl,
                                         ObjectChangeFilter objectChangeFilter,
                                         String snapshotTypes,
                                         CompareControl.SchemaComparison[] schemaComparisons)
            throws LiquibaseException, IOException, ParserConfigurationException {

        DiffToChangeLogCommand command = (DiffToChangeLogCommand) CommandFactory.getInstance().getCommand
                ("diffChangeLog");
        command.setReferenceDatabase(referenceDatabase)
                .setTargetDatabase(targetDatabase)
                .setSnapshotTypes(snapshotTypes)
                .setCompareControl(new CompareControl(schemaComparisons, snapshotTypes))
                .setObjectChangeFilter(objectChangeFilter)
                .setOutputStream(System.out);
        command.setChangeLogFile(changeLogFile)
                .setDiffOutputControl(diffOutputControl);

        try {
            command.execute();
        } catch (CommandExecutionException e) {
            throw new LiquibaseException(e);
        }

    }

    public static void doGenerateChangeLog(String changeLogFile, Database originalDatabase, String catalogName,
                                           String schemaName, String snapshotTypes, String author, String context,
                                           String dataDir, DiffOutputControl diffOutputControl) throws
            IOException, ParserConfigurationException, LiquibaseException {
        doGenerateChangeLog(changeLogFile, originalDatabase, new CatalogAndSchema[]{new CatalogAndSchema(catalogName,
                schemaName)}, snapshotTypes, author, context, dataDir, diffOutputControl);
    }

    public static void doGenerateChangeLog(String changeLogFile, Database originalDatabase, CatalogAndSchema[]
            schemas, String snapshotTypes, String author, String context, String dataDir, DiffOutputControl
                                                   diffOutputControl) throws IOException, ParserConfigurationException,
            LiquibaseException {
        CompareControl.SchemaComparison[] comparisons = new CompareControl.SchemaComparison[schemas.length];
        int i = 0;
        for (CatalogAndSchema schema : schemas) {
            comparisons[i++] = new CompareControl.SchemaComparison(schema, schema);
        }
        CompareControl compareControl = new CompareControl(comparisons, snapshotTypes);
        diffOutputControl.setDataDir(dataDir);

        GenerateChangeLogCommand command = (GenerateChangeLogCommand) CommandFactory.getInstance().getCommand
                ("generateChangeLog");

        command.setReferenceDatabase(originalDatabase)
                .setSnapshotTypes(snapshotTypes)
                .setOutputStream(System.out)
                .setCompareControl(compareControl);
        command.setChangeLogFile(changeLogFile)
                .setDiffOutputControl(diffOutputControl);
        command.setAuthor(author)
                .setContext(context);

        try {
            command.execute();
        } catch (CommandExecutionException e) {
            throw new LiquibaseException(e);
        }

    }

    public static String getBanner() {
        String myVersion = "";
        String buildTimeString = "";
        Calendar calendar = Calendar.getInstance();
        SimpleDateFormat dateFormat = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss z");

        Class clazz = CommandLineUtils.class;
        String className = clazz.getSimpleName() + ".class";
        String classPath = clazz.getResource(className).toString();
        if (classPath.startsWith("jar")) {
            String manifestPath = classPath.substring(0, classPath.lastIndexOf("!") + 1) +
                    "/META-INF/MANIFEST.MF";
            Manifest manifest = null;
            try {
                manifest = new Manifest(new URL(manifestPath).openStream());
            } catch (IOException e) {
                throw new UnexpectedLiquibaseException("Cannot open a URL to the manifest of our own JAR file.");
            }
            Attributes attr = manifest.getMainAttributes();
            myVersion = attr.getValue("Bundle-Version");
            buildTimeString = attr.getValue("Build-Time");
        }
        StringBuffer banner = new StringBuffer();

        banner.append(String.format(
            coreBundle.getString("starting.liquibase.at.timestamp"), dateFormat.format(calendar.getTime())
        ));
        if (StringUtil.isNotEmpty(myVersion) && StringUtil.isNotEmpty(buildTimeString)) {
            banner.append(String.format(coreBundle.getString("liquibase.version.builddate"), myVersion,
                buildTimeString));
        }
        return banner.toString();
    }

}


package liquibase.diff.compare.core;

import liquibase.database.Database;
import liquibase.diff.ObjectDifferences;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectComparator;
import liquibase.diff.compare.DatabaseObjectComparatorChain;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Column;
import liquibase.structure.core.DataType;

import java.util.Collection;
import java.util.HashSet;
import java.util.Set;

public final class DefaultDatabaseObjectComparator implements DatabaseObjectComparator {
    @Override
    public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
        return PRIORITY_DEFAULT;
    }

    @Override
    public String[] hash(DatabaseObject databaseObject, Database accordingTo, DatabaseObjectComparatorChain chain) {
        String name = databaseObject.getName();
        if (name == null) {
            name = "null";
        }
        return new String[] {name.toLowerCase()};
    }

    @Override
    public boolean isSameObject(DatabaseObject databaseObject1, DatabaseObject databaseObject2, Database accordingTo, DatabaseObjectComparatorChain chain) {
        if ((databaseObject1.getSchema() != null) && (databaseObject2.getSchema() != null) &&
            !DatabaseObjectComparatorFactory.getInstance().isSameObject(databaseObject1.getSchema(),
                databaseObject2.getSchema(), chain.getSchemaComparisons(), accordingTo)) {
            return false;
        }

        if (databaseObject1.getClass().isAssignableFrom(databaseObject2.getClass()) || databaseObject2.getClass().isAssignableFrom(databaseObject1.getClass())) {
            return nameMatches(databaseObject1, databaseObject2, accordingTo);
        }
        return false;

    }

    @Override
    public ObjectDifferences findDifferences(DatabaseObject databaseObject1, DatabaseObject databaseObject2, Database accordingTo, CompareControl compareControl, DatabaseObjectComparatorChain chain, Set<String> exclude) {

        Set<String> attributes = new HashSet<>();
        attributes.addAll(databaseObject1.getAttributes());
        attributes.addAll(databaseObject2.getAttributes());

        ObjectDifferences differences = new ObjectDifferences(compareControl);

        exclude.add("schema");
        exclude.add("catalog");

        for (String attribute : attributes) {
            if (exclude.contains(attribute)) {
                continue;
            }
            Object attribute1 = databaseObject1.getAttribute(attribute, Object.class);
            Object attribute2 = databaseObject2.getAttribute(attribute, Object.class);

            attribute1 = undoCollection(attribute1, attribute2);
            attribute2 = undoCollection(attribute2, attribute1);

            ObjectDifferences.CompareFunction compareFunction;
            if ((attribute1 instanceof DatabaseObject) || (attribute2 instanceof DatabaseObject)) {
                Class<? extends DatabaseObject> type;
                if (attribute1 != null) {
                    type = (Class<? extends DatabaseObject>) attribute1.getClass();
                } else {
                    type = (Class<? extends DatabaseObject>) attribute2.getClass();
                }
                compareFunction = new ObjectDifferences.DatabaseObjectNameCompareFunction(type, accordingTo);
            } else if ((attribute1 instanceof DataType) || (attribute2 instanceof DataType)) {
                compareFunction = new ObjectDifferences.ToStringCompareFunction(false);
            } else if ((attribute1 instanceof Column.AutoIncrementInformation) || (attribute2 instanceof Column
                .AutoIncrementInformation)) {
                compareFunction = new ObjectDifferences.ToStringCompareFunction(false);
            } else if ((attribute1 instanceof Collection) || (attribute2 instanceof Collection)) {
                compareFunction = new ObjectDifferences.OrderedCollectionCompareFunction(new ObjectDifferences.StandardCompareFunction(chain.getSchemaComparisons(), accordingTo));
            } else {
                compareFunction = new ObjectDifferences.StandardCompareFunction(chain.getSchemaComparisons(), accordingTo);
            }
            differences.compare(attribute, databaseObject1, databaseObject2, compareFunction);

        }

        return differences;
    }

    /**
     * Sometimes an attribute in one object is a single-entity collection and on the other it is just the object.
     * Check the passed potentialCollection and if it is a single-entry collection of the same type as the otherObject, return just the collection element.
     * Otherwise, return the original collection.
     */
    protected Object undoCollection(Object potentialCollection, Object otherObject) {
        if ((potentialCollection != null) && (otherObject != null) && (potentialCollection instanceof Collection) &&
            !(otherObject instanceof Collection)) {
            if ((((Collection) potentialCollection).size() == 1) && ((Collection) potentialCollection).iterator()
                .next().getClass().equals(otherObject.getClass())) {
                potentialCollection = ((Collection) potentialCollection).iterator().next();
            }
        }
        return potentialCollection;
    }

    //Static so it can be used in other comparators if needed
    public static boolean nameMatches(DatabaseObject databaseObject1, DatabaseObject databaseObject2, Database accordingTo) {
        String object1Name = accordingTo.correctObjectName(databaseObject1.getName(), databaseObject1.getClass());
        String object2Name = accordingTo.correctObjectName(databaseObject2.getName(), databaseObject2.getClass());

        if ((object1Name == null) && (object2Name == null)) {
            return true;
        }
        if ((object1Name == null) || (object2Name == null)) {
            return false;
        }
        if (accordingTo.isCaseSensitive()) {
            return object1Name.equals(object2Name);
        } else {
            return object1Name.equalsIgnoreCase(object2Name);
        }
    }

}


package liquibase.resource;

import liquibase.Scope;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.Logger;
import liquibase.util.CollectionUtil;

import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.FileSystems;
import java.nio.file.Path;
import java.util.*;
import java.util.jar.Manifest;

/**
 * An implementation of {@link FileSystemResourceAccessor} that builds up the file roots based on the passed {@link ClassLoader}.
 * If you are using a ClassLoader that isn't based on local files, you will need to use a different {@link ResourceAccessor} implementation.
 *
 * @see OSGiResourceAccessor for OSGi-based classloaders
 */
public class ClassLoaderResourceAccessor extends FileSystemResourceAccessor {

    public ClassLoaderResourceAccessor(ClassLoader... classLoaders) {
        try {
            for (ClassLoader classLoader : CollectionUtil.createIfNull(classLoaders)) {
                for (Path path : findRootPaths(classLoader)) {
                    addRootPath(path);
                }
                Enumeration<URL> manifests = classLoader.getResources("META-INF/MANIFEST.MF");
                while (manifests.hasMoreElements()) {
                    URL manifestUrl = manifests.nextElement();

                    File jarFile = new File(manifestUrl.getFile()
                            .replaceFirst("^jar:", "")
                            .replaceFirst("^file:/", "")
                            .replaceFirst("!.*", ""));

                    Manifest manifest = new Manifest(manifestUrl.openStream());
                    String classpath = manifest.getMainAttributes().getValue("Class-Path");
                    if (classpath != null) {
                        for (String entry : classpath.split("\\s+")) {
                            entry = entry.replaceFirst("file:", "");
                            File entryFile = new File(entry);

                            if (entryFile.exists()) {
                                addRootPath(entryFile.toPath());
                            } else {
                                Scope.getCurrentScope().getLog(getClass()).info("Missing file " + entryFile.getAbsolutePath() + " referenced in " + jarFile.getAbsolutePath() + "!/META-INF/MANIFEST.MF");
                            }
                        }

                    }
                }
            }
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    /**
     * Called by constructor to create all the root paths from the given classloader.
     * Works best if the passed classLoader is a {@link URLClassLoader} because it can get the root file/dirs directly.
     * If it is not a URLClassLoader, it still attempts to find files by looking up base packages and MANIFEST.MF files.
     * This may miss some roots, however, and so if you are not using a URLClassLoader consider using a custom subclass.
     */
    protected List<Path> findRootPaths(ClassLoader classLoader) throws Exception {
        if (classLoader == null) {
            return new ArrayList<>();

        }

        Logger logger = Scope.getCurrentScope().getLog(getClass());

        List<URL> returnUrls = new ArrayList<>();
        if (classLoader instanceof URLClassLoader) {
            returnUrls.addAll(CollectionUtil.createIfNull(Arrays.asList(((URLClassLoader) classLoader).getURLs())));
        } else {
            logger.info("Found non-URL ClassLoader classloader " + classLoader.toString() + ". Liquibase will try to figure out now to load resources from it, some may be missed. Consider using a custom ClassLoaderResourceAccessor subclass.");
            Set<String> seenUrls = new HashSet<>(); //need to track externalForm as a string to avoid java problem with URL.equals making a network call

            Enumeration<URL> emptyPathUrls = classLoader.getResources("");
            while (emptyPathUrls.hasMoreElements()) {
                URL url = emptyPathUrls.nextElement();
                if (seenUrls.add(url.toExternalForm())) {
                    returnUrls.add(url);
                }
            }

            Enumeration<URL> metaInfoPathUrls = classLoader.getResources("META-INF");
            while (metaInfoPathUrls.hasMoreElements()) {
                String originalUrl = metaInfoPathUrls.nextElement().toExternalForm();
                String finalUrl = originalUrl.replaceFirst("/META-INF", "");
                if (finalUrl.startsWith("jar:")) {
                    if (finalUrl.endsWith("!")) {
                        finalUrl = finalUrl.replaceFirst("^jar:", "").replaceFirst("!$", "");
                    } else {
                        logger.warning("ClassLoader URL " + finalUrl + " starts with jar: but does not end with !, don't knnow how to handle it. Skipping");
                        continue;
                    }
                }

                URL finalUrlObj = new URL(finalUrl);
                if (seenUrls.add(finalUrlObj.toExternalForm())) {
                    returnUrls.add(finalUrlObj);
                }
            }

        }

        List<Path> returnPaths = new ArrayList<>();
        for (URL url : returnUrls) {
            Path path = rootUrlToPath(url);
            if (path != null) {
                logger.fine("Found classloader root at " + path.toString());
                returnPaths.add(path);
            }
        }
        return returnPaths;
    }

    /**
     * Converts the given URL to a Path. Used by {@link #findRootPaths(ClassLoader)} to convert classloader URLs to Paths to pass to {@link #addRootPath(Path)}.
     *
     * @return null if url cannot or should not be converted to a Path.
     */
    protected Path rootUrlToPath(URL url) {
        String protocol = url.getProtocol();
        switch (protocol) {
            case "file":
                return FileSystems.getDefault().getPath(url.getFile().replace("%20", " ").replaceFirst("/(\\w)\\:/", "$1:/"));
            default:
                Scope.getCurrentScope().getLog(getClass()).warning("Unknown protocol '" + protocol + "' for ClassLoaderResourceAccessor on " + url.toExternalForm());
                return null;
        }
    }
}