package liquibase.snapshot.jvm;

import liquibase.CatalogAndSchema;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.core.DB2Database;
import liquibase.database.core.Db2zDatabase;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.database.core.MariaDBDatabase;
import liquibase.database.core.MySQLDatabase;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.DatabaseException;
import liquibase.snapshot.CachedRow;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.JdbcDatabaseSnapshot;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.*;

import java.sql.DatabaseMetaData;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

public class ForeignKeySnapshotGenerator extends JdbcSnapshotGenerator {

    protected static final String METADATA_DEFERRABILITY = "DEFERRABILITY";
    public static final String METADATA_FKTABLE_CAT = "FKTABLE_CAT";
    public static final String METADATA_FKTABLE_SCHEM = "FKTABLE_SCHEM";
    public static final String METADATA_FKTABLE_NAME = "FKTABLE_NAME";
    public static final String METADATA_FKCOLUMN_NAME = "FKCOLUMN_NAME";
    public static final String METADATA_PKTABLE_CAT = "PKTABLE_CAT";
    public static final String METADATA_PKTABLE_SCHEM = "PKTABLE_SCHEM";
    public static final String METADATA_PKTABLE_NAME = "PKTABLE_NAME";
    public static final String METADATA_PKCOLUMN_NAME = "PKCOLUMN_NAME";
    public static final String METADATA_UPDATE_RULE = "UPDATE_RULE";
    public static final String METADATA_DELETE_RULE = "DELETE_RULE";

    public ForeignKeySnapshotGenerator() {
        super(ForeignKey.class, new Class[]{Table.class});
    }

//    public Boolean has(DatabaseObject example, DatabaseSnapshot snapshot, SnapshotGeneratorChain chain) throws DatabaseException {
//        if (example instanceof ForeignKey) {
//            Database database = snapshot.getDatabase();
//            String searchCatalog = database.getJdbcCatalogName(example.getSchema());
//            String searchSchema = database.getJdbcSchemaName(example.getSchema());
//            String searchTableName = null;
//            if (((ForeignKey) example).getForeignKeyTable() != null) {
//                searchTableName = ((ForeignKey) example).getForeignKeyTable().getName();
//            }
//            String fkName = example.getName();
//
//            ResultSet rs = null;
//            try {
//                rs = getMetaDataFromCache(database).getForeignKeys(searchCatalog, searchSchema, searchTableName);
//                while (rs.next()) {
//                    if (fkName.equals(rs.getString("FK_NAME"))) {
//                        return true;
//                    }
//                }
//                return false;
//            } catch (SQLException e) {
//                throw new DatabaseException(e);
//            } finally {
//                if (rs != null) {
//                    try {
//                        rs.close();
//                    } catch (SQLException ignored) { }
//                }
//            }
//        } else {
//            return chain.has(example, snapshot);
//        }
//    }

    @Override
    protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        if (!snapshot.getSnapshotControl().shouldInclude(ForeignKey.class)) {
            return;
        }

