package liquibase.snapshot;

import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.diff.compare.CompareControl;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.parser.core.ParsedNode;
import liquibase.parser.core.ParsedNodeException;
import liquibase.resource.ResourceAccessor;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.structure.DatabaseObject;
import liquibase.structure.DatabaseObjectCollection;
import liquibase.structure.core.*;
import liquibase.util.ISODateFormat;
import liquibase.util.ObjectUtil;
import liquibase.util.StringUtil;

import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.CopyOnWriteArrayList;

public abstract class DatabaseSnapshot implements LiquibaseSerializable {

    private static final Logger LOGGER = Scope.getCurrentScope().getLog(DatabaseSnapshot.class);
    public static final String ALL_CATALOGS_STRING_SCRATCH_KEY = "DatabaseSnapshot.allCatalogsString";

    private final DatabaseObject[] originalExamples;
    private HashSet<String> serializableFields;
    private SnapshotControl snapshotControl;
    private Database database;
    private DatabaseObjectCollection allFound;
    private DatabaseObjectCollection referencedObjects;
    private Map<Class<? extends DatabaseObject>, Set<DatabaseObject>> knownNull = new HashMap<>();

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

    private Map<String, ResultSetCache> resultSetCaches = new HashMap<>();
    private CompareControl.SchemaComparison[] schemaComparisons;

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

    DatabaseSnapshot(DatabaseObject[] examples, Database database, SnapshotControl snapshotControl) throws DatabaseException, InvalidExampleException {
        this.database = database;
        allFound = new DatabaseObjectCollection(database);
        referencedObjects = new DatabaseObjectCollection(database);
        this.snapshotControl = snapshotControl;

        this.originalExamples = ((examples == null) ? new DatabaseObject[0] : examples);

        init(examples);

        this.serializableFields = new HashSet<>();
        this.serializableFields.add("snapshotControl");
        this.serializableFields.add("objects");
        this.serializableFields.add("referencedObjects");
        this.serializableFields.add("database");
        this.serializableFields.add("created");
        this.serializableFields.add("metadata");
    }

    public DatabaseSnapshot(DatabaseObject[] examples, Database database) throws DatabaseException, InvalidExampleException {
        this(examples, database, new SnapshotControl(database));
    }

    protected void init(DatabaseObject[] examples) throws DatabaseException, InvalidExampleException {
        if (examples != null) {
            Set<Catalog> catalogs = new HashSet<>();
            for (DatabaseObject object : examples) {
                if (object instanceof Schema) {
                    catalogs.add(((Schema) object).getCatalog());
                }
            }

            this.setScratchData("DatabaseSnapshot.allCatalogs", catalogs);

            if (catalogs.size() > 1) {
                List<String> quotedCatalogs = new ArrayList<String>();
                for (Catalog catalog : catalogs) {
                    quotedCatalogs.add("'" + catalog.getName() + "'");
                }
                this.setScratchData(ALL_CATALOGS_STRING_SCRATCH_KEY, StringUtil.join(quotedCatalogs, ", ").toUpperCase());
            }

            if (getDatabase().supportsCatalogs()) {
                for (Catalog catalog : catalogs) {
                    this.snapshotControl.addType(catalog.getClass(), database);
                    include(catalog);
                }
            }
            for (DatabaseObject obj : examples) {
                this.snapshotControl.addType(obj.getClass(), database);

                include(obj);
            }
        }
    }

    /**
     * Searches the current snapshot content for the given examples. Returns a new DatabaseSnapshot
     * containing a clone of every object from the examples array that was found.
     *
     * @param examples The array of snapshot objects to search and clone
     * @return a new DatabaseSnapshot object with the clones of the desired objects. If no object is
     * found, an empty DatabaseSnapshot will be returned.
     */
    public DatabaseSnapshot clone(DatabaseObject[] examples) {
        try {
            DatabaseSnapshot returnSnapshot = new RestoredDatabaseSnapshot(this.database);

            for (DatabaseObject example : examples) {
                DatabaseObject existingObject = this.get(example);
                if (existingObject == null) {
                    continue;
                }
                if (example instanceof Schema) {
                    for (Class<? extends DatabaseObject> type : this.snapshotControl.getTypesToInclude()) {

                        for (DatabaseObject object : this.get(type)) {
                            if (object.getSchema() == null) {
                                if (object instanceof Catalog) {
                                    if (DatabaseObjectComparatorFactory.getInstance().isSameObject(object, ((Schema) example).getCatalog(), null, database)) {
                                        returnSnapshot.allFound.add(object);
                                    }
                                } else {
                                    returnSnapshot.allFound.add(object);
                                }
                            } else {
                                if (DatabaseObjectComparatorFactory.getInstance().isSameObject(object.getSchema(), example, null, database)) {
                                    returnSnapshot.allFound.add(object);
                                } else {
                                    if (object.getClass().getName().contains("Synonym")
                                            && !object.getAttribute("private", false)) {
                                        Schema objectSchema = object.getAttribute("objectSchema", Schema.class);
                                        if (DatabaseObjectComparatorFactory.getInstance().isSameObject(objectSchema, example, null, database)) {
                                            returnSnapshot.allFound.add(object);
                                        }
                                    }
                                }
                            }
                        }
                    }
                } else {
                    returnSnapshot.allFound.add(existingObject);
                }
            }

            returnSnapshot.getMetadata().putAll(this.getMetadata());

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

    public SnapshotControl getSnapshotControl() {
        return snapshotControl;
    }

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

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

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

    @Override
    public Set<String> getSerializableFields() {
        return serializableFields;
    }

    @Override
    public Object getSerializableFieldValue(String field) {
        switch (field) {
            case "snapshotControl":
                return snapshotControl;
            case "objects":
                return allFound;
            case "referencedObjects":
                return referencedObjects;
            case "metadata":
                return metadata;
            case "created":
                return new ISODateFormat().format(new Timestamp(new Date().getTime()));
            case "database":
                Map<String, Object> map = new HashMap<>();
                map.put("shortName", database.getShortName());
                map.put("productName", database.getDatabaseProductName());
                map.put("url", database.getConnection().getURL());
                try {
                    map.put("majorVersion", String.valueOf(database.getDatabaseMajorVersion()));
                    map.put("minorVersion", String.valueOf(database.getDatabaseMinorVersion()));
                    map.put("productVersion", database.getDatabaseProductVersion());
                    map.put("user", database.getConnection().getConnectionUserName());
                } catch (DatabaseException e) {
                    //ok
                }
                return map;
            default:
                throw new UnexpectedLiquibaseException("Unknown field: " + field);
        }
    }

    @Override
    public SerializationType getSerializableFieldType(String field) {
        switch (field) {
            case "snapshotControl":
                return SerializationType.NESTED_OBJECT;
            case "objects":
                return SerializationType.NESTED_OBJECT;
            case "referencedObjects":
                return SerializationType.NESTED_OBJECT;
            default:
                throw new UnexpectedLiquibaseException("Unknown field: " + field);
        }
    }

    public Database getDatabase() {
        return database;
    }

    public ResultSetCache getResultSetCache(String key) {
        if (!resultSetCaches.containsKey(key)) {
            resultSetCaches.put(key, new ResultSetCache());
        }
        return resultSetCaches.get(key);
    }

    /**
     * Include the object described by the passed example object in this snapshot. Returns the object snapshot or null
     * if the object does not exist in the database. If the same object was returned by an earlier include() call,
     * the same object instance will be returned.
     */
    protected <T extends DatabaseObject> T include(T example) throws DatabaseException, InvalidExampleException {
        if (example == null) {
            return null;
        }

        if (database.isSystemObject(example)) {
            return null;
        }

        if ((example instanceof Schema) && (example.getName() == null) && ((((Schema) example).getCatalog() == null)
                || (((Schema) example).getCatalogName() == null))) {
            CatalogAndSchema catalogAndSchema = ((Schema) example).toCatalogAndSchema().customize(database);
            example = (T) new Schema(catalogAndSchema.getCatalogName(), catalogAndSchema.getSchemaName());
        }

        if (!snapshotControl.shouldInclude(example)) {
            LOGGER.fine(LogType.LOG, "Excluding " + example);
            return example;
        }

        T existing = get(example);
        if (existing != null) {
            return existing;
        }
        if (isKnownNull(example)) {
            return null;
        }

        SnapshotListener snapshotListener = snapshotControl.getSnapshotListener();

        SnapshotGeneratorChain chain = createGeneratorChain(example.getClass(), database);
        if (snapshotListener != null) {
            snapshotListener.willSnapshot(example, database);
        }

        T object = chain.snapshot(example, this);

        if (object == null) {
            Set<DatabaseObject> collection = knownNull.get(example.getClass());
            if (collection == null) {
                collection = new HashSet<>();
                knownNull.put(example.getClass(), collection);
            }
            collection.add(example);

            if (example instanceof Schema) {
                if (snapshotControl.isWarnIfObjectNotFound())
                    Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Did not find schema '" + example + "' to snapshot");
            }
            if (example instanceof Catalog) {
                if (snapshotControl.isWarnIfObjectNotFound())
                    Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Did not find catalog '" + example + "' to snapshot");
            }

        } else {
            allFound.add(object);

            try {
                includeNestedObjects(object);
            } catch (ReflectiveOperationException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }

        if (snapshotListener != null) {
            snapshotListener.finishedSnapshot(example, object, database);
        }

        return object;
    }

    private void includeNestedObjects(DatabaseObject object) throws DatabaseException, InvalidExampleException, ReflectiveOperationException {
        for (String field : new HashSet<>(object.getAttributes())) {
            Object fieldValue = object.getAttribute(field, Object.class);
            if ("columns".equals(field) && ((object.getClass() == PrimaryKey.class) || (object.getClass() == Index
                    .class) || (object.getClass() == UniqueConstraint.class))) {
                if ((fieldValue != null) && !((Collection) fieldValue).isEmpty()) {
                    String columnName = ((Column) ((Collection) fieldValue).iterator().next()).getName();
                    if (columnName.endsWith(" ASC") || columnName.endsWith(" DESC") || columnName.endsWith(" RANDOM")) {
                        continue;
                    }
                }
            }
            Object newFieldValue = replaceObject(fieldValue);
            if (newFieldValue == null) { //sometimes an object references a non-snapshotted object. Leave it with the unsnapshotted example
                if (((object instanceof UniqueConstraint) || (object instanceof PrimaryKey) || (object instanceof
                        ForeignKey)) && "backingIndex".equals(field)) { //unless it is the backing index, that is handled a bit strange and we need to handle the case where there is no backing index (disabled PK on oracle)
                    object.setAttribute(field, null);
                }
            } else if (fieldValue != newFieldValue) {
                object.setAttribute(field, newFieldValue);
            }
        }
    }

    private Object replaceObject(Object fieldValue) throws DatabaseException, InvalidExampleException, ReflectiveOperationException {
        if (fieldValue == null) {
            return null;
        }
        if (fieldValue instanceof DatabaseObject) {
            if (((DatabaseObject) fieldValue).getSnapshotId() != null) { //already been replaced
                return fieldValue;
            }

            if (!snapshotControl.shouldInclude(((DatabaseObject) fieldValue).getClass())) {
                return fieldValue;
            }

//            System.out.println("replaceObject "+fieldValue);
            if (!(fieldValue instanceof Catalog) && isWrongSchema(((DatabaseObject) fieldValue))) {
                DatabaseObject savedFieldValue = referencedObjects.get((DatabaseObject) fieldValue, schemaComparisons);
                if (savedFieldValue == null) {
                    savedFieldValue = (DatabaseObject) fieldValue;
                    savedFieldValue.setSnapshotId(SnapshotIdService.getInstance().generateId());
                    includeNestedObjects(savedFieldValue);

                    referencedObjects.add(savedFieldValue);
                }

                return savedFieldValue;
            }
            if ((fieldValue instanceof Catalog) && isWrongCatalog(((DatabaseObject) fieldValue))) {
                DatabaseObject savedFieldValue = referencedObjects.get((DatabaseObject) fieldValue, schemaComparisons);
                if (savedFieldValue == null) {
                    savedFieldValue = (DatabaseObject) fieldValue;
                    savedFieldValue.setSnapshotId(SnapshotIdService.getInstance().generateId());

                    referencedObjects.add(savedFieldValue);
                }

                return savedFieldValue;
            }

            if (((DatabaseObject) fieldValue).getSnapshotId() == null) {
                return include((DatabaseObject) fieldValue);
            } else {
                return fieldValue;
            }
            //            } else if (Set.class.isAssignableFrom(field.getType())) {
            //                field.setAccessible(true);
            //                Set fieldValue = field.get(object);
            //                for (Object val : fieldValue) {
            //
            //                }
        } else if (fieldValue instanceof Collection) {
            Iterator fieldValueIterator = new CopyOnWriteArrayList((Collection) fieldValue).iterator();
            List newValues = new ArrayList();
            while (fieldValueIterator.hasNext()) {
                Object obj = fieldValueIterator.next();
                if ((fieldValue instanceof DatabaseObject) && !snapshotControl.shouldInclude(((DatabaseObject)
                        fieldValue).getClass())) {
                    return fieldValue;
                }

                if ((obj instanceof DatabaseObject) && (((DatabaseObject) obj).getSnapshotId() == null)) {
                    obj = include((DatabaseObject) obj);
                }
                if (obj != null) {
                    newValues.add(obj);
                }
            }
            Collection newCollection = null;
            try {
                Class<?> collectionClass = fieldValue.getClass();
                if (List.class.isAssignableFrom(collectionClass)) {
                    collectionClass = ArrayList.class;
                }
                newCollection = (Collection) collectionClass.getConstructor().newInstance();
            } catch (InstantiationException e) {
                throw e;
            }
            newCollection.addAll(newValues);
            return newCollection;
        } else if (fieldValue instanceof Map) {
            Map newMap = (Map) fieldValue.getClass().getConstructor().newInstance();
            for (Map.Entry entry : new HashSet<>((Set<Map.Entry>) ((Map) fieldValue).entrySet())) {
                Object key = replaceObject(entry.getKey());
                Object value = replaceObject(entry.getValue());

                if (key != null) {
                    newMap.put(key, value);
                }
            }

            return newMap;

        }

        return fieldValue;
    }

    protected boolean isWrongSchema(DatabaseObject fieldValue) {
        boolean isSchemaExamples = true;
        if (originalExamples == null) {
            return false;
        }
        for (DatabaseObject obj : originalExamples) {
            if (!(obj instanceof Schema)) {
                isSchemaExamples = false;
                break;
            }
        }

        if (!isSchemaExamples) {
            return false;
        }

        for (DatabaseObject obj : originalExamples) {
            if (DatabaseObjectComparatorFactory.getInstance().isSameObject(fieldValue.getSchema(), obj, schemaComparisons, database)) {
                return false;
            }
        }
        return true;
    }

    protected boolean isWrongCatalog(DatabaseObject fieldValue) {
        String fieldCatalog;
        if (fieldValue instanceof Catalog) {
            fieldCatalog = fieldValue.getName();
        } else if (fieldValue instanceof Schema) {
            fieldCatalog = ((Schema) fieldValue).getCatalogName();
        } else {
            Schema fieldSchema = fieldValue.getSchema();
            if (fieldSchema == null) {
                return false;
            }
            fieldCatalog = fieldSchema.getCatalogName();
        }
        if (fieldCatalog == null) {
            return false;
        }

        Set<String> catalogNames = new HashSet<>();
        for (DatabaseObject obj : originalExamples) {
            String catalogName = null;
            if (obj instanceof Schema) {
                catalogName = ((Schema) obj).getCatalogName();
            } else if (obj instanceof Catalog) {
                catalogName = obj.getName();
            }
            if (catalogName != null) {
                catalogNames.add(catalogName.toLowerCase());
            }
        }

        return !catalogNames.contains(fieldCatalog.toLowerCase());
    }

    /**
     * Returns the object described by the passed example if it is already included in this snapshot.
     */
    public <DatabaseObjectType extends DatabaseObject> DatabaseObjectType get(DatabaseObjectType example) {
        return allFound.get(example, schemaComparisons);
    }

    /**
     * Returns all objects of the given type that are already included in this snapshot.
     */
    public <DatabaseObjectType extends DatabaseObject> Set<DatabaseObjectType> get(Class<DatabaseObjectType> type) {
        return allFound.get(type);
    }

    protected SnapshotGeneratorChain createGeneratorChain(Class<? extends DatabaseObject> databaseObjectType, Database database) {
        SortedSet<SnapshotGenerator> generators = SnapshotGeneratorFactory.getInstance().getGenerators(databaseObjectType, database);
        if ((generators == null) || generators.isEmpty()) {
            return null;
        }
        //noinspection unchecked
        return new SnapshotGeneratorChain(generators);
    }

    private boolean isKnownNull(DatabaseObject example) {
        Set<DatabaseObject> databaseObjects = knownNull.get(example.getClass());
        if (databaseObjects == null) {
            return false;
        }
        for (DatabaseObject obj : databaseObjects) {
            if (DatabaseObjectComparatorFactory.getInstance().isSameObject(obj, example, schemaComparisons, database)) {
                return true;
            }
        }
        return false;
    }

    @Override
    public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        try {
            Map<String, DatabaseObject> referencedObjects = new HashMap<>();
            Map<String, DatabaseObject> objects = new HashMap<>();
            Map<String, DatabaseObject> allObjects = new HashMap<>();
            ParsedNode databaseNode = parsedNode.getChild(null, "database");
            DatabaseConnection connection = getDatabase().getConnection();
            if ((databaseNode != null) && (connection instanceof OfflineConnection)) {
                ((OfflineConnection) connection).setDatabaseMajorVersion(databaseNode.getChildValue(null, "majorVersion", Integer.class));
                ((OfflineConnection) connection).setDatabaseMinorVersion(databaseNode.getChildValue(null, "minorVersion", Integer.class));
                ((OfflineConnection) connection).setProductVersion(databaseNode.getChildValue(null, "productVersion", String.class));
                ((OfflineConnection) connection).setConnectionUserName(databaseNode.getChildValue(null, "user", String.class));
            }

            loadObjects(referencedObjects, allObjects, parsedNode.getChild(null, "referencedObjects"), resourceAccessor);
            loadObjects(objects, allObjects, parsedNode.getChild(null, "objects"), resourceAccessor);

            for (DatabaseObject object : allObjects.values()) {
                for (String attr : new ArrayList<>(object.getAttributes())) {
                    Object value = object.getAttribute(attr, Object.class);
                    if ((value instanceof String) && allObjects.containsKey(value)) {
                        if (ObjectUtil.hasProperty(object, attr)) {
                            ObjectUtil.setProperty(object, attr, allObjects.get(value));
                        } else {
                            object.setAttribute(attr, allObjects.get(value));
                        }
                    } else if ((value instanceof Collection) && !((Collection) value).isEmpty() && allObjects
                            .containsKey(((Collection) value).iterator().next())) {
                        List newList = new ArrayList();
                        for (String element : (Collection<String>) value) {
                            newList.add(allObjects.get(element));
                        }
                        if (ObjectUtil.hasProperty(object, attr)) {
                            ObjectUtil.setProperty(object, attr, newList);
                        } else {
                            object.setAttribute(attr, newList);
                        }
                    } else {
                        if ((value != null) && ObjectUtil.hasProperty(object, attr)) {
                            if ((value instanceof byte[]) && ObjectUtil.getPropertyType(object, attr).equals(String
                                    .class)) {
                                value = new String((byte[]) value, LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding());
                            }
                            object.setAttribute(attr, null);
                            ObjectUtil.setProperty(object, attr, value);
                        }
                    }
                }
            }

            for (DatabaseObject object : objects.values()) {
                this.allFound.add(object);
            }
            for (DatabaseObject object : referencedObjects.values()) {
                this.referencedObjects.add(object);
            }
        } catch (Exception e) {
            throw new ParsedNodeException(e);
        }
    }

    protected void loadObjects(Map<String, DatabaseObject> objectMap, Map<String, DatabaseObject> allObjects, ParsedNode node, ResourceAccessor resourceAccessor) throws ReflectiveOperationException, ParsedNodeException {
        if (node == null) {
            return;
        }
        for (ParsedNode typeNode : node.getChildren()) {
            Class<? extends DatabaseObject> objectType = (Class<? extends DatabaseObject>) Class.forName(typeNode.getName());
            for (ParsedNode objectNode : typeNode.getChildren()) {
                DatabaseObject databaseObject = objectType.getConstructor().newInstance();
                databaseObject.load(objectNode, resourceAccessor);
                String key = objectType.getName() + "#" + databaseObject.getSnapshotId();
                objectMap.put(key, databaseObject);
                allObjects.put(key, databaseObject);
            }
        }
    }

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

    /**
     * Used to get and store misc data that should be scoped to the snapshot. Helpful for caching snapshot results.
     *
     * @deprecated Will be removed with 4.0
     */
    public Object getScratchData(String key) {
        return snapshotScratchPad.get(key);
    }

    public Object setScratchData(String key, Object data) {
        return snapshotScratchPad.put(key, data);
    }

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

    public void setSchemaComparisons(CompareControl.SchemaComparison[] schemaComparisons) {
        this.schemaComparisons = schemaComparisons;
    }

    public Map<String, Object> getMetadata() {
        return metadata;
    }

    public void setMetadata(Map<String, Object> metadata) {
        this.metadata = metadata;
    }
}


package liquibase.datatype.core;

import liquibase.Scope;
import liquibase.change.core.LoadDataChange;
import java.util.Locale;

import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.core.*;
import liquibase.datatype.DataTypeInfo;
import liquibase.datatype.DatabaseDataType;
import liquibase.datatype.LiquibaseDataType;
import liquibase.exception.DatabaseIncapableOfOperation;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.util.StringUtil;
import liquibase.util.grammar.ParseException;

import java.util.Locale;

/**
 * Data type support for TIMESTAMP data types in various DBMS. All DBMS are at least expected to support the
 * year, month, day, hour, minute and second parts. Optionally, fractional seconds and time zone information can be
 * specified as well.
 */
@DataTypeInfo(name = "timestamp", aliases = {"java.sql.Types.TIMESTAMP", "java.sql.Timestamp", "timestamptz"}, minParameters = 0, maxParameters = 1, priority = LiquibaseDataType.PRIORITY_DEFAULT)
public class TimestampType extends DateTimeType {

    /**
     * Returns a DBMS-specific String representation of this TimestampType for use in SQL statements.
     * @param database the database for which the String must be generated
     * @return a String with the DBMS-specific type specification
     */
    @Override
    public DatabaseDataType toDatabaseDataType(Database database) {
        String originalDefinition = StringUtil.trimToEmpty(getRawDefinition());
        // If a fractional precision is given, check is the DBMS supports the length
        if (getParameters().length > 0) {
            Integer desiredLength = null;
            try {
                desiredLength = Integer.parseInt(String.valueOf(getParameters()[0]));
            } catch (NumberFormatException e) {
                // That is ok, we won't touch the parameter in this case.
            }

            if (desiredLength != null) {
                int maxFractionalDigits = database.getMaxFractionalDigitsForTimestamp();
                if (maxFractionalDigits < desiredLength) {
                    throw new DatabaseIncapableOfOperation(
                            String.format(
                                    "Using a TIMESTAMP data type with a fractional precision of %d", desiredLength
                            ),
                            String.format(
                                    "A timestamp datatype with %d fractional digits was requested, but %s " +
                                            "only supports %d digits.",
                                    desiredLength,
                                    database.getDatabaseProductName(),
                                    maxFractionalDigits
                            ),
                            database
                    );
                }
            }
        }

        if (database instanceof MySQLDatabase) {
            if (originalDefinition.contains(" ") || originalDefinition.contains("(")) {
                return new DatabaseDataType(getRawDefinition());
            }
            return super.toDatabaseDataType(database);
        }
        if (database instanceof MSSQLDatabase) {
            if (!LiquibaseConfiguration.getInstance()
                    .getProperty(GlobalConfiguration.class, GlobalConfiguration.CONVERT_DATA_TYPES)
                    .getValue(Boolean.class)
                    && originalDefinition.toLowerCase(Locale.US).startsWith("timestamp")) {
                return new DatabaseDataType(database.escapeDataTypeName("timestamp"));
            }
            return new DatabaseDataType(database.escapeDataTypeName("datetime"));
        }
        if (database instanceof SybaseDatabase) {
            return new DatabaseDataType(database.escapeDataTypeName("datetime"));
        }
        if (database instanceof AbstractDb2Database) {
            Object[] parameters = getParameters();
            if ((parameters != null) && (parameters.length > 1)) {
                parameters = new Object[] {parameters[1]};
            }
            return new DatabaseDataType(database.escapeDataTypeName("timestamp"), parameters);
        }

        /*
         * From here on, we assume that we have a SQL standard compliant database that supports the
         * TIMESTAMP[(p)] [WITH TIME ZONE|WITHOUT TIME ZONE] syntax. p is the number of fractional digits,
         * i.e. if "2017-06-02 23:59:45.123456" is supported by the DBMS, p would be 6.
         */
        DatabaseDataType type;

        if (getParameters().length > 0) {
            int fractionalDigits = 0;
            String fractionalDigitsInput = getParameters()[0].toString();
            try {
                fractionalDigits = Integer.parseInt(fractionalDigitsInput);
            } catch (NumberFormatException e) {
                throw new RuntimeException(
                    new ParseException(String.format("A timestamp with '%s' fractional digits was requested, but '%s' does not " +
                        "seem to be an integer.", fractionalDigitsInput, fractionalDigitsInput))
                );
            }
            int maxFractionalDigits = database.getMaxFractionalDigitsForTimestamp();
            if (maxFractionalDigits < fractionalDigits) {
                Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, String.format(
                        "A timestamp datatype with %d fractional digits was requested, but the DBMS %s only supports " +
                                "%d digits. Because of this, the number of digits was reduced to %d.",
                        fractionalDigits, database.getDatabaseProductName(), maxFractionalDigits, maxFractionalDigits)
                );
                fractionalDigits = maxFractionalDigits;
            }
            type =  new DatabaseDataType("TIMESTAMP", fractionalDigits);
        } else {
            type = new DatabaseDataType("TIMESTAMP");
        }

        if (getAdditionalInformation() != null
                && (database instanceof PostgresDatabase
                || database instanceof OracleDatabase)
                || database instanceof H2Database
                || database instanceof HsqlDatabase){
            String additionalInformation = this.getAdditionalInformation();

            if (additionalInformation != null) {
                String additionInformation = additionalInformation.toUpperCase(Locale.US);
                if ((database instanceof PostgresDatabase) && additionInformation.toUpperCase(Locale.US).contains("TIMEZONE")) {
                    additionalInformation = additionInformation.toUpperCase(Locale.US).replace("TIMEZONE", "TIME ZONE");
                }
                // CORE-3229 Oracle 11g doesn't support WITHOUT clause in TIMESTAMP data type
                if ((database instanceof OracleDatabase) && additionInformation.startsWith("WITHOUT")) {
                    // https://docs.oracle.com/cd/B19306_01/server.102/b14225/ch4datetime.htm#sthref389
                    additionalInformation = null;
                }

                if ((database instanceof H2Database) && additionInformation.startsWith("WITHOUT")) {
                    // http://www.h2database.com/html/datatypes.html
                    additionalInformation = null;
                }
            }

            type.addAdditionalInformation(additionalInformation);
            return type;
        }

        return super.toDatabaseDataType(database);
    }

    @Override
    public LoadDataChange.LOAD_DATA_TYPE getLoadTypeName() {
        return LoadDataChange.LOAD_DATA_TYPE.DATE;
    }

}


package liquibase.database.core;

import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Index;
import liquibase.structure.core.PrimaryKey;
import liquibase.util.StringUtil;

import java.math.BigInteger;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import java.util.Set;

/**
 * Encapsulates MySQL database support.
 */
public class MySQLDatabase extends AbstractJdbcDatabase {
    private static final String PRODUCT_NAME = "MySQL";
    private static final Set<String> RESERVED_WORDS = createReservedWords();
    private Boolean hasJdbcConstraintDeferrableBug;