        if (foundObject instanceof Table) {
            Table table = (Table) foundObject;
            Database database = snapshot.getDatabase();
            Schema schema;
            schema = table.getSchema();

            Set<String> seenFks = new HashSet<>();
            List<CachedRow> importedKeyMetadataResultSet;
            try {
                importedKeyMetadataResultSet = ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache().getForeignKeys(((AbstractJdbcDatabase) database)
                                .getJdbcCatalogName(schema), ((AbstractJdbcDatabase) database).getJdbcSchemaName(schema),
                        database.correctObjectName(table.getName(), Table.class), null);

                for (CachedRow row : importedKeyMetadataResultSet) {
                    ForeignKey fk = new ForeignKey().setName(row.getString("FK_NAME")).setForeignKeyTable(table);
                    if (seenFks.add(fk.getName())) {
                        table.getOutgoingForeignKeys().add(fk);
                    }
                }
            } catch (Exception e) {
                throw new DatabaseException(e);
            }
        }
    }

    @Override
    protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {

        Database database = snapshot.getDatabase();

        List<CachedRow> importedKeyMetadataResultSet;
        try {
            Table fkTable = ((ForeignKey) example).getForeignKeyTable();
            String searchCatalog = ((AbstractJdbcDatabase) database).getJdbcCatalogName(fkTable.getSchema());
            String searchSchema = ((AbstractJdbcDatabase) database).getJdbcSchemaName(fkTable.getSchema());
            String searchTableName = database.correctObjectName(fkTable.getName(), Table.class);

            importedKeyMetadataResultSet = ((JdbcDatabaseSnapshot) snapshot).getMetaDataFromCache().getForeignKeys(searchCatalog,
                    searchSchema, searchTableName, example.getName());
            ForeignKey foreignKey = null;
            for (CachedRow row : importedKeyMetadataResultSet) {
                String fk_name = cleanNameFromDatabase(row.getString("FK_NAME"), database);
                if (snapshot.getDatabase().isCaseSensitive()) {
                    if (!fk_name.equals(example.getName())) {
                        continue;
                    } else if (!fk_name.equalsIgnoreCase(example.getName())) {
                        continue;
                    }
                }

                if (foreignKey == null) {
                    foreignKey = new ForeignKey();
                }

                foreignKey.setName(fk_name);

                String fkTableCatalog = cleanNameFromDatabase(row.getString(METADATA_FKTABLE_CAT), database);
                String fkTableSchema = cleanNameFromDatabase(row.getString(METADATA_FKTABLE_SCHEM), database);
                String fkTableName = cleanNameFromDatabase(row.getString(METADATA_FKTABLE_NAME), database);
                Table foreignKeyTable = new Table().setName(fkTableName);
                foreignKeyTable.setSchema(new Schema(new Catalog(fkTableCatalog), fkTableSchema));

                foreignKey.setForeignKeyTable(foreignKeyTable);
                Column fkColumn = new Column(cleanNameFromDatabase(row.getString(METADATA_FKCOLUMN_NAME), database)).setRelation(foreignKeyTable);
                boolean alreadyAdded = false;
                for (Column existing : foreignKey.getForeignKeyColumns()) {
                    if (DatabaseObjectComparatorFactory.getInstance().isSameObject(existing, fkColumn, snapshot.getSchemaComparisons(), database)) {
                        alreadyAdded = true; //already added. One is probably an alias
                    }
                }
                if (alreadyAdded) {
                    break;
                }

                CatalogAndSchema pkTableSchema = ((AbstractJdbcDatabase) database).getSchemaFromJdbcInfo(
                        row.getString(METADATA_PKTABLE_CAT), row.getString(METADATA_PKTABLE_SCHEM));
                Table tempPkTable = (Table) new Table().setName(row.getString(METADATA_PKTABLE_NAME)).setSchema(
                        new Schema(pkTableSchema.getCatalogName(), pkTableSchema.getSchemaName()));
                foreignKey.setPrimaryKeyTable(tempPkTable);
                Column pkColumn = new Column(cleanNameFromDatabase(row.getString(METADATA_PKCOLUMN_NAME), database))
                        .setRelation(tempPkTable);

                foreignKey.addForeignKeyColumn(fkColumn);
                foreignKey.addPrimaryKeyColumn(pkColumn);
                //todo foreignKey.setKeySeq(importedKeyMetadataResultSet.getInt("KEY_SEQ"));

                // DB2 on z/OS doesn't support ON UPDATE
                if (!(database instanceof Db2zDatabase)) {
                    ForeignKeyConstraintType updateRule = convertToForeignKeyConstraintType(
                        row.getInt(METADATA_UPDATE_RULE), database);
                    foreignKey.setUpdateRule(updateRule);
                }
                ForeignKeyConstraintType deleteRule = convertToForeignKeyConstraintType(
                        row.getInt(METADATA_DELETE_RULE), database);
                foreignKey.setDeleteRule(deleteRule);

                short deferrability;
                if (((database instanceof MySQLDatabase) && ((MySQLDatabase) database)
                    .hasBugJdbcConstraintsDeferrable()) || ((database instanceof MariaDBDatabase) && (
                        (MariaDBDatabase) database).hasBugJdbcConstraintsDeferrable())
                   )
                    deferrability = DatabaseMetaData.importedKeyNotDeferrable;
                else
                    deferrability = row.getShort(METADATA_DEFERRABILITY);

                // Hsqldb doesn't handle setting this property correctly, it sets it to 0.
                // it should be set to DatabaseMetaData.importedKeyNotDeferrable(7)
                if ((deferrability == 0) || (deferrability == DatabaseMetaData.importedKeyNotDeferrable)) {
                    foreignKey.setDeferrable(false);
                    foreignKey.setInitiallyDeferred(false);
                } else if (deferrability == DatabaseMetaData.importedKeyInitiallyDeferred) {
                    foreignKey.setDeferrable(true);
                    foreignKey.setInitiallyDeferred(true);
                } else if (deferrability == DatabaseMetaData.importedKeyInitiallyImmediate) {
                    foreignKey.setDeferrable(true);
                    foreignKey.setInitiallyDeferred(false);
                } else {
                    throw new RuntimeException("Unknown deferrability result: " + deferrability);
                }
                setValidateOptionIfAvailable(database, foreignKey, row);
                if (database.createsIndexesForForeignKeys()) {
                    Index exampleIndex = new Index().setRelation(foreignKey.getForeignKeyTable());
                    exampleIndex.getColumns().addAll(foreignKey.getForeignKeyColumns());
                    exampleIndex.addAssociatedWith(Index.MARK_FOREIGN_KEY);
                    foreignKey.setBackingIndex(exampleIndex);
                }
            }
            if (snapshot.get(ForeignKey.class).contains(foreignKey)) {
                return null;
            }
            return foreignKey;
        } catch (Exception e) {
            throw new DatabaseException(e);
        }
    }

    /**
     * Method to map 'validate' option for FK. This thing works only for ORACLE
     *
     * @param database - DB where FK will be created
     * @param foreignKey - FK object to persist validate option
     * @param cachedRow - it's a cache-map to get metadata about FK
     */
    private void setValidateOptionIfAvailable(Database database, ForeignKey foreignKey, CachedRow cachedRow) {
        if (!(database instanceof OracleDatabase)) {
            return;
        }
        final String constraintValidate = cachedRow.getString("FK_VALIDATE");
        final String VALIDATE = "VALIDATED";
        if (constraintValidate!=null && !constraintValidate.isEmpty()) {
            foreignKey.setShouldValidate(VALIDATE.equals(cleanNameFromDatabase(constraintValidate.trim(), database)));
        }
    }

    protected ForeignKeyConstraintType convertToForeignKeyConstraintType(Integer jdbcType, Database database) throws DatabaseException {
        if (jdbcType == null) {
            return ForeignKeyConstraintType.importedKeyRestrict;
        }
        if (driverUsesSpFkeys(database)) {
            if (jdbcType == 0) {
                return ForeignKeyConstraintType.importedKeyCascade;
            } else if (jdbcType == 1) {
                return ForeignKeyConstraintType.importedKeyNoAction;
            } else if (jdbcType == 2) {
                return ForeignKeyConstraintType.importedKeySetNull;
            } else if (jdbcType == 3) {
                return ForeignKeyConstraintType.importedKeySetDefault;
            } else {
                throw new DatabaseException("Unknown constraint type: " + jdbcType);
            }
        } else {
            if (jdbcType == DatabaseMetaData.importedKeyCascade) {
                return ForeignKeyConstraintType.importedKeyCascade;
            } else if (jdbcType == DatabaseMetaData.importedKeyNoAction) {
                return ForeignKeyConstraintType.importedKeyNoAction;
            } else if (jdbcType == DatabaseMetaData.importedKeyRestrict) {
                if (database instanceof MSSQLDatabase) {
                    //mssql doesn't support restrict. Not sure why it comes back with this type sometimes
                    return ForeignKeyConstraintType.importedKeyNoAction;
                } else {
                return ForeignKeyConstraintType.importedKeyRestrict;
                }
            } else if (jdbcType == DatabaseMetaData.importedKeySetDefault) {
                return ForeignKeyConstraintType.importedKeySetDefault;
            } else if (jdbcType == DatabaseMetaData.importedKeySetNull) {
                return ForeignKeyConstraintType.importedKeySetNull;
            } else {
                throw new DatabaseException("Unknown constraint type: " + jdbcType);
            }
        }
    }

    /*
    * Sql server JDBC drivers prior to 6.3.3 used sp_fkeys to determine the delete/cascade metadata.
    * The sp_fkeys stored procedure spec says that returned integer values of 0, 1, 2, or 4
    * translate to cascade, noAction, SetNull, or SetDefault which are not the values in the JDBC
    * standard.
    *
    * If this method returns true, the sp_fkeys values should be used. Otherwise use the standard jdbc logic
    *
    * The change in logic went in with https://github.com/Microsoft/mssql-jdbc/pull/490
    */
    private boolean driverUsesSpFkeys(Database database) throws DatabaseException {
        if (!(database instanceof MSSQLDatabase)) {
            return false;
        }
        DatabaseConnection connection = database.getConnection();
        if (!(connection instanceof JdbcConnection)) {
            return false;
        }

        try {
            DatabaseMetaData metaData = ((JdbcConnection) connection).getMetaData();
            int driverMajorVersion = metaData.getDriverMajorVersion();
            int driverMinorVersion= metaData.getDriverMinorVersion();
            String driverName = metaData.getDriverName();

            if (!driverName.startsWith("Microsoft")) {
                return false;
            }

            if (driverMajorVersion > 6 || (driverMajorVersion == 6 && driverMinorVersion >= 3)) {
                return false;
            }

            return true;
        } catch (SQLException e) {
            throw new DatabaseException(e);
        }
    }

    //from SQLiteDatabaseSnapshotGenerator
    //    protected void readForeignKeys(DatabaseSnapshot snapshot, String schema, DatabaseMetaData databaseMetaData) throws DatabaseException, SQLException {
//        updateListeners("Reading foreign keys for " + snapshot.getDatabase().toString() + " ...");
//        // Foreign keys are not supported in SQLite until now.
//        // ...do nothing here
//    }

    //Code from OracleDatabaseSnapshotGenerator
//    public List<ForeignKey> getAdditionalForeignKeys(String schemaName, Database database) throws DatabaseException {
//        List<ForeignKey> foreignKeys = super.getAdditionalForeignKeys(schemaName, database);
//
//        // Setting default schema name. Needed for correct statement generation
//        if (schemaName == null) {
//            schemaName = database.convertRequestedSchemaToSchema(schemaName);
//        }
//
//        // Create SQL statement to select all FKs in database which referenced to unique columns
//        String query = "select uc_fk.constraint_name FK_NAME,uc_fk.owner FKTABLE_SCHEM,ucc_fk.table_name FKTABLE_NAME,ucc_fk.column_name FKCOLUMN_NAME,decode(uc_fk.deferrable, 'DEFERRABLE', 5 ,'NOT DEFERRABLE', 7 , 'DEFERRED', 6 ) DEFERRABILITY, decode(uc_fk.delete_rule, 'CASCADE', 0,'NO ACTION', 3) DELETE_RULE,ucc_rf.table_name PKTABLE_NAME,ucc_rf.column_name PKCOLUMN_NAME from all_cons_columns ucc_fk,all_constraints uc_fk,all_cons_columns ucc_rf,all_constraints uc_rf where uc_fk.CONSTRAINT_NAME = ucc_fk.CONSTRAINT_NAME and uc_fk.constraint_type='R' and uc_fk.r_constraint_name=ucc_rf.CONSTRAINT_NAME and uc_rf.constraint_name = ucc_rf.constraint_name and uc_rf.constraint_type = 'U' and uc_fk.owner = '" + schemaName + "' and ucc_fk.owner = '" + schemaName + "' and uc_rf.owner = '" + schemaName + "' and ucc_rf.owner = '" + schemaName + "'";
//        Statement statement = null;
//        ResultSet rs = null;
//        try {
//            statement = ((JdbcConnection) database.getConnection()).getUnderlyingConnection().createStatement();
//            rs = statement.executeQuery(query);
//            while (rs.next()) {
//                ForeignKeyInfo fkInfo = new ForeignKeyInfo();
//                fkInfo.setReferencesUniqueColumn(true);
//                fkInfo.setFkName(cleanObjectNameFromDatabase(rs.getString("FK_NAME")));
//                fkInfo.setFkSchema(cleanObjectNameFromDatabase(rs.getString("FKTABLE_SCHEM")));
//                fkInfo.setFkTableName(cleanObjectNameFromDatabase(rs.getString("FKTABLE_NAME")));
//                fkInfo.setFkColumn(cleanObjectNameFromDatabase(rs.getString("FKCOLUMN_NAME")));
//
//                fkInfo.setPkTableName(cleanObjectNameFromDatabase(rs.getString("PKTABLE_NAME")));
//                fkInfo.setPkColumn(cleanObjectNameFromDatabase(rs.getString("PKCOLUMN_NAME")));
//
//                fkInfo.setDeferrablility(rs.getShort("DEFERRABILITY"));
//                ForeignKeyConstraintType deleteRule = convertToForeignKeyConstraintType(rs.getInt("DELETE_RULE"));
//                if (rs.wasNull()) {
//                    deleteRule = null;
//                }
//                fkInfo.setDeleteRule(deleteRule);
//                foreignKeys.add(generateForeignKey(fkInfo, database, foreignKeys));
//            }
//        } catch (SQLException e) {
//            throw new DatabaseException("Can't execute selection query to generate list of foreign keys", e);
//        } finally {
//            JdbcUtils.closeResultSet(rs);
//            JdbcUtils.closeStatement(statement);
//        }
//        return foreignKeys;
//    }
//

//
//
}


package liquibase.sqlgenerator.core;

import liquibase.Scope;
import liquibase.database.Database;
import liquibase.database.core.InformixDatabase;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.sql.Sql;
import liquibase.sql.UnparsedSql;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.statement.AutoIncrementConstraint;
import liquibase.statement.ForeignKeyConstraint;
import liquibase.statement.PrimaryKeyConstraint;
import liquibase.statement.UniqueConstraint;
import liquibase.statement.core.CreateTableStatement;
import liquibase.structure.core.Schema;
import liquibase.structure.core.Table;
import liquibase.util.StringUtil;

import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * An Informix-specific create table statement generator.
 *
 * @author islavov
 */
public class CreateTableGeneratorInformix extends CreateTableGenerator {

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

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

    /**
     * Informix SQL-specific implementation of the CREATE TABLE SQL generator.
     * @param statement The properties of the statement that we will translate into SQL
     * @param database For this implementation always an object of the InformixDatabase type
     * @param sqlGeneratorChain Other generators in the pipeline for this command
     * @return An array of Sql[] statements containing the requested SQL statements for Informix SQL
     */
    @Override
    public Sql[] generateSql(CreateTableStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
        StringBuilder buffer = new StringBuilder();

        // CREATE TABLE table_name ...
        buffer.append("CREATE TABLE ")
                .append(database.escapeTableName(statement.getCatalogName(), statement.getSchemaName(), statement.getTableName()))
                .append(" ");
        buffer.append("(");

        Iterator<String> columnIterator = statement.getColumns().iterator();
        List<String> primaryKeyColumns = new LinkedList<>();

        /*
         * Build the list of columns and constraints in the form
         * (
         *   column1,
         *   ...,
         *   columnN,
         *   constraint1,
         *   ...,
         *   constraintN
         * )
         */
        while (columnIterator.hasNext()) {
            String column = columnIterator.next();

            buffer.append(database.escapeColumnName(statement.getCatalogName(), statement.getSchemaName(), statement.getTableName(), column));
            buffer.append(" ").append(statement.getColumnTypes().get(column).toDatabaseDataType(database).toSql());

            AutoIncrementConstraint autoIncrementConstraint = null;

            for (AutoIncrementConstraint currentAutoIncrementConstraint : statement.getAutoIncrementConstraints()) {
                if (column.equals(currentAutoIncrementConstraint.getColumnName())) {
                    autoIncrementConstraint = currentAutoIncrementConstraint;
                    break;
                }
            }

            boolean isAutoIncrementColumn = autoIncrementConstraint != null;
            boolean isPrimaryKeyColumn = (statement.getPrimaryKeyConstraint() != null) && statement
                .getPrimaryKeyConstraint().getColumns().contains(column);

            if (isPrimaryKeyColumn) {
                primaryKeyColumns.add(column);
            }

            if (statement.getDefaultValue(column) != null) {
                Object defaultValue = statement.getDefaultValue(column);
                buffer.append(" DEFAULT ");
                buffer.append(statement.getColumnTypes().get(column).objectToSql(defaultValue, database));
            }

            if (isAutoIncrementColumn) {
                // TODO: check if database supports auto increment on non primary key column
                if (database.supportsAutoIncrement()) {
                    String autoIncrementClause = database.getAutoIncrementClause(autoIncrementConstraint.getStartWith(), autoIncrementConstraint.getIncrementBy());

                    if (!autoIncrementClause.isEmpty()) {
                        buffer.append(" ").append(autoIncrementClause);
                    }
                } else {
                    Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, database.getShortName()+" does not support autoincrement columns as requested for "+(database.escapeTableName(statement.getCatalogName(), statement.getSchemaName(), statement.getTableName())));
                }
            }

            if (statement.getNotNullColumns().get(column) != null) {
                buffer.append(" NOT NULL");
            }

            if (columnIterator.hasNext()) {
                buffer.append(", ");
            }
        }

        buffer.append(",");

        /*
         * We only create a PRIMARY KEY constraint if one is defined and has at least 1 column.
         * General Informix SQL syntax, according to the docs for 11.5
         * https://www.ibm.com/support/knowledgecenter/SSGU8G_11.50.0/com.ibm.sqls.doc/ids_sqs_0100.htm
         * is:
         * ( columns ... --> PRIMARY KEY (column1, ..., columnN) [CONSTRAINT pk_name]
          */
        //

        PrimaryKeyConstraint pkConstraint = statement.getPrimaryKeyConstraint();
        if ((statement.getPrimaryKeyConstraint() != null) && !statement.getPrimaryKeyConstraint().getColumns().isEmpty()) {
            buffer.append(" PRIMARY KEY (");
            buffer.append(StringUtil.join(primaryKeyColumns, ", "));
            buffer.append(")");

            if (! StringUtil.isEmpty(pkConstraint.getConstraintName() )) {
                buffer.append(" CONSTRAINT ");
                buffer.append(database.escapeConstraintName(pkConstraint.getConstraintName()));
            }
            // Setting up table space for PK's index if it exist
            buffer.append(",");
        }