    public MySQLDatabase() {
        super.setCurrentDateTimeFunction("NOW()");
        setHasJdbcConstraintDeferrableBug(null);
    }

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

    @Override
    public String correctObjectName(String name, Class<? extends DatabaseObject> objectType) {
        if (objectType.equals(PrimaryKey.class) && "PRIMARY".equals(name)) {
            return null;
        } else {
            name = super.correctObjectName(name, objectType);
            if (name == null) {
                return null;
            }
            if (!this.isCaseSensitive()) {
                return name.toLowerCase(Locale.US);
            }
            return name;
        }
    }

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

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

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

    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        // If it looks like a MySQL, swims like a MySQL and quacks like a MySQL,
        // it may still not be a MySQL, but a MariaDB.
        return (
                (PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName()))
                        && (!conn.getDatabaseProductVersion().toLowerCase().contains("mariadb"))
        );
    }

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

    @Override
    public boolean supportsSequences() {

        return false;
    }

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

    @Override
    protected boolean mustQuoteObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
        return super.mustQuoteObjectName(objectName, objectType) || (!objectName.contains("(") && !objectName.matches("\\w+"));
    }

    @Override
    public String getLineComment() {
        return "-- ";
    }

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

    @Override
    protected boolean generateAutoIncrementStartWith(final BigInteger startWith) {
        // startWith not supported here. StartWith has to be set as table option.
        return false;
    }

    public String getTableOptionAutoIncrementStartWithClause(BigInteger startWith) {
        String startWithClause = String.format(getAutoIncrementStartWithClause(), (startWith == null) ? defaultAutoIncrementStartWith : startWith);
        return getAutoIncrementClause() + startWithClause;
    }

    @Override
    protected boolean generateAutoIncrementBy(BigInteger incrementBy) {
        // incrementBy not supported
        return false;
    }

    @Override
    protected String getAutoIncrementOpening() {
        return "";
    }

    @Override
    protected String getAutoIncrementClosing() {
        return "";
    }

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

    @Override
    public String getConcatSql(String... values) {
        StringBuilder returnString = new StringBuilder();
        returnString.append("CONCAT_WS(");
        for (String value : values) {
            returnString.append(value).append(", ");
        }

        return returnString.toString().replaceFirst(", $", ")");
    }

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

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

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

    @Override
    public String escapeIndexName(String catalogName, String schemaName, String indexName) {
        return escapeObjectName(indexName, Index.class);
    }

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

    @Override
    public boolean disableForeignKeyChecks() throws DatabaseException {
        boolean enabled = ExecutorService.getInstance().getExecutor(this).queryForInt(new RawSqlStatement("SELECT @@FOREIGN_KEY_CHECKS")) == 1;
        ExecutorService.getInstance().getExecutor(this).execute(new RawSqlStatement("SET FOREIGN_KEY_CHECKS=0"));
        return enabled;
    }

    @Override
    public void enableForeignKeyChecks() throws DatabaseException {
        ExecutorService.getInstance().getExecutor(this).execute(new RawSqlStatement("SET FOREIGN_KEY_CHECKS=1"));
    }

    @Override
    public CatalogAndSchema getSchemaFromJdbcInfo(String rawCatalogName, String rawSchemaName) {
        return new CatalogAndSchema(rawCatalogName, null).customize(this);
    }

    @Override
    public String escapeStringForDatabase(String string) {
        string = super.escapeStringForDatabase(string);
        if (string == null) {
            return null;
        }
        return string.replace("\\", "\\\\");
    }

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

    @Override
    public boolean isReservedWord(String string) {
        if (RESERVED_WORDS.contains(string.toUpperCase())) {
            return true;
        }
        return super.isReservedWord(string);
    }

    public int getDatabasePatchVersion() throws DatabaseException {
        String databaseProductVersion = this.getDatabaseProductVersion();
        if (databaseProductVersion == null) {
            return 0;
        }

        String versionStrings[] = databaseProductVersion.split("\\.");
        try {
            return Integer.parseInt(versionStrings[2].replaceFirst("\\D.*", ""));
        } catch (IndexOutOfBoundsException | NumberFormatException e) {
            return 0;
        }

    }

    /**
     * Tests if this MySQL / MariaDB database has a bug where the JDBC driver returns constraints as
     * DEFERRABLE INITIAL IMMEDIATE even though neither MySQL nor MariaDB support DEFERRABLE CONSTRAINTs at all.
     * We need to know about this because this could lead to errors in database snapshots.
     * @return true if this database is affected, false if not, null if we cannot tell (e.g. OfflineConnection)
     */
    @SuppressWarnings("squid:S2447") // null is explicitly documented as a possible return value.
    // TODO: MariaDB connector 2.0.2 appearantly fixes this problem, and MySQL-ConnectorJ 6.0.6 did not have it in
    // the first place.. Replace this crude workaround with a proper JDBC driver version check
    public Boolean hasBugJdbcConstraintsDeferrable() throws DatabaseException {
        if (getConnection() instanceof OfflineConnection)
            return null;
        if (getHasJdbcConstraintDeferrableBug() != null)  // cached value
            return getHasJdbcConstraintDeferrableBug();

        String randomIdentifier = "TMP_" + StringUtil.randomIdentifer(16);
        try
        {
            // Get the real connection and metadata reference
            java.sql.Connection conn = ((JdbcConnection) getConnection()).getUnderlyingConnection();
            java.sql.DatabaseMetaData metaData = conn.getMetaData();
            String sql = "CREATE TABLE " + randomIdentifier + " (\n" +
                    "  id INT PRIMARY KEY,\n" +
                    "  self_ref INT NOT NULL,\n" +
                    "  CONSTRAINT c_self_ref FOREIGN KEY(self_ref) REFERENCES " + randomIdentifier + "(id)\n" +
                    ")";
            ExecutorService.getInstance().getExecutor(this).execute(new RawSqlStatement(sql));

            try (
                ResultSet rs = metaData.getImportedKeys(getDefaultCatalogName(), getDefaultSchemaName(), randomIdentifier)
            ) {
                if (!rs.next()) {
                    throw new UnexpectedLiquibaseException("Error during testing for MySQL/MariaDB JDBC driver bug: " +
                            "could not retrieve JDBC metadata information for temporary table '" +
                            randomIdentifier + "'");
                }
                if (rs.getShort("DEFERRABILITY") != DatabaseMetaData.importedKeyNotDeferrable) {
                    setHasJdbcConstraintDeferrableBug(true);
                    Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Your MySQL/MariaDB database JDBC driver might have " +
                            "a bug where constraints are reported as DEFERRABLE, even though MySQL/MariaDB do not " +
                            "support this feature. A workaround for this problem will be used. Please check with " +
                            "MySQL/MariaDB for availability of fixed JDBC drivers to avoid this warning.");
                } else {
                    setHasJdbcConstraintDeferrableBug(false);
                }
            }

        } catch (DatabaseException|SQLException e) {
            throw new UnexpectedLiquibaseException("Error during testing for MySQL/MariaDB JDBC driver bug.", e);
        } finally {
                ExecutorService.getInstance().reset();
                ExecutorService.getInstance().getExecutor(this).execute(
                        new RawSqlStatement("DROP TABLE " + randomIdentifier));
        }

        return getHasJdbcConstraintDeferrableBug();
    }

    /**
     * returns true if the JDBC drivers suffers from a bug where constraints are reported as DEFERRABLE, even though
     * MySQL/MariaDB do not support this feature.
     * @return true if the JDBC is probably affected, false if not.
     */
    protected Boolean getHasJdbcConstraintDeferrableBug() {
        return hasJdbcConstraintDeferrableBug;
    }

    protected void setHasJdbcConstraintDeferrableBug(Boolean hasJdbcConstraintDeferrableBug) {
        this.hasJdbcConstraintDeferrableBug = hasJdbcConstraintDeferrableBug;
    }

    @Override
    public int getMaxFractionalDigitsForTimestamp() {

        int major = 0;
        int minor = 0;
        int patch = 0;

        try {
            major = getDatabaseMajorVersion();
            minor = getDatabaseMinorVersion();
            patch = getDatabasePatchVersion();
        } catch (DatabaseException x) {
            Scope.getCurrentScope().getLog(getClass()).warning(
                    LogType.LOG, "Unable to determine exact database server version"
                            + " - specified TIMESTAMP precision"
                            + " will not be set: ", x);
            return 0;
        }

        String minimumVersion = getMinimumVersionForFractionalDigitsForTimestamp();

        if (StringUtil.isMinimumVersion(minimumVersion, major, minor, patch))
            return 6;
        else
            return 0;
    }

    protected String getMinimumVersionForFractionalDigitsForTimestamp() {
        // MySQL 5.6.4 introduced fractional support...
        // https://dev.mysql.com/doc/refman/5.7/en/data-types.html
        return "5.6.4";
    }

    @Override
    protected String getQuotingStartCharacter() {
        return "`"; // objects in mysql are always case sensitive

    }

    @Override
    protected String getQuotingEndCharacter() {
        return "`"; // objects in mysql are always case sensitive
    }

    /**
     *
Returns the default timestamp fractional digits if nothing is specified.

     * https://dev.mysql.com/doc/refman/5.7/en/fractional-seconds.html :
     * "The fsp value, if given, must be in the range 0 to 6. A value of 0 signifies that there is no fractional part.
     * If omitted, the default precision is 0. (This differs from the STANDARD SQL default of 6, for compatibility
     * with previous MySQL versions.)"
     *
     * @return always 0
     */
    @Override
    public int getDefaultFractionalDigitsForTimestamp() {
        return 0;
    }

    /*
     * list from http://dev.mysql.com/doc/refman/5.6/en/reserved-words.html
     */
    private static Set<String> createReservedWords() {
        return new HashSet<String>(Arrays.asList("ACCESSIBLE",
            "ADD",
            "ADMIN",
                "ALL",
            "ALTER",
            "ANALYZE",
            "AND",
            "AS",
            "ASC",
            "ASENSITIVE",
            "BEFORE",
            "BETWEEN",
            "BIGINT",
            "BINARY",
            "BLOB",
            "BOTH",
                "BUCKETS",
            "BY",
            "CALL",
            "CASCADE",
            "CASE",
            "CHANGE",
            "CHAR",
            "CHARACTER",
            "CHECK",
            "COLLATE",
            "COLUMN",
            "CONDITION",
            "CONSTRAINT",
            "CONTINUE",
            "CONVERT",
            "CREATE",
            "CROSS",
                "CLONE",
                "COMPONENT",
                "CUME_DIST",
            "CURRENT_DATE",
            "CURRENT_TIME",
            "CURRENT_TIMESTAMP",
            "CURRENT_USER",
            "CURSOR",
            "DATABASE",
            "DATABASES",
            "DAY_HOUR",
            "DAY_MICROSECOND",
            "DAY_MINUTE",
            "DAY_SECOND",
            "DEC",
            "DECIMAL",
            "DECLARE",
            "DEFAULT",
                "DEFINITION",
            "DELAYED",
            "DELETE",
                "DENSE_RANK",
            "DESC",
            "DESCRIBE",
                "DESCRIPTION",
            "DETERMINISTIC",
            "DISTINCT",
            "DISTINCTROW",
            "DIV",
            "DOUBLE",
            "DROP",
            "DUAL",
            "EACH",
            "ELSE",
            "ELSEIF",
                "EMPTY",
            "ENCLOSED",
            "ESCAPED",
                "EXCEPT",
                "EXCLUDE",
            "EXISTS",
            "EXIT",
            "EXPLAIN",
            "FALSE",
            "FETCH",
                "FIRST_VALUE",
            "FLOAT",
            "FLOAT4",
            "FLOAT8",
                "FOLLOWING",
            "FOR",
            "FORCE",
            "FOREIGN",
            "FROM",
            "FULLTEXT",
                "GEOMCOLLECTION",
            "GENERATED",
            "GET",
                "GET_MASTER_PUBLIC_KEY",
            "GRANT",
            "GROUP",
            "GROUPING",
                "GROUPS",
                "HAVING",
            "HIGH_PRIORITY",
                "HISTOGRAM",
                "HISTORY",
            "HOUR_MICROSECOND",
            "HOUR_MINUTE",
            "HOUR_SECOND",
            "IF",
            "IGNORE",
            "IN",
            "INDEX",
            "INFILE",
            "INNER",
            "INOUT",
            "INSENSITIVE",
            "INSERT",
            "INT",
            "INT1",
            "INT2",
            "INT3",
            "INT4",
            "INT8",
            "INTEGER",
            "INTERVAL",
            "INTO",
            "IS",
            "ITERATE",
                "INVISIBLE",
            "JOIN",
                "JSON_TABLE",
            "KEY",
            "KEYS",
            "KILL",
            "LAG",
                "LAST_VALUE",
                "LEAD",
                "LEADING",
            "LEAVE",
            "LEFT",
            "LIKE",
            "LIMIT",
            "LINEAR",
            "LINES",
            "LOAD",
            "LOCALTIME",
            "LOCALTIMESTAMP",
            "LOCK",
            "LOCKED",
                "LONG",
            "LONGBLOB",
            "LONGTEXT",
            "LOOP",
            "LOW_PRIORITY",
                "MASTER_PUBLIC_KEY_PATH",
            "MASTER_SSL_VERIFY_SERVER_CERT",
            "MATCH",
            "MAXVALUE",
            "MEDIUMBLOB",
            "MEDIUMINT",
            "MEDIUMTEXT",
            "MIDDLEINT",
            "MINUTE_MICROSECOND",
            "MINUTE_SECOND",
            "MOD",
            "MODIFIES",
            "NATURAL",
                "NESTED",
            "NOT",
            "NOWAIT",
                "NO_WRITE_TO_BINLOG",
            "NTH_VALUE",
                "NTILE",
                "NULL",
            "NULLS",
                "NUMERIC",
            "OF",
                "ON",
            "OPTIMIZE",
            "OPTIMIZER_COSTS",
            "OPTION",
            "OPTIONALLY",
            "OR",
            "ORDER",
                "ORDINALITY",
                "ORGANIZATION",
            "OUT",
            "OUTER",
            "OUTFILE",
                "OTHERS",
                "OVER",
            "PARTITION",
            "PATH",
                "PERCENT_RANK",
                "PERSIST",
                "PERSIST_ONLY",
                "PRECEDING",
                "PRECISION",
            "PRIMARY",
            "PROCEDURE",
                "PROCESS",
            "PURGE",
            "RANGE",
            "RANK",
                "READ",
            "READS",
            "READ_WRITE",
            "REAL",
            "RECURSIVE",
                "REFERENCE",
                "REFERENCES",
            "REGEXP",
            "RELEASE",
                "REMOTE",
            "RENAME",
            "REPEAT",
            "REPLACE",
            "REQUIRE",
            "RESIGNAL",
                "RESOURCE",
                "RESPECT",
                "RESTART",
            "RESTRICT",
            "RETURN",
                "REUSE",
            "REVOKE",
            "RIGHT",
            "RLIKE",
                "ROLE",
                "ROW_NUMBER",
            "SCHEMA",
            "SCHEMAS",
            "SECOND_MICROSECOND",
            "SELECT",
            "SENSITIVE",
            "SEPARATOR",
            "SET",
            "SHOW",
            "SIGNAL",
                "SKIP",
            "SMALLINT",
            "SPATIAL",
            "SPECIFIC",
            "SQL",
            "SQLEXCEPTION",
            "SQLSTATE",
            "SQLWARNING",
            "SQL_BIG_RESULT",
            "SQL_CALC_FOUND_ROWS",
            "SQL_SMALL_RESULT",
            "SRID",
                "SSL",
            "STARTING",
            "STORED",
            "STRAIGHT_JOIN",
            "SYSTEM",
                "TABLE",
            "TERMINATED",
            "THEN",
                "THREAD_PRIORITY",
                "TIES",
            "TINYBLOB",
            "TINYINT",
            "TINYTEXT",
            "TO",
            "TRAILING",
            "TRIGGER",
            "TRUE",
            "UNBOUNDED",
                "UNDO",
            "UNION",
            "UNIQUE",
            "UNLOCK",
            "UNSIGNED",
            "UPDATE",
            "USAGE",
            "USE",
            "USING",
            "UTC_DATE",
            "UTC_TIME",
            "UTC_TIMESTAMP",
            "VALUES",
            "VARBINARY",
            "VARCHAR",
            "VARCHARACTER",
            "VARYING",
            "VCPU",
                "VISIBLE",
                "VIRTUAL",
            "WHEN",
            "WHERE",
            "WHILE",
                "WINDOW",
            "WITH",
            "WRITE",
            "XOR",
            "YEAR_MONTH",
            "ZEROFILL"
        ));
    }

}