        for (ForeignKeyConstraint fkConstraint : statement.getForeignKeyConstraints()) {
            String referencesString = fkConstraint.getReferences();
            if (!referencesString.contains(".") && (database.getDefaultSchemaName() != null)) {
                referencesString = database.getDefaultSchemaName()+"."+referencesString;
            }
            buffer.append(" FOREIGN KEY (")
                .append(database.escapeColumnName(statement.getCatalogName(), statement.getSchemaName(), statement.getTableName(), fkConstraint.getColumn()))
                .append(") REFERENCES ")
                .append(referencesString);

            if (fkConstraint.isDeleteCascade()) {
                buffer.append(" ON DELETE CASCADE");
            }

            buffer.append(" CONSTRAINT ");
            buffer.append(database.escapeConstraintName(fkConstraint.getForeignKeyName()));

            if (fkConstraint.isInitiallyDeferred()) {
                buffer.append(" INITIALLY DEFERRED");
            }
            if (fkConstraint.isDeferrable()) {
                buffer.append(" DEFERRABLE");
            }
            buffer.append(",");
        }

        // TODO: code duplication. Maybe we can merge this whole class into CreateTableGenerator again.
        for (UniqueConstraint uniqueConstraint : statement.getUniqueConstraints()) {
            if ((uniqueConstraint.getConstraintName() != null) && !constraintNameAfterUnique(database)) {
                buffer.append(" CONSTRAINT ");
                buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
            }
            buffer.append(" UNIQUE (");
            buffer.append(database.escapeColumnNameList(StringUtil.join(uniqueConstraint.getColumns(), ", ")));
            buffer.append(")");
            if ((uniqueConstraint.getConstraintName() != null) && constraintNameAfterUnique(database)) {
                buffer.append(" CONSTRAINT ");
                buffer.append(database.escapeConstraintName(uniqueConstraint.getConstraintName()));
            }
            buffer.append(",");
        }

        /*
         * Here, the list of columns and constraints in the form
         * ( column1, ..., columnN, constraint1, ..., constraintN,
         * ends. We cannot leave an expression like ", )", so we remove the last comma.
         */
        String sql = buffer.toString().replaceFirst(",\\s*$", "") + ")";

        if ((statement.getTablespace() != null) && database.supportsTablespaces()) {
            sql += " IN " + statement.getTablespace();
        }

        return new Sql[] { new UnparsedSql(sql, new Table().setName(statement.getTableName()).setSchema(new Schema(statement.getCatalogName(), statement.getSchemaName()))) };
    }

    private boolean constraintNameAfterUnique(Database database) {
        return true;
    }
}


package liquibase.integration.cdi;

import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.integration.cdi.annotations.LiquibaseType;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.resource.ResourceAccessor;
import liquibase.util.LiquibaseUtil;
import liquibase.util.NetUtil;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.spi.Extension;
import javax.inject.Inject;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Map;

/**
 * A CDI wrapper for Liquibase.
 *

 * Example Configuration:
 *

 *

 * This CDI configuration example will cause liquibase to run
 * automatically when the CDI container is initialized. It will load
 * db-changelog.xml from the classpath and apply it against
 * myDataSource.
 *

 * Various producers methods are required to resolve the dependencies, i.e.
 *

 * {@code
 *
 * public class CDILiquibaseProducer {
 *
 *  @literal @Produces @LiquibaseType
 *   public CDILiquibaseConfig createConfig() {
 *      CDILiquibaseConfig config = new CDILiquibaseConfig();
 *      config.setChangeLog("liquibase/parser/core/xml/simpleChangeLog.xml");
 *      return config;
 *   }
 *
 *  @literal @Produces @LiquibaseType
 *   public DataSource createDataSource() throws SQLException {
 *      jdbcDataSource ds = new jdbcDataSource();
 *      ds.setDatabase("jdbc:hsqldb:mem:test");
 *      ds.setUser("sa");
 *      ds.setPassword("");
 *      return ds;
 *   }
 *
 *  @literal @Produces @LiquibaseType
 *   public ResourceAccessor create() {
 *      return new ClassLoaderResourceAccessor(getClass().getClassLoader());
 *   }
 *
 * }
 *
 * }
 *

 * @author Aaron Walker (http://github.com/aaronwalker)
 */
@ApplicationScoped
public class CDILiquibase implements Extension {

    @Inject
    @LiquibaseType
    ResourceAccessor resourceAccessor;

    @Inject @LiquibaseType
    private CDILiquibaseConfig config;
    @Inject @LiquibaseType
    private DataSource dataSource;
    private boolean initialized;
    private boolean updateSuccessful;

    public boolean isInitialized() {
        return initialized;
    }

    public boolean isUpdateSuccessful() {
        return updateSuccessful;
    }

    @PostConstruct
    public void onStartup() {
        Logger log = Scope.getCurrentScope().getLog(getClass());

        log.info(LogType.LOG, "Booting Liquibase " + LiquibaseUtil.getBuildVersion());
        String hostName;
        try {
            hostName = NetUtil.getLocalHostName();
        } catch (Exception e) {
            log.warning(LogType.LOG, "Cannot find hostname: " + e.getMessage());
            log.fine(LogType.LOG, "", e);
            return;
        }

        LiquibaseConfiguration liquibaseConfiguration = LiquibaseConfiguration.getInstance();
        if (!liquibaseConfiguration.getConfiguration(GlobalConfiguration.class).getShouldRun()) {
            log.info(LogType.LOG, String.format("Liquibase did not run on %s because %s was set to false.",
                    hostName,
                liquibaseConfiguration.describeValueLookupLogic(
                    GlobalConfiguration.class, GlobalConfiguration.SHOULD_RUN)
            ));
            return;
        }
        if (!config.getShouldRun()) {
            log.info(LogType.LOG, String.format("Liquibase did not run on %s because CDILiquibaseConfig.shouldRun was set to false.", hostName));
            return;
        }
        initialized = true;
        try {
            performUpdate();
        } catch (LiquibaseException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    private void performUpdate() throws LiquibaseException {
        Connection c = null;
        Liquibase liquibase = null;
        try {
            c = dataSource.getConnection();
            liquibase = createLiquibase(c);
            liquibase.update(new Contexts(config.getContexts()), new LabelExpression(config.getLabels()));
            updateSuccessful = true;
        } catch (SQLException e) {
            throw new DatabaseException(e);
        } catch (LiquibaseException ex) {
            updateSuccessful = false;
            throw ex;
        } finally {
            if ((liquibase != null) && (liquibase.getDatabase() != null)) {
                liquibase.getDatabase().close();
            } else if (c != null) {
                try {
                    c.rollback();
                    c.close();
                } catch (SQLException e) {
                    //nothing to do
                }

            }

        }
    }

    protected Liquibase createLiquibase(Connection c) throws LiquibaseException {
        Liquibase liquibase = new Liquibase(config.getChangeLog(), resourceAccessor, createDatabase(c));
        if (config.getParameters() != null) {
            for(Map.Entry<String, String> entry: config.getParameters().entrySet()) {
                liquibase.setChangeLogParameter(entry.getKey(), entry.getValue());
            }
        }

        if (config.isDropFirst()) {
            liquibase.dropAll();
        }

        return liquibase;
    }

    /**
     * Subclasses may override this method add change some database settings such as
     * default schema before returning the database object.
     * @param c
     * @return a Database implementation retrieved from the {@link liquibase.database.DatabaseFactory}.
     * @throws DatabaseException
     */
    protected Database createDatabase(Connection c) throws DatabaseException {
        Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(c));
        if (config.getDefaultSchema() != null) {
            database.setDefaultSchemaName(config.getDefaultSchema());
        }
        return database;
    }
}


package liquibase.snapshot;

import liquibase.database.Database;
import liquibase.diff.output.ObjectChangeFilter;
import liquibase.exception.UnexpectedLiquibaseException;
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.core.DatabaseObjectFactory;

import java.util.*;

/**
 * Allows the class user to influence various aspects of the database object snapshot generation, e.g.
 * what types of database objects they want.
 */
public class SnapshotControl implements LiquibaseSerializable {