package liquibase.snapshot.jvm;

import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.core.InformixDatabase;
import liquibase.database.core.PostgresDatabase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.diff.DiffStatusListener;
import liquibase.exception.DatabaseException;
import liquibase.logging.LogType;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotGenerator;
import liquibase.snapshot.SnapshotGeneratorChain;
import liquibase.structure.DatabaseObject;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public abstract class JdbcSnapshotGenerator implements SnapshotGenerator {
    private Set<DiffStatusListener> statusListeners = new HashSet<>();

    private Class<? extends DatabaseObject> defaultFor;
    private Class<? extends DatabaseObject>[] addsTo;

    protected JdbcSnapshotGenerator(Class<? extends DatabaseObject> defaultFor) {
        this.defaultFor = defaultFor;
    }

    protected JdbcSnapshotGenerator(Class<? extends DatabaseObject> defaultFor, Class<? extends DatabaseObject>[] addsTo) {
        this.defaultFor = defaultFor;
        this.addsTo = addsTo;
    }

    @Override
    public int getPriority(Class<? extends DatabaseObject> objectType, Database database) {
        if (database instanceof AbstractJdbcDatabase) {
            if ((defaultFor != null) && defaultFor.isAssignableFrom(objectType)) {
                return PRIORITY_DEFAULT;
            }
            if (addsTo() != null) {
                for (Class<? extends DatabaseObject> type : addsTo()) {
                    if (type.isAssignableFrom(objectType)) {
                        return PRIORITY_ADDITIONAL;
                    }
                }
            }
        }
        return PRIORITY_NONE;

    }

    @Override
    public Class<? extends DatabaseObject>[] addsTo() {
        return addsTo;
    }

    @Override
    public DatabaseObject snapshot(DatabaseObject example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) throws DatabaseException, InvalidExampleException {
        if ((defaultFor != null) && defaultFor.isAssignableFrom(example.getClass())) {
            return snapshotObject(example, snapshot);
        }

        DatabaseObject chainResponse = chain.snapshot(example, snapshot);
        if (chainResponse == null) {
            return null;
        }

        if (shouldAddTo(example.getClass(), snapshot)) {
            if (addsTo() != null) {
                for (Class<? extends DatabaseObject> addType : addsTo()) {
                    if (addType.isAssignableFrom(example.getClass())) {
                        if (chainResponse != null) {
                            addTo(chainResponse, snapshot);
                        }
                    }
                }
            }
        }
        return chainResponse;

    }

    protected boolean shouldAddTo(Class<? extends DatabaseObject> databaseObjectType, DatabaseSnapshot snapshot) {
        return (defaultFor != null) && snapshot.getSnapshotControl().shouldInclude(defaultFor);
    }

    @Override
    public Class<? extends SnapshotGenerator>[] replaces() {
        return null;
    }

    protected abstract DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException;

    protected abstract void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException;

    public void addStatusListener(DiffStatusListener listener) {
        statusListeners.add(listener);
    }

    protected void updateListeners(String message) {
        if (this.statusListeners == null) {
            return;
        }
        Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, message);
        for (DiffStatusListener listener : this.statusListeners) {
            listener.statusUpdate(message);
        }
    }

    protected String cleanNameFromDatabase(String objectName, Database database) {
        if (objectName == null) {
            return null;
        }
        if (!(database instanceof InformixDatabase)) {
            objectName = objectName.trim();
        }
        if (database instanceof PostgresDatabase) {
            return objectName.replaceAll("\"", "");
        }
        return objectName;
    }

    /**
     * Fetches an array of Strings with the catalog names in the database.
     * @param database The database from which to get the schema names
     * @return An array of catalog name Strings (May be an empty array)
     * @throws SQLException propagated java.sql.SQLException
     * @throws DatabaseException if a different problem occurs during the DBMS-specific code
     */
    protected String[] getDatabaseCatalogNames(Database database) throws SQLException, DatabaseException {
        List<String> returnList = new ArrayList<>();

        ResultSet catalogs = null;

        try {
            if (((AbstractJdbcDatabase) database).jdbcCallsCatalogsSchemas()) {
                catalogs = ((JdbcConnection) database.getConnection()).getMetaData().getSchemas();
            } else {
                catalogs = ((JdbcConnection) database.getConnection()).getMetaData().getCatalogs();
            }
            while (catalogs.next()) {
                if (((AbstractJdbcDatabase) database).jdbcCallsCatalogsSchemas()) {
                    returnList.add(catalogs.getString("TABLE_SCHEM"));
                } else {
                    returnList.add(catalogs.getString("TABLE_CAT"));
                }
            }
        } finally {
            if (catalogs != null) {
                try {
                    catalogs.close();
                } catch (SQLException ignore) {

                }
            }

        }
        return returnList.toArray(new String[returnList.size()]);
    }

}