    private Set<Class<? extends DatabaseObject>> types;
    private ObjectChangeFilter objectChangeFilter;
    private SnapshotListener snapshotListener;
    private boolean warnIfObjectNotFound = true;

    /**
     * Create a SnapshotControl for a given database and mark the database's standard types for inclusion.
     * @param database the DBMS for which snapshots should be generated
     */
    public SnapshotControl(Database database) {
        setTypes(DatabaseObjectFactory.getInstance().getStandardTypes(), database);
    }

    /**
     * Create a Snapshot control for a given database, but explicitly set the object types to be included in snapshots.
     * @param database the DBMS for which snapshots should be generated
     * @param types the list of object types to be included in the snapshot
     */
    public SnapshotControl(Database database, Class<? extends DatabaseObject>... types) {
        this(database, true, types);
    }

    public SnapshotControl(Database database, boolean expandTypesIfNeeded, Class<? extends DatabaseObject>... types) {
        if ((types == null) || (types.length == 0)) {
            setTypes(DatabaseObjectFactory.getInstance().getStandardTypes(), database);
        } else {
            if (expandTypesIfNeeded) {
                setTypes(new HashSet<>(Arrays.asList(types)), database);
            } else {
                this.types = new HashSet<>(Arrays.asList(types));
            }
        }
    }

    /**
     * Create a Snapshot control for a given database, but explicitly set the object types to be included in snapshots.
     * @param database the DBMS for which snapshots should be generated
     * @param types the list of object types to be included in the snapshot, separated by commas
     */
    public SnapshotControl(Database database, String types) {
        setTypes(DatabaseObjectFactory.getInstance().parseTypes(types), database);
    }

    public SnapshotControl(Database database, ObjectChangeFilter objectChangeFilter, Class<? extends
            DatabaseObject>... types) {
        this(database, true, types);

        this.objectChangeFilter = objectChangeFilter;
    }

    public SnapshotListener getSnapshotListener() {
        return snapshotListener;
    }

    public void setSnapshotListener(SnapshotListener snapshotListener) {
        this.snapshotListener = snapshotListener;
    }

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

    @Override
    public Set<String> getSerializableFields() {
        return new HashSet<>(Arrays.asList("includedType"));
    }

    @Override
    public Object getSerializableFieldValue(String field) {
        if ("includedType".equals(field)) {
            SortedSet<String> types = new TreeSet<>();
            for (Class type : this.getTypesToInclude()) {
                types.add(type.getName());
            }
            return types;
        } else {
            throw new UnexpectedLiquibaseException("Unknown field "+field);
        }
    }

    @Override
    public SerializationType getSerializableFieldType(String field) {
        if ("includedType".equals(field)) {
            return SerializationType.NESTED_OBJECT;
        } else {
            throw new UnexpectedLiquibaseException("Unknown field "+field);
        }
    }

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

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

    private void setTypes(Set<Class<? extends DatabaseObject>> types, Database database) {
        this.types = new HashSet<>();
        for (Class<? extends DatabaseObject> type : types) {
            addType(type, database);
        }
    }

    /**
     * Adds a new DatabaseObject type to the list of object types to be included in snapshots.
     * @param type The type to be added
     * @param database The database to check for any dependent types that need to be included as well
     * @return true if the type was added to the list, false if it was already present.
     */
    public boolean addType(Class<? extends DatabaseObject> type, Database database) {
        boolean added = this.types.add(type);
        if (added) {
            for (Class<? extends DatabaseObject> container : SnapshotGeneratorFactory.getInstance().getContainerTypes(type, database)) {
                addType(container, database);
            }
        }
        return added;
    }

    /**
     * Return the types to be included in snapshots
     * @return the set of currently registered types
     */
    public Set<Class<? extends DatabaseObject>> getTypesToInclude() {
        return types;
    }

    /**
     * Queries the currently registered list of types to be included and returns true if the given type is in that list
     * @param type the DatabaseObject type to be checked
     * @return true if that type is registered for inclusion, false if not
     */
    public boolean shouldInclude(Class<? extends DatabaseObject> type) {
        return types.contains(type);
    }

    @Override
    public void load(ParsedNode parsedNode, ResourceAccessor resourceAccessor) throws ParsedNodeException {
        throw new RuntimeException("TODO");
    }

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

    /**
     * Returns if the code should log a LogLevel.WARNING message if the object to be snapshotted could not be found.
     * @return true if WARNINGs should be emitted (default), false if not.
     */
    public boolean isWarnIfObjectNotFound() {
        return warnIfObjectNotFound;
    }

    /**
     * Configures the code to log a LogLevel.WARNING message if the object to be snapshotted could not be found.
     * @param warnIfObjectNotFound true if a warning should emitted (default value), false if not.
     */
    public SnapshotControl setWarnIfObjectNotFound(boolean warnIfObjectNotFound) {
        this.warnIfObjectNotFound = warnIfObjectNotFound;
        return this;
    }

    public <T extends DatabaseObject> boolean shouldInclude(T example) {
        if (objectChangeFilter != null) {
            return objectChangeFilter.include(example);
        }
        return shouldInclude(example.getClass());
    }
}


package liquibase.change;

import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.core.MSSQLDatabase;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.exception.ValidationErrors;
import liquibase.exception.Warnings;
import liquibase.logging.LogType;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.RawSqlStatement;
import liquibase.util.StringUtil;

import java.io.*;
import java.util.ArrayList;
import java.util.List;

/**
 * A common parent for all raw SQL related changes regardless of where the sql was sourced from.
 *
 * Implements the necessary logic to choose how the SQL string should be parsed to generate the statements.
 *
 */
public abstract class AbstractSQLChange extends AbstractChange implements DbmsTargetedChange {

    private boolean stripComments;
    private boolean splitStatements;
    private String endDelimiter;
    private String sql;
    private String dbms;

    protected String encoding;

    protected AbstractSQLChange() {
        setStripComments(null);
        setSplitStatements(null);
    }

    public InputStream openSqlStream() throws IOException {
        return null;
    }

    @Override
    @DatabaseChangeProperty(since = "3.0", exampleValue = "h2, oracle")
    public String getDbms() {
        return dbms;
    }

    @Override
    public void setDbms(final String dbms) {
        this.dbms = dbms;
    }

    /**
     * {@inheritDoc}
     * @param database
     * @return always true (in AbstractSQLChange)
     */
    @Override
    public boolean supports(Database database) {
        return true;
    }

    @Override
    public Warnings warn(Database database) {
        return new Warnings();
    }

    @Override
    public ValidationErrors validate(Database database) {
        ValidationErrors validationErrors = new ValidationErrors();
        if (StringUtil.trimToNull(sql) == null) {
            validationErrors.addError("'sql' is required");
        }
        return validationErrors;

    }

    /**
     * Return if comments should be stripped from the SQL before passing it to the database.
     *

     * This will always return a non-null value and should be a boolean rather than a Boolean, but that breaks the Bean Standard.
     */
    @DatabaseChangeProperty(description = "Set to true to remove any comments in the SQL before executing, otherwise false. Defaults to false if not set")
    public Boolean isStripComments() {
        return stripComments;
    }