package liquibase.database.core;

import java.sql.ResultSet;
import java.sql.Statement;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Locale;

import liquibase.CatalogAndSchema;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.DateParseException;
import liquibase.executor.ExecutorService;
import liquibase.statement.core.GetViewDefinitionStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Catalog;
import liquibase.structure.core.Index;
import liquibase.structure.core.Schema;
import liquibase.util.JdbcUtils;
import liquibase.util.StringUtil;

import java.sql.ResultSet;
import java.sql.Statement;
import java.text.ParseException;
import java.text.SimpleDateFormat;

public abstract class AbstractDb2Database extends AbstractJdbcDatabase {

    public AbstractDb2Database() {
        super.setCurrentDateTimeFunction("CURRENT TIMESTAMP");
        super.sequenceNextValueFunction = "NEXT VALUE FOR %s";
        super.sequenceCurrentValueFunction = "PREVIOUS VALUE FOR %s";
        super.unquotedObjectsAreUppercased=true;
    }

    @Override
    public String getDefaultDriver(String url) {
        if (url.startsWith("jdbc:db2")) {
            return "com.ibm.db2.jcc.DB2Driver";
        }
        return null;
    }

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

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

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

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

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

    @Override
    public String getDefaultCatalogName() {

        if (defaultCatalogName != null) {
            return defaultCatalogName;
        }

        if (defaultSchemaName != null) {
            return defaultSchemaName;
        }

        if (getConnection() == null) {
            return null;
        }
        if (getConnection() instanceof OfflineConnection) {
            return ((OfflineConnection) getConnection()).getSchema();
        }

        Statement stmt = null;
        ResultSet rs = null;
        try {
            stmt = ((JdbcConnection) getConnection()).createStatement();
            rs = stmt.executeQuery("select current schema from sysibm.sysdummy1");
            if (rs.next()) {
                String result = rs.getString(1);
                if (result != null) {
                    this.defaultSchemaName = StringUtil.trimToNull(result);
                } else {
                    this.defaultSchemaName = StringUtil.trimToNull(super.getDefaultSchemaName());
                }
            }
        } catch (Exception e) {
            throw new RuntimeException("Could not determine current schema", e);
        } finally {
            JdbcUtils.close(rs, stmt);
        }

        return defaultSchemaName;
    }

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