    /**
     * Return true if comments should be stripped from the SQL before passing it to the database.
     * Passing null sets stripComments to the default value (false).
     */
    public void setStripComments(Boolean stripComments) {
        if (stripComments == null) {
            this.stripComments = false;
        } else {
            this.stripComments = stripComments;
        }
    }

    /**
     * Return if the SQL should be split into multiple statements before passing it to the database.
     * By default, statements are split around ";" and "go" delimiters.
     *

     * This will always return a non-null value and should be a boolean rather than a Boolean, but that breaks the Bean Standard.
     */
    @DatabaseChangeProperty(description = "Set to false to not have liquibase split statements on ;'s and GO's. Defaults to true if not set")
    public Boolean isSplitStatements() {
        return splitStatements;
    }

    /**
     * Set whether SQL should be split into multiple statements.
     * Passing null sets stripComments to the default value (true).
     */
    public void setSplitStatements(Boolean splitStatements) {
        if (splitStatements == null) {
            this.splitStatements = true;
        } else {
            this.splitStatements = splitStatements;
        }
    }

    /**
     * Return the raw SQL managed by this Change
     */
    @DatabaseChangeProperty(serializationType = SerializationType.DIRECT_VALUE)
    public String getSql() {
        return sql;
    }

    /**
     * Set the raw SQL managed by this Change. The passed sql is trimmed and set to null if an empty string is passed.
     */
    public void setSql(String sql) {
       this.sql = StringUtil.trimToNull(sql);
    }

    /**
     * Set the end delimiter used to split statements. Will return null if the default delimiter should be used.
     *
     * @see #splitStatements
     */
    @DatabaseChangeProperty(description = "Delimiter to apply to the end of the statement. Defaults to ';', may be set to ''.", exampleValue = "\\nGO")
    public String getEndDelimiter() {
        return endDelimiter;
    }

    /**
     * Set the end delimiter for splitting SQL statements. Set to null to use the default delimiter.
     * @param endDelimiter
     */
    public void setEndDelimiter(String endDelimiter) {
        this.endDelimiter = endDelimiter;
    }

    /**
     * Calculates the checksum based on the contained SQL.
     *
     * @see liquibase.change.AbstractChange#generateCheckSum()
     */
    @Override
    public CheckSum generateCheckSum() {
        InputStream stream = null;
        try {
            stream = openSqlStream();

            String sql = this.sql;
            if ((stream == null) && (sql == null)) {
                sql = "";
            }

            if (sql != null) {
                stream = new ByteArrayInputStream(
                    sql.getBytes(
                        LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class)
                        .getOutputEncoding()
                    )
                );
            }

            return CheckSum.compute(new NormalizingStream(this.getEndDelimiter(), this.isSplitStatements(), this.isStripComments(), stream), false);
        } catch (IOException e) {
            throw new UnexpectedLiquibaseException(e);
        } finally {
            if (stream != null) {
                try {
                    stream.close();
                } catch (IOException e) {
                    Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, "Error closing stream", e);
                }
            }
        }
    }

    /**
     * Generates one or more SqlStatements depending on how the SQL should be parsed.
     * If split statements is set to true then the SQL is split and each command is made into a separate SqlStatement.
     *

     * If stripping comments is true then any comments are removed before the splitting is executed.
     * The set SQL is passed through the {@link java.sql.Connection#nativeSQL} method if a connection is available.
     */
    @Override
    public SqlStatement[] generateStatements(Database database) {

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

        String sql = StringUtil.trimToNull(getSql());
        if (sql == null) {
            return new SqlStatement[0];
        }

        String processedSQL = normalizeLineEndings(sql);
        for (String statement : StringUtil.processMutliLineSQL(processedSQL, isStripComments(), isSplitStatements(), getEndDelimiter())) {
            if (database instanceof MSSQLDatabase) {
                 statement = statement.replaceAll("\\n", "\r\n");
             }

            String escapedStatement = statement;
            try {
                if (database.getConnection() != null) {
                    escapedStatement = database.getConnection().nativeSQL(statement);
                }
            } catch (DatabaseException e) {
                escapedStatement = statement;
            }

            returnStatements.add(new RawSqlStatement(escapedStatement, getEndDelimiter()));
        }

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

    @Override
    public boolean generateStatementsVolatile(Database database) {
        return false;
    }

    @Override
    public boolean generateRollbackStatementsVolatile(Database database) {
        return false;
    }

    @Override
    public ChangeStatus checkStatus(Database database) {
        return new ChangeStatus().unknown("Cannot check raw sql status");
    }

    protected String normalizeLineEndings(String string) {
        return string.replace("\r", "");
    }

    public static class NormalizingStream extends InputStream {
        private ByteArrayInputStream headerStream;
        private PushbackInputStream stream;

        private byte[] quickBuffer = new byte[100];
        private List<Byte> resizingBuffer = new ArrayList<>();

        private int lastChar = 'X';
        private boolean seenNonSpace;

        public NormalizingStream(String endDelimiter, Boolean splitStatements, Boolean stripComments, InputStream stream) {
            this.stream = new PushbackInputStream(stream, 2048);
            try {
                this.headerStream = new ByteArrayInputStream((endDelimiter+":"+splitStatements+":"+stripComments+":").getBytes(LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()));
            } catch (UnsupportedEncodingException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }

        @Override
        public int read() throws IOException {
            if (headerStream != null) {
                int returnChar = headerStream.read();
                if (returnChar != -1) {
                    return returnChar;
                }
                headerStream = null;
            }

            int returnChar = stream.read();
            if (isWhiteSpace(returnChar)) {
                returnChar = ' ';
            }

            while ((returnChar == ' ') && (!seenNonSpace || (lastChar == ' '))) {
                returnChar = stream.read();

                if (isWhiteSpace(returnChar)) {
                    returnChar = ' ';
                }
            }

            seenNonSpace = true;

            lastChar = returnChar;

            if ((lastChar == ' ') && isOnlyWhitespaceRemaining()) {
                return -1;
            }

            return returnChar;
        }

        @Override
        public int available() throws IOException {
            return stream.available();
        }

        @Override
        public boolean markSupported() {
            return stream.markSupported();
        }

        @Override
        public void mark(int readlimit) {
            stream.mark(readlimit);
        }

        @Override
        public void reset() throws IOException {
            stream.reset();
        }

        private boolean isOnlyWhitespaceRemaining() throws IOException {
            try {
                int quickBufferUsed = 0;
                while (true) {
                    byte read = (byte) stream.read();
                    if (quickBufferUsed >= quickBuffer.length) {
                        resizingBuffer.add(read);
                    } else {
                        quickBuffer[quickBufferUsed++] = read;
                    }

                    if (read == -1) {
                        return true;
                    }
                    if (!isWhiteSpace(read)) {
                        if (!resizingBuffer.isEmpty()) {

                            byte[] buf = new byte[resizingBuffer.size()];
                            for (int i=0; i< resizingBuffer.size(); i++) {
                                buf[i] = resizingBuffer.get(i);
                            }

                            stream.unread(buf);
                        }

                        stream.unread(quickBuffer, 0, quickBufferUsed);
                        return false;
                    }
                }
            } finally {
                resizingBuffer.clear();
            }
        }

        private boolean isWhiteSpace(int read) {
            return (read == ' ') || (read == '\n') || (read == '\r') || (read == '\t');
        }

        @Override
        public void close() throws IOException {
            stream.close();
        }
    }
}


package liquibase.integration.spring;

import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.Scope;
import liquibase.configuration.ConfigurationProperty;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.DatabaseFactory;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.LiquibaseException;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.util.StringUtil;
import liquibase.util.file.FilenameUtils;
import org.springframework.beans.factory.BeanNameAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.core.io.support.ResourcePatternUtils;

import javax.sql.DataSource;
import java.io.*;
import java.net.URLConnection;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.jar.Attributes;
import java.util.jar.Manifest;

/**
 * A Spring-ified wrapper for Liquibase.
 *

 * Example Configuration:
 *

 *

 * This Spring configuration example will cause liquibase to run automatically when the Spring context is
 * initialized. It will load db-changelog.xml from the classpath and apply it against
 * myDataSource.
 *

 *

 *
 *

 * <bean id="myLiquibase"
 *          class="liquibase.spring.SpringLiquibase"
 *          >
 *
 *      <property name="dataSource" ref="myDataSource" />
 *
 *      <property name="changeLog" value="classpath:db-changelog.xml" />
 *
 * </bean>
 *
 *

 *
 * @author Rob Schoening
 */
public class SpringLiquibase implements InitializingBean, BeanNameAware, ResourceLoaderAware {

    protected final Logger log = Scope.getCurrentScope().getLog(SpringLiquibase.class);
    protected String beanName;

	protected ResourceLoader resourceLoader;

	protected DataSource dataSource;
	protected String changeLog;
	protected String contexts;
    protected String labels;
    protected String tag;
	protected Map<String, String> parameters;
	protected String defaultSchema;
	protected String liquibaseSchema;
	protected String databaseChangeLogTable;
	protected String databaseChangeLogLockTable;
	protected String liquibaseTablespace;
	protected boolean dropFirst;
	protected boolean clearCheckSums;
	protected boolean shouldRun = true;
	protected File rollbackFile;

	/**
     * Ignores classpath prefix during changeset comparison.
     * This is particularly useful if Liquibase is run in different ways.
     *
     * For instance, if Maven plugin is used to run changesets, as in:
     *
     *      <configuration>
     *          ...
     *          <changeLogFile>path/to/changelog</changeLogFile>
     *      </configuration>
     *
     *
     * And {@link SpringLiquibase} is configured like:
     *
     *     SpringLiquibase springLiquibase = new SpringLiquibase();
     *     springLiquibase.setChangeLog("classpath:path/to/changelog");
     *
     *
     * or, in equivalent XML configuration:
     *
     *     <bean id="springLiquibase" class="liquibase.integration.spring.SpringLiquibase">
     *         <property name="changeLog" value="path/to/changelog" />
     *      </bean>
     *
     *
     * {@link Liquibase#listUnrunChangeSets(Contexts, )} will
     * always, by default, return changesets, regardless of their
     * execution by Maven.
     * Maven-executed changeset path name are not be prepended by
     * "classpath:" whereas the ones parsed via SpringLiquibase are.
     *
     * To avoid this issue, just set ignoreClasspathPrefix to true.
     */
    private boolean ignoreClasspathPrefix = true;

	protected boolean testRollbackOnUpdate = false;

	public SpringLiquibase() {
		super();
	}

	public boolean isDropFirst() {
		return dropFirst;
	}

	public void setDropFirst(boolean dropFirst) {
		this.dropFirst = dropFirst;
	}

	public boolean isClearCheckSums() {
		return clearCheckSums;
	}

	public void setClearCheckSums(boolean clearCheckSums) {
		this.clearCheckSums = clearCheckSums;
	}

	public void setShouldRun(boolean shouldRun) {
		this.shouldRun = shouldRun;
	}

	public String getDatabaseProductName() throws DatabaseException {
		Connection connection = null;
        Database database = null;
		String name = "unknown";
		try {
			connection = getDataSource().getConnection();
			database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
			name = database.getDatabaseProductName();
		} catch (SQLException e) {
			throw new DatabaseException(e);
		} finally {
            if (database != null) {
                database.close();
            } else if (connection != null) {
				try {
					if (!connection.getAutoCommit()) {
						connection.rollback();
					}
					connection.close();
                } catch (SQLException e) {
					log.warning(LogType.LOG, "problem closing connection", e);
				}
			}
		}
		return name;
	}

	/**
	 * The DataSource that liquibase will use to perform the migration.
	 */
	public DataSource getDataSource() {
		return dataSource;
	}

	/**
	 * The DataSource that liquibase will use to perform the migration.
	 */
	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}

	/**
	 * Returns a Resource that is able to resolve to a file or classpath resource.
	 */
	public String getChangeLog() {
		return changeLog;
	}

	/**
	 * Sets a Spring Resource that is able to resolve to a file or classpath resource.
	 * An example might be classpath:db-changelog.xml.
	 */
	public void setChangeLog(String dataModel) {

		this.changeLog = dataModel;
	}

	public String getContexts() {
		return contexts;
	}

	public void setContexts(String contexts) {
		this.contexts = contexts;
	}

    public String getLabels() {
        return labels;
    }

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

    public String getTag() {
        return tag;
    }

    public void setTag(String tag) {
        this.tag = tag;
    }

    public String getDefaultSchema() {
		return defaultSchema;
	}

	public void setDefaultSchema(String defaultSchema) {
		this.defaultSchema = defaultSchema;
	}

    public String getLiquibaseTablespace() {
        return liquibaseTablespace;
    }

    public void setLiquibaseTablespace(String liquibaseTablespace) {
        this.liquibaseTablespace = liquibaseTablespace;
    }

    public String getLiquibaseSchema() {
        return liquibaseSchema;
    }

    public void setLiquibaseSchema(String liquibaseSchema) {
        this.liquibaseSchema = liquibaseSchema;
    }

    public String getDatabaseChangeLogTable() {
        return databaseChangeLogTable;
    }

    public void setDatabaseChangeLogTable(String databaseChangeLogTable) {
        this.databaseChangeLogTable = databaseChangeLogTable;
    }

    public String getDatabaseChangeLogLockTable() {
        return databaseChangeLogLockTable;
    }

	public void setDatabaseChangeLogLockTable(String databaseChangeLogLockTable) {
		this.databaseChangeLogLockTable = databaseChangeLogLockTable;
	}

	/**
	 * Returns whether a rollback should be tested at update time or not.
	 */
	public boolean isTestRollbackOnUpdate() {
		return testRollbackOnUpdate;
	}

	/**
	 * If testRollbackOnUpdate is set to true a rollback will be tested at tupdate time.
	 * For doing so when the update is performed
	 * @param testRollbackOnUpdate
     */
	public void setTestRollbackOnUpdate(boolean testRollbackOnUpdate) {
		this.testRollbackOnUpdate = testRollbackOnUpdate;
	}

	/**
	 * Executed automatically when the bean is initialized.
	 */
	@Override
    public void afterPropertiesSet() throws LiquibaseException {
        ConfigurationProperty shouldRunProperty = LiquibaseConfiguration.getInstance()
            .getProperty(GlobalConfiguration.class, GlobalConfiguration.SHOULD_RUN);

		if (!shouldRunProperty.getValue(Boolean.class)) {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Liquibase did not run because " + LiquibaseConfiguration
                .getInstance().describeValueLookupLogic(shouldRunProperty) + " was set to false");
            return;
		}
		if (!shouldRun) {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Liquibase did not run because 'shouldRun' " + "property was set " +
                "to false on " + getBeanName() + " Liquibase Spring bean.");
            return;
		}

		Connection c = null;
		Liquibase liquibase = null;
		try {
			c = getDataSource().getConnection();
            liquibase = createLiquibase(c);
			generateRollbackFile(liquibase);
			performUpdate(liquibase);
		} catch (SQLException e) {
			throw new DatabaseException(e);
		} finally {
			Database database = null;
			if (liquibase != null) {
				database = liquibase.getDatabase();
			}
			if (database != null) {
                database.close();
            }
        }

	}

    private void generateRollbackFile(Liquibase liquibase) throws LiquibaseException {
        if (rollbackFile != null) {

            try (
                FileOutputStream fileOutputStream = new FileOutputStream(rollbackFile);
                Writer output = new OutputStreamWriter(fileOutputStream, LiquibaseConfiguration.getInstance()
                    .getConfiguration(GlobalConfiguration.class).getOutputEncoding())

            ) {

                if (tag != null) {
                    liquibase.futureRollbackSQL(tag, new Contexts(getContexts()),
                        new LabelExpression(getLabels()), output);
                } else {
                    liquibase.futureRollbackSQL(new Contexts(getContexts()), new LabelExpression(getLabels()), output);
                }
            } catch (IOException e) {
                throw new LiquibaseException("Unable to generate rollback file.", e);
            }
        }
    }

    protected void performUpdate(Liquibase liquibase) throws LiquibaseException {
		if (isClearCheckSums()) {
			liquibase.clearCheckSums();
		}

		if (isTestRollbackOnUpdate()) {
			if (tag != null) {
				liquibase.updateTestingRollback(tag, new Contexts(getContexts()), new LabelExpression(getLabels()));
			} else {
				liquibase.updateTestingRollback(new Contexts(getContexts()), new LabelExpression(getLabels()));
			}
		} else {
			if (tag != null) {
				liquibase.update(tag, new Contexts(getContexts()), new LabelExpression(getLabels()));
			} else {
				liquibase.update(new Contexts(getContexts()), new LabelExpression(getLabels()));
			}
		}
    }

	protected Liquibase createLiquibase(Connection c) throws LiquibaseException {
		SpringResourceAccessor resourceAccessor = createResourceOpener();
		Liquibase liquibase = new Liquibase(getChangeLog(), resourceAccessor, createDatabase(c, resourceAccessor));
        liquibase.setIgnoreClasspathPrefix(isIgnoreClasspathPrefix());
		if (parameters != null) {
			for (Map.Entry<String, String> entry : parameters.entrySet()) {
				liquibase.setChangeLogParameter(entry.getKey(), entry.getValue());
			}
		}

		if (isDropFirst()) {
			liquibase.dropAll();
		}

		return liquibase;
	}

	/**
	 * Subclasses may override this method add change some database settings such as
	 * default schema before returning the database object.
	 *
	 * @param c
	 * @return a Database implementation retrieved from the {@link DatabaseFactory}.
	 * @throws DatabaseException
	 */
	protected Database createDatabase(Connection c, ResourceAccessor resourceAccessor) throws DatabaseException {

        DatabaseConnection liquibaseConnection;
        if (c == null) {
            log.warning(LogType.LOG,
                "Null connection returned by liquibase datasource. Using offline unknown database");
            liquibaseConnection = new OfflineConnection("offline:unknown", resourceAccessor);

        } else {
            liquibaseConnection = new JdbcConnection(c);
        }

        Database database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(liquibaseConnection);
		if (StringUtil.trimToNull(this.defaultSchema) != null) {
            if (database.supportsSchemas()) {
                database.setDefaultSchemaName(this.defaultSchema);
            } else if (database.supportsCatalogs()) {
                database.setDefaultCatalogName(this.defaultSchema);
            }
        }
        if (StringUtil.trimToNull(this.liquibaseSchema) != null) {
            if (database.supportsSchemas()) {
                database.setLiquibaseSchemaName(this.liquibaseSchema);
            } else if (database.supportsCatalogs()) {
                database.setLiquibaseCatalogName(this.liquibaseSchema);
            }
        }
        if (StringUtil.trimToNull(this.liquibaseTablespace) != null && database.supportsTablespaces()) {
            database.setLiquibaseTablespaceName(this.liquibaseTablespace);
        }
        if (StringUtil.trimToNull(this.databaseChangeLogTable) != null) {
            database.setDatabaseChangeLogTableName(this.databaseChangeLogTable);
        }
        if (StringUtil.trimToNull(this.databaseChangeLogLockTable) != null) {
            database.setDatabaseChangeLogLockTableName(this.databaseChangeLogLockTable);
        }
		return database;
	}

	public void setChangeLogParameters(Map<String, String> parameters) {
		this.parameters = parameters;
	}

	/**
	 * Create a new resourceOpener.
	 */
	protected SpringResourceAccessor createResourceOpener() {
		return new SpringResourceAccessor();
	}

	/**
	 * Gets the Spring-name of this instance.
	 *
	 * @return
	 */
	public String getBeanName() {
		return beanName;
	}

    /**
     * Spring sets this automatically to the instance's configured bean name.
     */
    @Override
    public void setBeanName(String name) {
        this.beanName = name;
    }

    public ResourceLoader getResourceLoader() {
        return resourceLoader;
    }

    @Override
    public void setResourceLoader(ResourceLoader resourceLoader) {
        this.resourceLoader = resourceLoader;
    }

	public void setRollbackFile(File rollbackFile) {
		this.rollbackFile = rollbackFile;
    }

    public boolean isIgnoreClasspathPrefix() {
        return ignoreClasspathPrefix;
    }

    public void setIgnoreClasspathPrefix(boolean ignoreClasspathPrefix) {
        this.ignoreClasspathPrefix = ignoreClasspathPrefix;
	}

	@Override
    public String toString() {
        return getClass().getName() + "(" + this.getResourceLoader().toString() + ")";
    }

    public class SpringResourceAccessor extends ClassLoaderResourceAccessor {

    }
}