    /**
     * Return an DB2 date literal with the same value as a string formatted using ISO 8601.
     *

     * Convert an ISO8601 date string to one of the following results:
     * to_date('1995-05-23', 'YYYY-MM-DD')
     * to_date('1995-05-23 09:23:59', 'YYYY-MM-DD HH24:MI:SS')
     *

     * Implementation restriction:
     * Currently, only the following subsets of ISO8601 are supported:
     * YYYY-MM-DD
     * hh:mm:ss
     * YYYY-MM-DDThh:mm:ss
     */
    @Override
    public String getDateLiteral(String isoDate) {
        String normalLiteral = super.getDateLiteral(isoDate);

        if (isDateOnly(isoDate)) {
            StringBuffer val = new StringBuffer();
            val.append("DATE(");
            val.append(normalLiteral);
            val.append(')');
            return val.toString();
        } else if (isTimeOnly(isoDate)) {
            StringBuffer val = new StringBuffer();
            val.append("TIME(");
            val.append(normalLiteral);
            val.append(')');
            return val.toString();
        } else if (isDateTime(isoDate)) {
            StringBuffer val = new StringBuffer();
            val.append("TIMESTAMP(");
            val.append(normalLiteral);
            val.append(')');
            return val.toString();
        } else {
            return "UNSUPPORTED:" + isoDate;
        }
    }

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

    @Override
    public String getViewDefinition(CatalogAndSchema schema, String viewName) throws DatabaseException {
        schema = schema.customize(this);
        String definition = ExecutorService.getInstance().getExecutor(this).queryForObject(new GetViewDefinitionStatement(schema.getCatalogName(), schema.getSchemaName(), viewName), String.class);

        return "FULL_DEFINITION: " + definition;
    }

    @Override
    public java.util.Date parseDate(String dateAsString) throws DateParseException {
        try {
            if (dateAsString.indexOf(' ') > 0) {
                return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").parse(dateAsString);
            } else if (dateAsString.indexOf('.') > 0 && dateAsString.indexOf('-') > 0) {
                return new SimpleDateFormat("yyyy-MM-dd-HH.mm.ss.SSSSSS").parse(dateAsString);

            } else {
                if (dateAsString.indexOf(':') > 0) {
                    return new SimpleDateFormat("HH:mm:ss").parse(dateAsString);
                } else if (dateAsString.indexOf('.') > 0) {
                    return new SimpleDateFormat("HH.mm.ss").parse(dateAsString);
                } else {
                    return new SimpleDateFormat("yyyy-MM-dd").parse(dateAsString);
                }
            }
        } catch (ParseException e) {
            throw new DateParseException(dateAsString);
        }
    }

    @Override
    public String generatePrimaryKeyName(String tableName) {
        if (tableName.equals(getDatabaseChangeLogTableName())) {
            tableName = "DbChgLog".toUpperCase(Locale.US);
        } else if (tableName.equals(getDatabaseChangeLogLockTableName())) {
            tableName = "DbChgLogLock".toUpperCase(Locale.US);
        }

        String pkName = super.generatePrimaryKeyName(tableName);
        if (pkName.length() > 18) {
            pkName = pkName.substring(0, 17);
        }
        return pkName;
    }

    @Override
    public CatalogAndSchema getSchemaFromJdbcInfo(String rawCatalogName, String rawSchemaName) {
        if (rawCatalogName != null && rawSchemaName == null) {
            rawSchemaName = rawCatalogName;
        }
        return new CatalogAndSchema(rawSchemaName, null).customize(this);
    }

    @Override
    public String getJdbcCatalogName(CatalogAndSchema schema) {
        return null;
    }

    @Override
    public String getJdbcSchemaName(CatalogAndSchema schema) {
        return correctObjectName(schema.getCatalogName(), Catalog.class);
    }

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

    @Override
    public boolean isSystemObject(DatabaseObject example) {
        if (example instanceof Index && example.getName() != null && example.getName().matches("SQL\\d+")) {
            return true;
        }
        return super.isSystemObject(example);
    }

    @Override
    public String correctObjectName(final String objectName, final Class<? extends DatabaseObject> objectType) {
        return objectName;
    }

    protected boolean mustQuoteObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
        if (objectType.isAssignableFrom(Schema.class) || objectType.isAssignableFrom(Catalog.class)) {
            return true;
        }
        return super.mustQuoteObjectName(objectName, objectType);
    }

    /**
     * DB2 database are not case sensitive. However schemas and catalogs are case sensitive
     */
    @Override
    public CatalogAndSchema.CatalogAndSchemaCase getSchemaAndCatalogCase() {
        return CatalogAndSchema.CatalogAndSchemaCase.ORIGINAL_CASE;
    }
}


package liquibase;

import liquibase.database.Database;
import liquibase.structure.core.Catalog;
import liquibase.structure.core.Schema;
import liquibase.util.StringUtil;

import java.util.Locale;

/**
 * Object representing a database catalog and schema. This differs from {@link liquibase.structure.core.Schema} in that it has
 * not come from an actual database Schema.
 * <p>
 * A null value for catalogName or schemaName signifies the default catalog/schema.
 */
public class CatalogAndSchema {
    public static final CatalogAndSchema DEFAULT = new CatalogAndSchema(null, null);
    private String catalogName;
    private String schemaName;

    public CatalogAndSchema(String catalogName, String schemaName) {
        this.catalogName = catalogName;
        this.schemaName = schemaName;
    }

    public enum CatalogAndSchemaCase {
        LOWER_CASE, UPPER_CASE, ORIGINAL_CASE
    }

    public String getCatalogName() {
        return catalogName;
    }

    public String getSchemaName() {
        return schemaName;
    }

    public boolean equals(CatalogAndSchema catalogAndSchema, Database accordingTo) {
        if (!accordingTo.supportsCatalogs()) {
            return true;
        }

        CatalogAndSchema workCatalogAndSchema = catalogAndSchema.customize(accordingTo);
        CatalogAndSchema thisCatalogAndSchema = this.customize(accordingTo);

        boolean catalogMatches;
        if (workCatalogAndSchema.getCatalogName() == null) {
            catalogMatches = (thisCatalogAndSchema.getCatalogName() == null);
        } else {
            catalogMatches = equals(accordingTo, workCatalogAndSchema.getCatalogName(),thisCatalogAndSchema.getCatalogName());
        }
        if (!catalogMatches) {
            return false;
        }

        if (accordingTo.supportsSchemas()) {
            if (workCatalogAndSchema.getSchemaName() == null) {
                return thisCatalogAndSchema.getSchemaName() == null;
            } else {
                return equals(accordingTo, workCatalogAndSchema.getSchemaName(), thisCatalogAndSchema.getSchemaName());
            }
        } else {
            return true;
        }
    }