package liquibase.util.csv.opencsv.bean;

import java.beans.PropertyDescriptor;
import java.beans.PropertyEditor;

/**
 * Converts CSV data to objects.
 */
public abstract class AbstractCSVToBean {

    /**
     * Attempt to find custom property editor on descriptor first, else try the propery editor manager.
     *
     * @param desc - PropertyDescriptor.
     * @return - the PropertyEditor for the given PropertyDescriptor.
     * @throws ReflectiveOperationException - thrown when getting the PropertyEditor for the class.
     */
    protected abstract PropertyEditor getPropertyEditor(PropertyDescriptor desc) throws ReflectiveOperationException;

    /**
     * Returns the trimmed value of the string only if the property the string is describing should be trimmed
     * to be converted to that type.
     *
     * @param s    - String describing the value.
     * @param prop - property descriptor of the value.
     * @return the string passed in if the property is a string otherwise it will return the string with the
     * beginning and end whitespace removed.
     */
    protected String checkForTrim(String s, PropertyDescriptor prop) {
        return trimmableProperty(prop) ? s.trim() : s;
    }

    private boolean trimmableProperty(PropertyDescriptor prop) {
        return !prop.getPropertyType().getName().contains("String");
    }

    /**
     * Convert a string value to its Object value.
     *
     * @param value - String value
     * @param prop  - PropertyDescriptor
     * @return The object set to value (i.e. Integer).  Will return String if no PropertyEditor is found.
     * @throws ReflectiveOperationException - Thrown on error getting the property editor from the property descriptor.
     */
    protected Object convertValue(String value, PropertyDescriptor prop) throws ReflectiveOperationException {
        PropertyEditor editor = getPropertyEditor(prop);
        Object obj = value;
        if (null != editor) {
            editor.setAsText(value);
            obj = editor.getValue();
        }
        return obj;
    }

}