    /**
     * This method returns a new CatalogAndSchema adjusted based on the configuration of the passed database.
     * If the database does not support schemas, the returned object will have a null schema.
     * If the database does not support catalogs, the returned object will have a null catalog.
     * If either the schema or catalog matches the database default catalog or schema, they will be nulled out.
     * Catalog and/or schema names will be upper case unless the database violates the SQL standard by being
     * case-sensitive about some or all unquoted identifiers.
     *
     * @see CatalogAndSchema#customize(liquibase.database.Database)
     * */
    public CatalogAndSchema standardize(Database accordingTo) {
        String workCatalogName = StringUtil.trimToNull(getCatalogName());
        String workSchemaName = StringUtil.trimToNull(getSchemaName());

        if (!accordingTo.supportsCatalogs()) {
            return new CatalogAndSchema(null, null);
        }

        if (accordingTo.supportsSchemas()) {
            if ((workSchemaName != null) && workSchemaName.equalsIgnoreCase(accordingTo.getDefaultSchemaName())) {
                workSchemaName = null;
            }
        } else {
            if ((workCatalogName == null) && (workSchemaName != null)) { //had names in the wrong order
                workCatalogName = workSchemaName;
            }
            workSchemaName = workCatalogName;
        }

        if (workCatalogName != null && equals(accordingTo, workCatalogName, accordingTo.getDefaultCatalogName())) {
            workCatalogName = null;
        }
        if (workSchemaName != null && equals(accordingTo, workSchemaName, accordingTo.getDefaultSchemaName())) {
            workSchemaName = null;
        }
        if (!accordingTo.supportsSchemas() && (workCatalogName != null) && (workSchemaName != null) &&
            !workCatalogName.equals(workSchemaName)) {
            workSchemaName = null;
        }

        if (CatalogAndSchemaCase.LOWER_CASE.equals(accordingTo.getSchemaAndCatalogCase())) {
            if (workCatalogName != null) {
                workCatalogName = workCatalogName.toLowerCase(Locale.US);
            }
            if (workSchemaName != null) {
                workSchemaName = workSchemaName.toLowerCase(Locale.US);
            }
        } else if (CatalogAndSchemaCase.UPPER_CASE.equals(accordingTo.getSchemaAndCatalogCase())) {
            if (!accordingTo.isCaseSensitive()) {
                if (workCatalogName != null) {
                    workCatalogName = workCatalogName.toUpperCase(Locale.US);
                }
                if (workSchemaName != null) {
                    workSchemaName = workSchemaName.toUpperCase(Locale.US);
                }
            }
        }
        return new CatalogAndSchema(workCatalogName, workSchemaName);

    }

    /**
     * Returns a new CatalogAndSchema object with null/default catalog and schema names set to the
     * correct default catalog and schema. If the database does not support catalogs or schemas they will
     * retain a null value.
     * Catalog and schema capitalization will match what the database expects.
     *
     * @see CatalogAndSchema#standardize(liquibase.database.Database)
     */
    public CatalogAndSchema customize(Database accordingTo) {
        CatalogAndSchema standard = standardize(accordingTo);

        String workCatalogName = standard.getCatalogName();
        String workSchemaName = standard.getSchemaName();

        if (workCatalogName == null) {
            if (!accordingTo.supportsSchemas() && (workSchemaName != null)) {
                return new CatalogAndSchema(accordingTo.correctObjectName(workSchemaName, Catalog.class), null);
            }
            workCatalogName = accordingTo.getDefaultCatalogName();
        }

        if (workSchemaName == null) {
            workSchemaName = accordingTo.getDefaultSchemaName();
        }

        if (workCatalogName != null) {
            workCatalogName = accordingTo.correctObjectName(workCatalogName, Catalog.class);
        }
        if (workSchemaName != null) {
            workSchemaName = accordingTo.correctObjectName(workSchemaName, Schema.class);
        }

        return new CatalogAndSchema(workCatalogName, workSchemaName);
    }

    /**
     * String version includes both catalog and schema. If either is null it returns the string "DEFAULT" in its place.
     */
    @Override
    public String toString() {
        String tmpCatalogName = getCatalogName();
        String tmpSchemaName = getSchemaName();

        if (tmpCatalogName == null) {
            tmpCatalogName = "DEFAULT";
        }
        if (tmpSchemaName == null) {
            tmpSchemaName = "DEFAULT";
        }

        return tmpCatalogName + "." + tmpSchemaName;
    }

    /**
     * This method does schema or catalog comparision
     *
     * @param database - it's db object to getSchemaAndCatalogCase
     * @param value1 - schema or catalog to compare with value2
     * @param value2 - schema or catalog to compare with value1
     *
     * @return true if value1 and value2 equal
     */
    private boolean equals(Database database, String value1, String value2) {
        CatalogAndSchemaCase schemaAndCatalogCase = database.getSchemaAndCatalogCase();
        if (CatalogAndSchemaCase.UPPER_CASE.equals(schemaAndCatalogCase) ||
                CatalogAndSchemaCase.LOWER_CASE.equals(schemaAndCatalogCase)) {
            return value1.equalsIgnoreCase(value2);
        }

        return value1.equals(value2);
    }

}


package liquibase.util;

import java.util.*;

/**
 * Implementation of Map with the added methods {@link #get(String, Class)}  and {@link #get(String, Object)} to make the return values type safe and/or auto-converted.
 * Also adds {@link #set(String, Object)} for easier builder-style code.
 * Returns keys in alphabetical order.
 */
public class SmartMap implements Map<String, Object> {
    private SortedMap<String, Object> values = new TreeMap<String, Object>();

    @Override
    public int size() {
        return values.size();
    }

    @Override
    public boolean isEmpty() {
        return values.isEmpty();
    }

    @Override
    public boolean containsKey(Object key) {
        return values.containsKey(key);
    }

    @Override
    public boolean containsValue(Object value) {
        return values.containsValue(value);
    }

    @Override
    public Object get(Object key) {
        return values.get(key);
    }

    /**
     * Return the value for the given key converted to the passed type. If the value associated with the key is null, null is returned.
     * If the stored value is different than the passed type, a conversion is attempted using {@link ObjectUtil#convert(Object, Class)}.
     * Any conversion is done only on the return value. The stored value is unchanged.
     */
    public <T> T get(String key, Class<T> type) {
        return (T) ObjectUtil.convert(values.get(key), type);
    }

    /**
     * Return the value for the given key converted to the type of the default value.
     * If the value is null, defaultValue is returned.
     * Conversion is done using {@link #get(String, Class)}
     */
    public <T> T get(String key, T defaultValue) {
        Class type;
        if (defaultValue == null) {
            type = Object.class;
        } else {
            type = defaultValue.getClass();
        }
        Object value = get(key, type);

        if (value == null) {
            return defaultValue;
        }
        return (T) value;
    }

    /**
     * Like normal {@link Map#put(Object, Object)} operation, but if null is passed as "value" it removes the key/value from the map.
     */
    @Override
    public Object put(String key, Object value) {
        if (value == null) {
            return values.remove(key);
        } else {
            return values.put(key, value);
        }
    }

    /**
     * Works like {@link #put(String, Object)} but returns this SmartMap rather than the old value.
     */
    public SmartMap set(String key, Object value) {
        put(key, value);
        return this;
    }

    @Override
    public Object remove(Object key) {
        return values.remove(key);
    }

    @Override
    public void putAll(Map<? extends String, ?> m) {
        values.putAll(m);
    }

    @Override
    public void clear() {
        values.clear();
    }

    @Override
    public boolean equals(Object o) {
        return values.equals(o);
    }

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

    @Override
    public Set<String> keySet() {
        return values.keySet();
    }

    @Override
    public Collection<Object> values() {
        return values.values();
    }

    @Override
    public Set<Entry<String, Object>> entrySet() {
        return values.entrySet();
    }
}


package liquibase.servicelocator;

import liquibase.logging.LogType;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

/**
 * WebSphere specific resolver to handle loading annotated resources in JAR files.
 */
public class WebSpherePackageScanClassResolver extends DefaultPackageScanClassResolver {

    private final String resourcePath;

    /**
     * Constructor.
     *
     * @param resourcePath  the fixed resource path to use for fetching camel jars in WebSphere.
     */
    public WebSpherePackageScanClassResolver(String resourcePath) {
        this.resourcePath = resourcePath;
    }

    /**
     * Is the classloader from IBM and thus the WebSphere platform?
     *
     * @param loader  the classloader
     * @return  <tt>true</tt> if IBM classloader, <tt>false</tt> otherwise.
     */
    public static boolean isWebSphereClassLoader(ClassLoader loader) {
        return loader.getClass().getName().startsWith("com.ibm");
    }

    /**
     * Overloaded to handle specific problem with getting resources on the IBM WebSphere platform.
     *

     * WebSphere can not load resources if the resource to load is a folder name, such as a
     * packagename, you have to explicit name a resource that is a file.
     *
     * @param loader  the classloader
     * @param packageName   the packagename for the package to load
     * @return  URL's for the given package
     * @throws java.io.IOException is thrown by the classloader
     */
    @Override
    protected Enumeration<URL> getResources(ClassLoader loader, String packageName) throws IOException {
        // try super first, just in vase
        Enumeration<URL> enumeration = super.getResources(loader, packageName);
        if (!enumeration.hasMoreElements()) {
            log.fine(LogType.LOG, "Using WebSphere workaround to load the camel jars with the annotated converters.");
            // Special WebSphere trick to load a file that exists in the JAR and then let it go from there.
            // The trick is that we just need the URL's for the .jars that contains the type
            // converters that is annotated. So by searching for this resource WebSphere is able to find
            // it and return the URL to the .jar file with the resource. Then the DefaultPackageScanClassResolver
            // can take it from there and find the classes that are annotated.
            enumeration = loader.getResources(resourcePath);
        }

        return enumeration;
    }

}