package liquibase.executor.jvm;

import liquibase.Scope;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.database.PreparedStatementFactory;
import liquibase.database.core.Db2zDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.executor.AbstractExecutor;
import liquibase.logging.LogType;
import liquibase.logging.Logger;
import liquibase.sql.CallableSql;
import liquibase.sql.Sql;
import liquibase.sql.visitor.SqlVisitor;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.CallableSqlStatement;
import liquibase.statement.CompoundStatement;
import liquibase.statement.ExecutablePreparedStatement;
import liquibase.statement.SqlStatement;
import liquibase.util.JdbcUtils;
import liquibase.util.StringUtil;

import java.sql.CallableStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Class to simplify execution of SqlStatements.  Based heavily on Spring's JdbcTemplate.
 *

 * Note: This class is currently intended for Liquibase-internal use only and may change without notice in the future
 */
public class JdbcExecutor extends AbstractExecutor {

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

    public Object execute(StatementCallback action, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        DatabaseConnection con = database.getConnection();
        Statement stmt = null;
        try {
            if (con instanceof OfflineConnection) {
                throw new DatabaseException("Cannot execute commands against an offline database");
            }
            stmt = ((JdbcConnection) con).getUnderlyingConnection().createStatement();
            Statement stmtToUse = stmt;

            return action.doInStatement(stmtToUse);
        }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            String url;
            if (con.isClosed()) {
                url = "CLOSED CONNECTION";
            } else {
                url = con.getURL();
            }
            throw new DatabaseException("Error executing SQL " + StringUtil.join(applyVisitors(action.getStatement(), sqlVisitors), "; on "+ url)+": "+ex.getMessage(), ex);
        }
        finally {
            JdbcUtils.closeStatement(stmt);
        }
    }

    // Incorrect warning, at least at this point. The situation here is not that we inject some unsanitised parameter
    // into a query. Instead, we process a whole query. The check should be performed at the places where
    // the query is composed.
    @SuppressWarnings("squid:S2077")
    public Object execute(CallableStatementCallback action, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        DatabaseConnection con = database.getConnection();

        if (con instanceof OfflineConnection) {
            throw new DatabaseException("Cannot execute commands against an offline database");
        }

        CallableStatement stmt = null;
        try {
            String sql = applyVisitors(action.getStatement(), sqlVisitors)[0];

            stmt = ((JdbcConnection) con).getUnderlyingConnection().prepareCall(sql);
            return action.doInCallableStatement(stmt);
        }
        catch (SQLException ex) {
            // Release Connection early, to avoid potential connection pool deadlock
            // in the case when the exception translator hasn't been initialized yet.
            JdbcUtils.closeStatement(stmt);
            stmt = null;
            throw new DatabaseException("Error executing SQL " + StringUtil.join(applyVisitors(action.getStatement(), sqlVisitors), "; on "+ con.getURL())+": "+ex.getMessage(), ex);
        }
        finally {
            JdbcUtils.closeStatement(stmt);
        }
    }

    @Override
    public void execute(final SqlStatement sql) throws DatabaseException {
        execute(sql, new ArrayList<SqlVisitor>());
    }

    @Override
    public void execute(final SqlStatement sql, final List<SqlVisitor> sqlVisitors) throws DatabaseException {
        if(sql instanceof ExecutablePreparedStatement) {
            ((ExecutablePreparedStatement) sql).execute(new PreparedStatementFactory((JdbcConnection)database.getConnection()));
            return;
        }
        if (sql instanceof CompoundStatement) {
            if (database instanceof Db2zDatabase) {
                executeDb2ZosComplexStatement(sql);
                return;
            }
        }

        execute(new ExecuteStatementCallback(sql, sqlVisitors), sqlVisitors);
    }

    public Object query(final SqlStatement sql, final ResultSetExtractor rse) throws DatabaseException {
        return query(sql, rse, new ArrayList<SqlVisitor>());
    }

    public Object query(final SqlStatement sql, final ResultSetExtractor rse, final List<SqlVisitor> sqlVisitors) throws DatabaseException {
        if (sql instanceof CallableSqlStatement) {
            return execute(new QueryCallableStatementCallback(sql, rse), sqlVisitors);
        }

        return execute(new QueryStatementCallback(sql, rse, sqlVisitors), sqlVisitors);
    }

    public List query(SqlStatement sql, RowMapper rowMapper) throws DatabaseException {
        return query(sql, rowMapper, new ArrayList());
    }

    public List query(SqlStatement sql, RowMapper rowMapper, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        return (List) query(sql, new RowMapperResultSetExtractor(rowMapper), sqlVisitors);
    }

    public Object queryForObject(SqlStatement sql, RowMapper rowMapper) throws DatabaseException {
        return queryForObject(sql, rowMapper, new ArrayList());
    }

    public Object queryForObject(SqlStatement sql, RowMapper rowMapper, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        List results = query(sql, rowMapper, sqlVisitors);
        try {
            return JdbcUtils.requiredSingleResult(results);
        } catch (DatabaseException e) {
            throw new DatabaseException("Expected single row from " + sql + " but got "+results.size(), e);
        }
    }

    @Override
    public <T> T queryForObject(SqlStatement sql, Class<T> requiredType) throws DatabaseException {
        return (T) queryForObject(sql, requiredType, new ArrayList());
    }

    @Override
    public <T> T queryForObject(SqlStatement sql, Class<T> requiredType, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        return (T) queryForObject(sql, getSingleColumnRowMapper(requiredType), sqlVisitors);
    }

    @Override
    public long queryForLong(SqlStatement sql) throws DatabaseException {
        return queryForLong(sql, new ArrayList());
    }

    @Override
    public long queryForLong(SqlStatement sql, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        Number number = queryForObject(sql, Long.class, sqlVisitors);
        return ((number != null) ? number.longValue() : 0);
    }

    @Override
    public int queryForInt(SqlStatement sql) throws DatabaseException {
        return queryForInt(sql, new ArrayList());
    }

    @Override
    public int queryForInt(SqlStatement sql, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        Number number = queryForObject(sql, Integer.class, sqlVisitors);
        return ((number != null) ? number.intValue() : 0);
    }

    @Override
    public List queryForList(SqlStatement sql, Class elementType) throws DatabaseException {
        return queryForList(sql, elementType, new ArrayList());
    }

    @Override
    public List queryForList(SqlStatement sql, Class elementType, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        return query(sql, getSingleColumnRowMapper(elementType), sqlVisitors);
    }

    @Override
    public List<Map<String, ?>> queryForList(SqlStatement sql) throws DatabaseException {
        return queryForList(sql, new ArrayList());
    }

    @Override
    public List<Map<String, ?>> queryForList(SqlStatement sql, List<SqlVisitor> sqlVisitors) throws DatabaseException {
        return (List<Map<String, ?>>) query(sql, getColumnMapRowMapper(), sqlVisitors);
    }

    @Override
    public int update(final SqlStatement sql) throws DatabaseException {
        return update(sql, new ArrayList());
    }

    // Incorrect warning, at least at this point. The situation here is not that we inject some unsanitised parameter
    // into a query. Instead, we process a whole query. The check should be performed at the places where
    // the query is composed.
    @SuppressWarnings("squid:S2077")
    @Override
    public int update(final SqlStatement sql, final List<SqlVisitor> sqlVisitors) throws DatabaseException {
        if (sql instanceof CallableSqlStatement) {
            throw new DatabaseException("Direct update using CallableSqlStatement not currently implemented");
        }

        class UpdateStatementCallback implements StatementCallback {
            @Override
            public Object doInStatement(Statement stmt) throws SQLException, DatabaseException {
                String[] sqlToExecute = applyVisitors(sql, sqlVisitors);
                if (sqlToExecute.length != 1) {
                    throw new DatabaseException("Cannot call update on Statement that returns back multiple Sql objects");
                }
                Scope.getCurrentScope().getLog(getClass()).fine(LogType.WRITE_SQL, sqlToExecute[0]);
                return stmt.executeUpdate(sqlToExecute[0]);
            }

            @Override
            public SqlStatement getStatement() {
                return sql;
            }
        }
        return (Integer) execute(new UpdateStatementCallback(), sqlVisitors);
    }

    /**
     * Create a new RowMapper for reading columns as key-value pairs.
     *
     * @return the RowMapper to use
     * @see ColumnMapRowMapper
     */
    protected RowMapper getColumnMapRowMapper() {
        return new ColumnMapRowMapper();
    }

    /**
     * Create a new RowMapper for reading result objects from a single column.
     *
     * @param requiredType the type that each result object is expected to match
     * @return the RowMapper to use
     * @see SingleColumnRowMapper
     */
    protected RowMapper getSingleColumnRowMapper(Class requiredType) {
        return new SingleColumnRowMapper(requiredType);
    }

    @Override
    public void comment(String message) throws DatabaseException {
        Scope.getCurrentScope().getLog(getClass()).fine(LogType.LOG, message);
    }

    private void executeDb2ZosComplexStatement(SqlStatement sqlStatement) throws DatabaseException {
        DatabaseConnection con = database.getConnection();

        if (con instanceof OfflineConnection) {
            throw new DatabaseException("Cannot execute commands against an offline database");
        }
        Sql[] sqls = SqlGeneratorFactory.getInstance().generateSql(sqlStatement, database);
        for (Sql sql : sqls) {
            try {
                if (sql instanceof CallableSql) {
                    CallableStatement call = null;
                    ResultSet resultSet = null;
                    try {
                        call = ((JdbcConnection) con).getUnderlyingConnection().prepareCall(sql.toSql());
                        resultSet = call.executeQuery();
                        checkCallStatus(resultSet, ((CallableSql) sql).getExpectedStatus());
                    } finally {
                        JdbcUtils.close(resultSet, call);
                    }
                } else {
                    Statement stmt = null;
                    try {
                        stmt = ((JdbcConnection) con).getUnderlyingConnection().createStatement();
                        stmt.execute(sql.toSql());
                        con.commit();
                    } finally {
                        JdbcUtils.closeStatement(stmt);
                    }
                }
            } catch (Exception e) {
                throw new DatabaseException(e.getMessage() + " [Failed SQL: " + getErrorCode(e) + sql.toSql() + "]", e);
            }
        }
    }

    private void checkCallStatus(ResultSet resultSet, String status) throws SQLException, DatabaseException {
        if (status != null) {
            StringBuilder message = new StringBuilder();
            while (resultSet.next()) {
                String string = resultSet.getString(2);
                if (string.contains(status)) {
                    return;
                }
                message.append(string).append("\n");
            }
            throw new DatabaseException(message.toString());
        }
    }

    String getErrorCode(Throwable e) {
        if (e instanceof SQLException) {
            return "(" + ((SQLException)e).getErrorCode() + ") ";
        }
        return "";
    }

    private class ExecuteStatementCallback implements StatementCallback {

        private final SqlStatement sql;
        private final List<SqlVisitor> sqlVisitors;

        private ExecuteStatementCallback(SqlStatement sql, List<SqlVisitor> sqlVisitors) {
            this.sql = sql;
            this.sqlVisitors = sqlVisitors;
        }

        @Override
        public Object doInStatement(Statement stmt) throws SQLException, DatabaseException {
            Logger log = Scope.getCurrentScope().getLog(getClass());

            for (String statement : applyVisitors(sql, sqlVisitors)) {
                if (database instanceof OracleDatabase) {
                    while (statement.matches("(?s).*[\\s\\r\\n]*/[\\s\\r\\n]*$")) { //all trailing /'s
                        statement = statement.replaceFirst("[\\s\\r\\n]*/[\\s\\r\\n]*$", "");
                    }
                }

                log.info(LogType.WRITE_SQL, String.format("%s", statement));
                if (statement.contains("?")) {
                    stmt.setEscapeProcessing(false);
                }
                try {
                    //if execute returns false, we can retrieve the affected rows count
                    // (true used when resultset is returned)
                    if (!stmt.execute(statement)) {
                        log.fine(Integer.toString(stmt.getUpdateCount()) + " row(s) affected");
                    }
                } catch (Throwable e) {
                    throw new DatabaseException(e.getMessage()+ " [Failed SQL: " + getErrorCode(e) + statement+"]", e);
                }
                try {
                    int updateCount = 0;
                    //cycle for retrieving row counts from all statements
                    do {
                        if (!stmt.getMoreResults()) {
                            updateCount = stmt.getUpdateCount();
                            if (updateCount != -1)
                                log.fine(Integer.toString(updateCount) + " row(s) affected");
                        }
                    } while (updateCount != -1);

                } catch (Exception e) {
                    throw new DatabaseException(e.getMessage()+ " [Failed SQL: "+ getErrorCode(e) + statement+"]", e);
                }
            }
            return null;
        }

        @Override
        public SqlStatement getStatement() {
            return sql;
        }
    }

    private class QueryStatementCallback implements StatementCallback {

        private final SqlStatement sql;
        private final List<SqlVisitor> sqlVisitors;
        private final ResultSetExtractor rse;

        private QueryStatementCallback(SqlStatement sql, ResultSetExtractor rse, List<SqlVisitor> sqlVisitors) {
            this.sql = sql;
            this.rse = rse;
            this.sqlVisitors = sqlVisitors;
        }

        /**
         * 1. Applies all SqlVisitor to the stmt
         * 2. Executes the (possibly modified) stmt
         * 3. Reads all data from the java.sql.ResultSet into an Object and returns the Object.
         * @param stmt A JDBC Statement that is expected to return a ResultSet (e.g. SELECT)
         * @return An object representing all data from the result set.
         * @throws SQLException If an error occurs during SQL processing
         * @throws DatabaseException If an error occurs in the DBMS-specific program code
         */
        @Override
        // Incorrect warning, at least at this point. The situation here is not that we inject some unsanitised
        // parameter into a query. Instead, we process a whole query. The check should be performed at the places where
        // the query is composed.
        @SuppressWarnings("squid:S2077")
        public Object doInStatement(Statement stmt) throws SQLException, DatabaseException {
            ResultSet rs = null;
            try {
                String[] sqlToExecute = applyVisitors(sql, sqlVisitors);

                if (sqlToExecute.length != 1) {
                    throw new DatabaseException("Can only query with statements that return one sql statement");
                }
                Scope.getCurrentScope().getLog(getClass()).info(LogType.READ_SQL, sqlToExecute[0]);

                rs = stmt.executeQuery(sqlToExecute[0]);
                ResultSet rsToUse = rs;
                return rse.extractData(rsToUse);
            }
            finally {
                if (rs != null) {
                        JdbcUtils.closeResultSet(rs);
                }
            }
        }

        @Override
        public SqlStatement getStatement() {
            return sql;
        }
    }

    private class QueryCallableStatementCallback implements CallableStatementCallback {

        private final SqlStatement sql;
        private final ResultSetExtractor rse;

        private QueryCallableStatementCallback(SqlStatement sql, ResultSetExtractor rse) {
            this.sql = sql;
            this.rse = rse;
        }

        @Override
        public Object doInCallableStatement(CallableStatement cs) throws SQLException, DatabaseException {
            ResultSet rs = null;
            try {
                rs = cs.executeQuery();
                return rse.extractData(rs);
            }
            finally {
                JdbcUtils.closeResultSet(rs);
            }
        }

        @Override
        public SqlStatement getStatement() {
            return sql;
        }
    }

}


package liquibase.executor.jvm;

import liquibase.util.JdbcUtils;
import liquibase.util.NumberUtils;

import java.math.BigDecimal;
import java.sql.*;

/**
 * RowMapper implementation that converts a single column into
 * a single result value per row. Expects to work on a ResultSet
 * that just contains a single column.
 *

 *
The type of the result value for each row can be specified.
 * The value for the single column will be extracted from the ResultSet
 * and converted into the specified target type.
 *
 * @author Spring Framework
 */
class SingleColumnRowMapper implements RowMapper {

    private Class requiredType;

    /**
     * Create a new SingleColumnRowMapper.
     *
     * @see #setRequiredType
     */
    public SingleColumnRowMapper() {
    }

    /**
     * Create a new SingleColumnRowMapper.
     *
     * @param requiredType the type that each result object is expected to match
     */
    public SingleColumnRowMapper(Class requiredType) {
        this.requiredType = requiredType;
    }

    /**
     * Set the type that each result object is expected to match.
     *
If not specified, the column value will be exposed as
     * returned by the JDBC driver.
     */
    public void setRequiredType(Class requiredType) {
        this.requiredType = requiredType;
    }

    /**
     * Extract a value for the single column in the current row.
     *
Validates that there is only one column selected,
     * then delegates to getColumnValue() and also
     * convertValueToRequiredType, if necessary.
     *
     * @see java.sql.ResultSetMetaData#getColumnCount()
     * @see #getColumnValue(java.sql.ResultSet,int,Class)
     * @see #convertValueToRequiredType(Object,Class)
     */
    @Override
    public Object mapRow(ResultSet rs, int rowNum) throws SQLException {
        // Validate column count.
        ResultSetMetaData rsmd = rs.getMetaData();
        int nrOfColumns = rsmd.getColumnCount();
        if (nrOfColumns == 0) {
            throw new SQLException("Returned no columns!");
        } else if (nrOfColumns != 1) {
            throw new SQLException("Returned too many columns: "+ nrOfColumns);
        }

        // Extract column value from JDBC ResultSet
        Object result = getColumnValue(rs, 1, this.requiredType);
        if ((result != null) && (this.requiredType != null) && !this.requiredType.isInstance(result)) {
            // Extracted value does not match already: try to convert it.
            try {
                return convertValueToRequiredType(result, this.requiredType);
            }
            catch (IllegalArgumentException ex) {
                throw new SQLException(
                        "Type mismatch affecting row number " + rowNum + " and column type '" +
                                rsmd.getColumnTypeName(1) + "': " + ex.getMessage());
            }
        }
        return result;
    }

    /**
     * Retrieve a JDBC object value for the specified column.
     *
The default implementation calls ResultSet.getString(index) etc
     * for all standard value types (String, Boolean, number types, date types, etc).
     * It calls ResultSet.getObject(index) else.
     *
If no required type has been specified, this method delegates to
     * getColumnValue(rs, index), which basically calls
     * ResultSet.getObject(index) but applies some additional
     * default conversion to appropriate value types.
     *
Explicit extraction of a String is necessary to properly extract an Oracle
     * RAW value as a String, for example. For the other given types, it is also
     * recommendable to extract the desired types explicitly, to let the JDBC driver
     * perform appropriate (potentially database-specific) conversion.
     *
     * @param rs           is the ResultSet holding the data
     * @param index        is the column index
     * @param requiredType the type that each result object is expected to match
     *                     (or null if none specified)
     * @return the Object value
     * @see java.sql.ResultSet#getString(int)
     * @see java.sql.ResultSet#getObject(int)
     * @see #getColumnValue(java.sql.ResultSet,int)
     */
    protected Object getColumnValue(ResultSet rs, int index, Class requiredType) throws SQLException {
        if (requiredType != null) {
            Object value;
            boolean wasNullCheck = false;

            // Explicitly extract typed value, as far as possible.
            if (String.class.equals(requiredType)) {
                value = rs.getString(index);
            } else if (Boolean.class.equals(requiredType)) {
                value = (rs.getBoolean(index) ? Boolean.TRUE : Boolean.FALSE);
                wasNullCheck = true;
            } else if (Byte.class.equals(requiredType)) {
                value = rs.getByte(index);
                wasNullCheck = true;
            } else if (Short.class.equals(requiredType)) {
                value = rs.getShort(index);
                wasNullCheck = true;
            } else if (Integer.class.equals(requiredType)) {
                value = rs.getInt(index);
                wasNullCheck = true;
            } else if (Long.class.equals(requiredType)) {
                value = rs.getLong(index);
                wasNullCheck = true;
            } else if (Float.class.equals(requiredType)) {
                value = rs.getFloat(index);
                wasNullCheck = true;
            } else if (Double.class.equals(requiredType) || Number.class.equals(requiredType)) {
                value = rs.getDouble(index);
                wasNullCheck = true;
            } else if (byte[].class.equals(requiredType)) {
                value = rs.getBytes(index);
            } else if (java.sql.Date.class.equals(requiredType)) {
                value = rs.getDate(index);
            } else if (java.sql.Time.class.equals(requiredType)) {
                value = rs.getTime(index);
            } else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) {
                value = rs.getTimestamp(index);
            } else if (BigDecimal.class.equals(requiredType)) {
                value = rs.getBigDecimal(index);
            } else if (Blob.class.equals(requiredType)) {
                value = rs.getBlob(index);
            } else if (Clob.class.equals(requiredType)) {
                value = rs.getClob(index);
            } else {
                // Some unknown type desired -> rely on getObject.
                value = rs.getObject(index);
            }

            // Perform was-null check if demanded (for results that the
            // JDBC driver returns as primitives).
            if (wasNullCheck && (value != null) && rs.wasNull()) {
                value = null;
            }
            return value;
        } else {
            // No required type specified -> perform default extraction.
            return getColumnValue(rs, index);
        }
    }

    /**
     * Retrieve a JDBC object value for the specified column, using the most
     * appropriate value type. Called if no required type has been specified.
     *
The default implementation delegates to JdbcUtils.getResultSetValue(),
     * which uses the ResultSet.getObject(index) method. Additionally,
     * it includes a "hack" to get around Oracle returning a non-standard object for
     * their TIMESTAMP datatype. See the JdbcUtils#getResultSetValue()
     * javadoc for details.
     *
     * @param rs    is the ResultSet holding the data
     * @param index is the column index
     * @return the Object value
     */
    protected Object getColumnValue(ResultSet rs, int index) throws SQLException {
        return JdbcUtils.getResultSetValue(rs, index);
    }

    /**
     * Convert the given column value to the specified required type.
     * Only called if the extracted column value does not match already.
     *
If the required type is String, the value will simply get stringified
     * via toString(). In case of a Number, the value will be
     * converted into a Number, either through number conversion or through
     * String parsing (depending on the value type).
     *
     * @param value        the column value as extracted from getColumnValue()
     *                     (never null)
     * @param requiredType the type that each result object is expected to match
     *                     (never null)
     * @return the converted value
     * @see #getColumnValue(java.sql.ResultSet,int,Class)
     */
    protected Object convertValueToRequiredType(Object value, Class requiredType) {
        if (String.class.equals(this.requiredType)) {
            return value.toString();
        } else if (Number.class.isAssignableFrom(this.requiredType)) {
            if (value instanceof Number) {
                // Convert original Number to target Number class.
                return NumberUtils.convertNumberToTargetClass(((Number) value), this.requiredType);
            } else {
                // Convert stringified value to target Number class.
                return NumberUtils.parseNumber(value.toString(), this.requiredType);
            }
        } else {
            throw new IllegalArgumentException(
                    "Value [" + value + "] is of type [" + value.getClass().getName() +
                            "] and cannot be converted to required type [" + this.requiredType.getName() + "]");
        }
    }
}


package liquibase.resource;

import liquibase.Scope;
import liquibase.util.CollectionUtil;
import liquibase.util.StringUtil;

import java.io.*;
import java.net.URI;
import java.nio.file.*;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.zip.GZIPInputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;

/**
 * A @{link ResourceAccessor} implementation for files on the file system.
 * Will look for files in zip and jar files if they are added as root paths.
 */
public class FileSystemResourceAccessor extends AbstractResourceAccessor {

    //Set to avoid duplicates but LinkedHashSet to preserve order. Kept private to control access through get/set since we are an ExtensibleObject
    private LinkedHashSet<Path> rootPaths = new LinkedHashSet<>();

    /**
     * Creates a FileSystemResourceAccessor with the given directories/files as the roots.
     */
    public FileSystemResourceAccessor(File... baseDirsAndFiles) {
        for (File base : CollectionUtil.createIfNull(baseDirsAndFiles)) {
            if (!base.exists()) {
                Scope.getCurrentScope().getLog(getClass()).warning("Non-existent path: " + base.getAbsolutePath());
            } else if (base.isDirectory()) {
                addRootPath(base.toPath());
            } else if (base.getName().endsWith(".jar") || base.getName().toLowerCase().endsWith("zip")) {
                addRootPath(base.toPath());
            } else {
                throw new IllegalArgumentException(base.getAbsolutePath() + " must be a directory, jar or zip");
            }
        }
    }

    protected void addRootPath(Path path) {
        rootPaths.add(path);
    }

    protected LinkedHashSet<Path> getRootPaths() {
        return rootPaths;
    }

    protected File toFile(String path) {
        for (Path root : getRootPaths()) {
            File file = root.resolve(path).toFile();
            if (file.exists()) {
                return file;
            }
        }

        return new File(path);
    }

    @Override
    public InputStreamList openStreams(String relativeTo, String streamPath) throws IOException {
        streamPath = streamPath.replace("\\", "/");
        streamPath = streamPath.replaceFirst("^[\\\\/]([a-zA-Z]:)", "$1");
        final InputStreamList streams = new InputStreamList();

        streamPath = streamPath.replaceFirst("^/", ""); //Path is always relative to the file roots

        if (relativeTo != null) {
            relativeTo = relativeTo.replace("\\", "/");
            relativeTo = relativeTo.replaceFirst("^[\\\\/]([a-zA-Z]:)", "$1");
            relativeTo = relativeTo.replaceFirst("^/", ""); //Path is always relative to the file roots
        }

        for (Path rootPath : rootPaths) {
            URI streamURI = null;
            if (rootPath == null) {
                continue;
            }
            InputStream stream = null;
            if (isCompressedFile(rootPath)) {
                String finalPath = streamPath;

                try (ZipFile zipFile = new ZipFile(rootPath.toFile())) {
                    if (relativeTo != null) {
                        ZipEntry relativeEntry = zipFile.getEntry(relativeTo);
                        if (relativeEntry == null || relativeEntry.isDirectory()) {
                            //not a file, maybe a directory
                            finalPath = relativeTo + "/" + streamPath;
                        } else {
                            //is a file, find path relative to parent
                            String actualRelativeTo = relativeTo;
                            if (actualRelativeTo.contains("/")) {
                                actualRelativeTo = relativeTo.replaceFirst("/[^/]+?$", "");
                            } else {
                                actualRelativeTo = "";
                            }
                            finalPath = actualRelativeTo + "/" + streamPath;
                        }

                    }

                    //resolve any ..'s and duplicated /'s and convert back to standard '/' separator format
                    finalPath = Paths.get(finalPath.replaceFirst("^/", "")).normalize().toString().replace("\\", "/");

                    ZipEntry entry = zipFile.getEntry(finalPath);
                    if (entry != null) {
                        stream = zipFile.getInputStream(entry);
                        streamURI = URI.create(rootPath.normalize().toUri() + "!" + entry.toString());
                    }
                }
            } else {
                Path finalRootPath = rootPath;
                if (relativeTo != null) {
                    finalRootPath = finalRootPath.resolve(relativeTo);
                    File rootPathFile = finalRootPath.toFile();
                    if (rootPathFile.exists()) {
                        if (rootPathFile.isFile()) {
                            //relative to directory
                            finalRootPath = rootPathFile.getParentFile().toPath();
                        }
                    } else {
                        Scope.getCurrentScope().getLog(getClass()).fine("No relative path " + relativeTo + " in " + rootPath);
                        continue;
                    }
                }
                try {
                    if (Paths.get(streamPath).startsWith(finalRootPath) || Paths.get(streamPath).startsWith("/" + finalRootPath)) {
                        streamPath = finalRootPath.relativize(Paths.get(streamPath)).toString();
                    }
                } catch (InvalidPathException ignored) {
                    //that is ok
                }

                if (Paths.get(streamPath).isAbsolute()) {
                    continue; //on a windows system with an absolute path that doesn't start with rootPath
                }

                File resolvedFile = finalRootPath.resolve(streamPath).toFile();
                if (resolvedFile.exists()) {
                    streamURI = resolvedFile.getCanonicalFile().toURI();
                    stream = new BufferedInputStream(new FileInputStream(resolvedFile));
                }

            }

            if (stream != null) {
                if (streamPath.toLowerCase().endsWith(".gz")) {
                    stream = new GZIPInputStream(stream);
                }

                streams.add(streamURI, stream);
            }

        }

        return streams;
    }

    @Override
    public SortedSet<String> list(String relativeTo, String path, boolean recursive, boolean includeFiles, boolean includeDirectories) throws IOException {
        final SortedSet<String> returnList = new TreeSet<>();

        int maxDepth = recursive ? Integer.MAX_VALUE : 1;

        for (final Path rootPath : getRootPaths()) {
            SimpleFileVisitor<Path> fileVisitor = new SimpleFileVisitor<Path>() {
                @Override
                public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
                    if (includeFiles && attrs.isRegularFile()) {
                        addToReturnList(file);
                    }
                    if (includeDirectories && attrs.isDirectory()) {
                        addToReturnList(file);
                    }
                    return FileVisitResult.CONTINUE;
                }

                @Override
                public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException {
                    if (includeDirectories) {
                        addToReturnList(dir);
                    }
                    return FileVisitResult.CONTINUE;
                }

                protected void addToReturnList(Path file) {
                    String pathToAdd;
                    if (isCompressedFile(rootPath)) {
                        pathToAdd = file.normalize().toString().substring(1);  //pull off leading /
                    } else {
                        pathToAdd = rootPath.relativize(file).normalize().toString().replace("\\", "/");
                    }

                    pathToAdd = pathToAdd.replaceFirst("/$", "");
                    returnList.add(pathToAdd);
                }

            };

            if (isCompressedFile(rootPath)) {
                try (FileSystem fs = FileSystems.newFileSystem(rootPath, null)) {
                    Path basePath = fs.getRootDirectories().iterator().next();

                    if (relativeTo != null) {
                        basePath = basePath.resolve(relativeTo);
                        if (!Files.exists(basePath)) {
                            Scope.getCurrentScope().getLog(getClass()).info("Relative path "+relativeTo+" in "+rootPath+" does not exist");
                            continue;
                        } else if (Files.isRegularFile(basePath)) {
                            basePath = basePath.getParent();
                        }
                    }

                    if (path != null) {
                        basePath = basePath.resolve(path);
                    }

                    Files.walkFileTree(basePath, Collections.singleton(FileVisitOption.FOLLOW_LINKS), maxDepth, fileVisitor);
                } catch (NoSuchFileException e) {
                    //nothing to do, return null
                }
            } else {
                Path basePath = rootPath;

                if (relativeTo != null) {
                    basePath = basePath.resolve(relativeTo);
                    if (!Files.exists(basePath)) {
                        Scope.getCurrentScope().getLog(getClass()).info("Relative path "+relativeTo+" in "+rootPath+" does not exist");
                        continue;
                    } else if (Files.isRegularFile(basePath)) {
                        basePath = basePath.getParent();
                    }
                }

                if (path != null) {
                    if (path.startsWith("/") || path.startsWith("\\")) {
                        path = path.substring(1);
                    }

                    basePath = basePath.resolve(path);
                }

                if (!Files.exists(basePath)) {
                    continue;
                }
                Files.walkFileTree(basePath, Collections.singleton(FileVisitOption.FOLLOW_LINKS), maxDepth, fileVisitor);
            }
        }

        returnList.remove(path);
        return returnList;
    }

    /**
     * Returns true if the given path is a compressed file.
     */
    protected boolean isCompressedFile(Path path) {
        return path != null && (path.toString().startsWith("jar:") || path.toString().toLowerCase().endsWith(".jar") || path.toString().toLowerCase().endsWith(".zip"));
    }

    @Override
    public String toString() {
        return getClass().getName() + " (" + StringUtil.join(getRootPaths(), ", ", new StringUtil.ToStringFormatter()) + ")";
    }

}


package liquibase.structure.core;

import liquibase.structure.AbstractDatabaseObject;
import liquibase.structure.DatabaseObject;
import liquibase.util.StringUtil;

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

/**
 * A container of columns. Usually a table or view.
 */
public abstract class Relation extends AbstractDatabaseObject {

    private String name;

    protected Relation() {
        setAttribute("columns", new ArrayList());
        setAttribute("uniqueConstraints", new ArrayList<UniqueConstraint>());
        setAttribute("indexes", new ArrayList<Index>());
    }

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

    @Override
    public Relation setName(String name) {
        setAttribute("name", name);
        this.name = name;

        return this;
    }

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

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

    @Override
    public DatabaseObject[] getContainingObjects() {
        return new DatabaseObject[]{
                getSchema()
        };

    }

    public String getRemarks() {
        return getAttribute("remarks", String.class);
    }

    public Relation setRemarks(String remarks) {
        setAttribute("remarks", remarks);
        return this;
    }

    public List<Column> getColumns() {
        return getAttribute("columns", List.class);
    }

    public Relation addColumn(Column column) {
        this.getColumns().add(column);

        return this;
    }

    /**
     * Returns the column object for the given columnName.  If the column does not exist in this table,
     * return null.
     */
    public Column getColumn(String columnName) {
        for (Column column : getColumns()) {
            if (column.getName().equalsIgnoreCase(columnName)) {
                return column;
            }
        }
        return null;
    }

    /**
     * @return Returns the schema.
     */
    @Override
    public Schema getSchema() {
        return getAttribute("schema", Schema.class);
    }

    /**
     * @param schema The schema to set.
     */
    public Relation setSchema(Schema schema) {
        setAttribute("schema", schema);
        return this;
    }

    public Relation setSchema(String catalog, String schema) {
        return setSchema(new Schema(catalog, schema));
    }

    public int compareTo(Object o) {
        Relation that = (Relation) o;
        int returnValue = 0;
        if ((this.getSchema() != null) && (that.getSchema() != null)) {
            returnValue = StringUtil.trimToEmpty(this.getSchema().getName()).compareToIgnoreCase(StringUtil.trimToEmpty(that.getSchema().getName()));
        }

        if (returnValue == 0) {
            returnValue = this.getName().compareToIgnoreCase(((Relation) o).getName());
        }
        return  returnValue;
    }

}


package liquibase;

import liquibase.database.Database;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.listener.LiquibaseListener;
import liquibase.logging.LogService;
import liquibase.logging.Logger;
import liquibase.logging.core.JavaLogService;
import liquibase.logging.core.LogServiceFactory;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.servicelocator.ServiceLocator;
import liquibase.servicelocator.StandardServiceLocator;
import liquibase.util.SmartMap;

import java.lang.reflect.Constructor;
import java.nio.charset.Charset;
import java.util.*;

/**
 * This scope object is used to hold configuration and other parameters within a call without needing complex method signatures.
 * It also allows new parameters to be added by extensions without affecting standard method signatures.
 * Scope objects can be created in a hierarchical manner with the {@link #child(Map, ScopedRunner)} or {@link #child(String, Object, ScopedRunner)} methods.
 * Values set in parent scopes are visible in child scopes, but values in child scopes are not visible to parent scopes.
 * Values with the same key in different scopes "mask" each other with the value furthest down the scope chain being returned.
 */
public class Scope {

    /**
     * Enumeration containing standard attributes. Normally use methods like convenience {@link #getResourceAccessor()} or {@link #getDatabase()}
     */
    public enum Attr {
        logService,
        resourceAccessor,
        classLoader,
        database,
        quotingStrategy,
        changeLogHistoryService,
        lockService,
        executeMode,
        lineSeparator,
        serviceLocator,
        fileEncoding,
        databaseChangeLog,
        changeSet,
    }

    private static ScopeManager scopeManager;

    private Scope parent;
    private SmartMap values = new SmartMap();

    private LiquibaseListener listener;

    public static Scope getCurrentScope() {
        if (scopeManager == null) {
            scopeManager = new SingletonScopeManager();
            Scope rootScope = new Scope();
            scopeManager.setCurrentScope(rootScope);

            LogService overrideLogService = rootScope.getSingleton(LogServiceFactory.class).getDefaultLogService();
            if (overrideLogService == null) {
                throw new UnexpectedLiquibaseException("Cannot find default log service");
            }
            rootScope.values.put(Attr.logService.name(), overrideLogService);
        }
        return scopeManager.getCurrentScope();
    }

    public static void setScopeManager(ScopeManager scopeManager)  {
        Scope currentScope = getCurrentScope();
        if (currentScope == null) {
            currentScope = new Scope();
        }

        try {
            currentScope = scopeManager.init(currentScope);
        } catch (Exception e) {
            Scope.getCurrentScope().getLog(Scope.class).warning(e.getMessage(), e);
        }
        scopeManager.setCurrentScope(currentScope);

        Scope.scopeManager = scopeManager;

    }

    /**
     * Creates a new "root" scope.
     * Defaults resourceAccessor to {@link ClassLoaderResourceAccessor}.
     * Defaults serviceLocator to {@link StandardServiceLocator}
     */
    private Scope() {
        values.put(Attr.logService.name(), new JavaLogService());
        values.put(Attr.resourceAccessor.name(), new ClassLoaderResourceAccessor());
        values.put(Attr.serviceLocator.name(), new StandardServiceLocator());
    }

    protected Scope(Scope parent, Map<String, Object> scopeValues) {
        this.parent = parent;
        if (scopeValues != null) {
            for (Map.Entry<String, Object> entry : scopeValues.entrySet()) {
                values.put(entry.getKey(), entry.getValue());
            }
        }
    }

    /**
     * Returns the parent scope to this scope. Returns null if this is a root scope.
     */
    public Scope getParent() {
        return parent;
    }

    /**
     * Creates a new scope that is a child of this scope.
     */
    public static void child(Map<String, Object> scopeValues, ScopedRunner runner) throws Exception {
        child((LiquibaseListener) null, scopeValues, runner);
    }

    /**
     * Creates a new child scope that includes the given {@link LiquibaseListener}.
     * You cannot unassign a listener, they simply fall out of scope when the Scope does.
     *
     * @see #getListeners(Class)
     */
    public static void child(LiquibaseListener listener, ScopedRunner runner) throws Exception {
        child(listener, null, runner);
    }

    public static void child(LiquibaseListener listener, Map<String, Object> scopeValues, ScopedRunner runner) throws Exception{
        child(listener, scopeValues, () -> {
            runner.run();
            return null;
        });
    }

    public static <T> T child(LiquibaseListener listener, Map<String, Object> scopeValues, ScopedRunnerWithReturn<T> runner) throws Exception{
        Scope originalScope = getCurrentScope();
        Scope child = new Scope(originalScope, scopeValues);
        child.listener = listener;
        try {
            scopeManager.setCurrentScope(child);
            return runner.run();
        } finally {
            scopeManager.setCurrentScope(originalScope);
        }
    }

    /**
     * Creates a new scope that is a child of this scope.
     */
    public static void child(String newValueKey, Object newValue, ScopedRunner runner) throws Exception {
        Map<String, Object> scopeValues = new HashMap<String, Object>();
        scopeValues.put(newValueKey, newValue);

        child(scopeValues, runner);
    }

    public static void child(Enum newValueKey, Object newValue, ScopedRunner runner) throws Exception {
        child(newValueKey.name(), newValue, runner);
    }

    /**
     * Return true if the given key is defined.
     */
    public boolean has(String key) {
        return get(key, Object.class) != null;
    }

    /**
     * Return true if the given key is defined.
     */
    public boolean has(Enum key) {
        return has(key.name());
    }

    public <T> T get(Enum key, Class<T> type) {
        return get(key.name(), type);
    }

    public <T> T get(Enum key, T defaultValue) {
        return get(key.name(), defaultValue);
    }

    /**
     * Return the value associated with the given key in this scope or any parent scope.
     * The value is converted to the given type if necessary using {@link liquibase.util.ObjectUtil#convert(Object, Class)}.
     * Returns null if key is not defined in this or any parent scopes.
     */
    public <T> T get(String key, Class<T> type) {
        T value = values.get(key, type);
        if (value == null && parent != null) {
            value = parent.get(key, type);
        }
        return value;
    }

    /**
     * Return the value associated with the given key in this scope or any parent scope.
     * If the value is not defined, the passed defaultValue is returned.
     * The value is converted to the given type if necessary using {@link liquibase.util.ObjectUtil#convert(Object, 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;
    }

    /**
     * Looks up the singleton object of the given type. If the singleton has not been created yet, it will be instantiated.
     * The singleton is a singleton based on the root scope and the same object will be returned for all child scopes of the root.
     */
    public <T extends SingletonObject> T getSingleton(Class<T> type) {
        if (getParent() != null) {
            return getParent().getSingleton(type);
        }

        String key = type.getName();
        T singleton = get(key, type);
        if (singleton == null) {
            try {
                try {
                    Constructor<T> constructor = type.getDeclaredConstructor(Scope.class);
                    constructor.setAccessible(true);
                    singleton = constructor.newInstance(this);
                } catch (NoSuchMethodException e) { //try without scope
                    Constructor<T> constructor = type.getDeclaredConstructor();
                    constructor.setAccessible(true);
                    singleton = constructor.newInstance();
                }
            } catch (Exception e) {
                throw new UnexpectedLiquibaseException(e);
            }

            values.put(key, singleton);
        }
        return singleton;
    }

    public Logger getLog(Class clazz) {
        return get(Attr.logService, LogService.class).getLog(clazz);
    }

    public Database getDatabase() {
        return get(Attr.database, Database.class);
    }

    public ClassLoader getClassLoader() {
        return get(Attr.classLoader, Thread.currentThread().getContextClassLoader());
    }

    public ClassLoader getClassLoader(boolean fallbackToContextClassLoader) {
        ClassLoader classLoader = getClassLoader();
        if (classLoader == null && fallbackToContextClassLoader) {
            return Thread.currentThread().getContextClassLoader();
        }
        return classLoader;
    }

    public ServiceLocator getServiceLocator() {
        return get(Attr.serviceLocator, ServiceLocator.class);
    }

    public ResourceAccessor getResourceAccessor() {
        return get(Attr.resourceAccessor, ResourceAccessor.class);
    }

    public String getLineSeparator() {
        return get(Attr.lineSeparator, System.lineSeparator());
    }

    public Charset getFileEncoding() {
        return get(Attr.fileEncoding, Charset.defaultCharset());
    }

    /**
     * Returns {@link LiquibaseListener}s defined in this scope and/or all its parents that are of the given type.
     */
    public <T extends LiquibaseListener> Collection<T> getListeners(Class<T> type) {
        List<T> returnList = new ArrayList<>();

        Scope scopeToCheck = this;
        while (scopeToCheck != null) {
            if (scopeToCheck.listener != null && type.isAssignableFrom(scopeToCheck.listener.getClass())) {
                returnList.add((T) scopeToCheck.listener);
            }
            scopeToCheck = scopeToCheck.getParent();
        }

        return returnList;
    }

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

    public String describe() {
        String databaseName = null;
        Database database = getDatabase();
        if (database != null) {
            databaseName = database.getShortName();

            DatabaseConnection connection = database.getConnection();
            if (connection == null) {
                databaseName = "unconnected " + databaseName;
            } else if (connection instanceof OfflineConnection) {
                databaseName = "offline " + databaseName;
            } else if (connection instanceof JdbcConnection) {
                databaseName = "jdbc " + databaseName;
            }
        }
        return "scope(database=" + databaseName + ")";
    }

    public interface ScopedRunner<T> {
        void run() throws Exception;
    }
    public interface ScopedRunnerWithReturn<T> {
        T run() throws Exception;
    }

}


package liquibase.changelog;

import liquibase.*;
import liquibase.change.CheckSum;
import liquibase.change.ColumnConfig;
import liquibase.database.Database;
import liquibase.database.core.DB2Database;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.SQLiteDatabase;
import liquibase.exception.DatabaseException;
import liquibase.exception.DatabaseHistoryException;
import liquibase.exception.LiquibaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.snapshot.InvalidExampleException;
import liquibase.snapshot.SnapshotControl;
import liquibase.snapshot.SnapshotGeneratorFactory;
import liquibase.sqlgenerator.SqlGeneratorFactory;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.*;
import liquibase.structure.core.Column;
import liquibase.structure.core.DataType;
import liquibase.structure.core.Table;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;

public class StandardChangeLogHistoryService extends AbstractChangeLogHistoryService {

    private List<RanChangeSet> ranChangeSetList;
    private boolean serviceInitialized;
    private Boolean hasDatabaseChangeLogTable;
    private boolean databaseChecksumsCompatible = true;
    private Integer lastChangeSetSequenceValue;

    protected static final String LABELS_SIZE = "255";
    protected static final String CONTEXTS_SIZE = "255";

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

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

    public String getDatabaseChangeLogTableName() {
        return getDatabase().getDatabaseChangeLogTableName();
    }

    public String getLiquibaseSchemaName() {
        return getDatabase().getLiquibaseSchemaName();
    }

    public String getLiquibaseCatalogName() {
        return getDatabase().getLiquibaseCatalogName();
    }

    public boolean canCreateChangeLogTable() {
        return true;
    }

    @Override
    public void reset() {
        this.ranChangeSetList = null;
        this.serviceInitialized = false;
        this.hasDatabaseChangeLogTable = null;
    }

    public boolean hasDatabaseChangeLogTable() {
        if (hasDatabaseChangeLogTable == null) {
            try {
                hasDatabaseChangeLogTable = SnapshotGeneratorFactory.getInstance().hasDatabaseChangeLogTable
                    (getDatabase());
            } catch (LiquibaseException e) {
                throw new UnexpectedLiquibaseException(e);
            }
        }
        return hasDatabaseChangeLogTable;
    }

    protected String getCharTypeName() {
        if ((getDatabase() instanceof MSSQLDatabase) && ((MSSQLDatabase) getDatabase())
            .sendsStringParametersAsUnicode()) {
            return "nvarchar";
        }
        return "varchar";
    }

    public void init() throws DatabaseException {
        if (serviceInitialized) {
            return;
        }
        Database database = getDatabase();

        Table changeLogTable = null;
        try {
            changeLogTable = SnapshotGeneratorFactory.getInstance().getDatabaseChangeLogTable(new SnapshotControl
                (database, false, Table.class, Column.class), database);
        } catch (LiquibaseException e) {
            throw new UnexpectedLiquibaseException(e);
        }

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

        boolean changeLogCreateAttempted = false;
        Executor executor = ExecutorService.getInstance().getExecutor(database);
        if (changeLogTable != null) {
            boolean hasDescription = changeLogTable.getColumn("DESCRIPTION") != null;
            boolean hasComments = changeLogTable.getColumn("COMMENTS") != null;
            boolean hasTag = changeLogTable.getColumn("TAG") != null;
            boolean hasLiquibase = changeLogTable.getColumn("LIQUIBASE") != null;
            boolean hasContexts = changeLogTable.getColumn("CONTEXTS") != null;
            boolean hasLabels = changeLogTable.getColumn("LABELS") != null;
            boolean liquibaseColumnNotRightSize = false;
            if (!(this.getDatabase() instanceof SQLiteDatabase)) {
                DataType type = changeLogTable.getColumn("LIQUIBASE").getType();
                if (type.getTypeName().toLowerCase().startsWith("varchar")) {
                    Integer columnSize = type.getColumnSize();
                    liquibaseColumnNotRightSize = (columnSize != null) && (columnSize < 20);
                } else {
                    liquibaseColumnNotRightSize = false;
                }
            }
            boolean hasOrderExecuted = changeLogTable.getColumn("ORDEREXECUTED") != null;
            boolean checksumNotRightSize = false;
            if (!(this.getDatabase() instanceof SQLiteDatabase)) {
                DataType type = changeLogTable.getColumn("MD5SUM").getType();
                if (type.getTypeName().toLowerCase().startsWith("varchar")) {
                    Integer columnSize = type.getColumnSize();
                    checksumNotRightSize = (columnSize != null) && (columnSize < 35);
                } else {
                    liquibaseColumnNotRightSize = false;
                }
            }
            boolean hasExecTypeColumn = changeLogTable.getColumn("EXECTYPE") != null;
            String charTypeName = getCharTypeName();
            boolean hasDeploymentIdColumn = changeLogTable.getColumn("DEPLOYMENT_ID") != null;

            if (!hasDescription) {
                executor.comment("Adding missing databasechangelog.description column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "DESCRIPTION", charTypeName + "(255)", null));
            }
            if (!hasTag) {
                executor.comment("Adding missing databasechangelog.tag column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "TAG", charTypeName + "(255)", null));
            }
            if (!hasComments) {
                executor.comment("Adding missing databasechangelog.comments column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "COMMENTS", charTypeName + "(255)", null));
            }
            if (!hasLiquibase) {
                executor.comment("Adding missing databasechangelog.liquibase column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "LIQUIBASE", charTypeName + "(20)",
                    null));
            }
            if (!hasOrderExecuted) {
                executor.comment("Adding missing databasechangelog.orderexecuted column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "ORDEREXECUTED", "int", null));
                statementsToExecute.add(new UpdateStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName()).addNewColumnValue("ORDEREXECUTED", -1));
                statementsToExecute.add(new SetNullableStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "ORDEREXECUTED", "int", false));
            }
            if (checksumNotRightSize) {
                executor.comment("Modifying size of databasechangelog.md5sum column");

                statementsToExecute.add(new ModifyDataTypeStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "MD5SUM", charTypeName + "(35)"));
            }
            if (liquibaseColumnNotRightSize) {
                executor.comment("Modifying size of databasechangelog.liquibase column");

                statementsToExecute.add(new ModifyDataTypeStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "LIQUIBASE", charTypeName + "(20)"));
            }
            if (!hasExecTypeColumn) {
                executor.comment("Adding missing databasechangelog.exectype column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "EXECTYPE", charTypeName + "(10)",
                    null));
                statementsToExecute.add(new UpdateStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName()).addNewColumnValue("EXECTYPE", "EXECUTED"));
                statementsToExecute.add(new SetNullableStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "EXECTYPE", charTypeName + "(10)",
                    false));
            }

            if (hasContexts) {
                Integer columnSize = changeLogTable.getColumn("CONTEXTS").getType().getColumnSize();
                if ((columnSize != null) && (columnSize < Integer.parseInt(CONTEXTS_SIZE))) {
                    executor.comment("Modifying size of databasechangelog.contexts column");
                    statementsToExecute.add(new ModifyDataTypeStatement(getLiquibaseCatalogName(),
                        getLiquibaseSchemaName(), getDatabaseChangeLogTableName(), "CONTEXTS",
                        charTypeName + "("+ CONTEXTS_SIZE+")"));
                }
            } else {
                executor.comment("Adding missing databasechangelog.contexts column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "CONTEXTS", charTypeName + "("
                    + CONTEXTS_SIZE+")", null));
            }

            if (hasLabels) {
                Integer columnSize = changeLogTable.getColumn("LABELS").getType().getColumnSize();
                if ((columnSize != null) && (columnSize < Integer.parseInt(LABELS_SIZE))) {
                    executor.comment("Modifying size of databasechangelog.labels column");
                    statementsToExecute.add(new ModifyDataTypeStatement(getLiquibaseCatalogName(),
                        getLiquibaseSchemaName(), getDatabaseChangeLogTableName(), "LABELS",
                        charTypeName + "(" + LABELS_SIZE + ")"));
                }
            } else {
                executor.comment("Adding missing databasechangelog.labels column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "LABELS", charTypeName + "(" +
                    LABELS_SIZE + ")", null));
            }

            if (!hasDeploymentIdColumn) {
                executor.comment("Adding missing databasechangelog.deployment_id column");
                statementsToExecute.add(new AddColumnStatement(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName(), "DEPLOYMENT_ID", "VARCHAR(10)", null));
                if (database instanceof DB2Database) {
                    statementsToExecute.add(new ReorganizeTableStatement(getLiquibaseCatalogName(),
                        getLiquibaseSchemaName(), getDatabaseChangeLogTableName()));
                }
            }

            List<Map<String, ?>> md5sumRS = ExecutorService.getInstance().getExecutor(database).queryForList(new
                SelectFromDatabaseChangeLogStatement(new SelectFromDatabaseChangeLogStatement.ByNotNullCheckSum(),
                new ColumnConfig().setName("MD5SUM")).setLimit(1));
            if (!md5sumRS.isEmpty()) {
                String md5sum = md5sumRS.get(0).get("MD5SUM").toString();
                if (!md5sum.startsWith(CheckSum.getCurrentVersion() + ":")) {
                    executor.comment("DatabaseChangeLog checksums are an incompatible version.  Setting them to null " +
                        "so they will be updated on next database update");
                    databaseChecksumsCompatible = false;
                    statementsToExecute.add(new RawSqlStatement(
                        "UPDATE " + getDatabase().escapeTableName(getLiquibaseCatalogName(), getLiquibaseSchemaName()
                            , getDatabaseChangeLogTableName()) + " " +
                            "SET " +  getDatabase().escapeObjectName("MD5SUM", Column.class) + " = NULL"));
                }
            }

        } else if (!changeLogCreateAttempted) {
            executor.comment("Create Database Change Log Table");
            SqlStatement createTableStatement = new CreateDatabaseChangeLogTableStatement();
            if (!canCreateChangeLogTable()) {
                throw new DatabaseException("Cannot create " + getDatabase().escapeTableName(getLiquibaseCatalogName
                    (), getLiquibaseSchemaName(), getDatabaseChangeLogTableName()) + " table for your getDatabase()" +
                    ".\n\n" +
                        "Please construct it manually using the following SQL as a base and re-run Liquibase:\n\n" +
                        createTableStatement);
            }
            // If there is no table in the database for recording change history create one.
            statementsToExecute.add(createTableStatement);
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Creating database history table with name: " +
                getDatabase().escapeTableName(getLiquibaseCatalogName(), getLiquibaseSchemaName(),
                    getDatabaseChangeLogTableName()));
        }

        for (SqlStatement sql : statementsToExecute) {
            if (SqlGeneratorFactory.getInstance().supports(sql, database)) {
                executor.execute(sql);
                getDatabase().commit();
            } else {
                Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Cannot run " + sql.getClass().getSimpleName() + " on" +
                    " " + getDatabase().getShortName() + " when checking databasechangelog table");
            }
        }
        serviceInitialized = true;
    }

    @Override
    public void upgradeChecksums(final DatabaseChangeLog databaseChangeLog, final Contexts contexts, LabelExpression
        labels) throws DatabaseException {
        super.upgradeChecksums(databaseChangeLog, contexts, labels);
        getDatabase().commit();
    }

    /**
     * Returns the ChangeSets that have been run against the current getDatabase().
     */
    public List<RanChangeSet> getRanChangeSets() throws DatabaseException {
        if (this.ranChangeSetList == null) {
            Database database = getDatabase();
            String databaseChangeLogTableName = getDatabase().escapeTableName(getLiquibaseCatalogName(),
                getLiquibaseSchemaName(), getDatabaseChangeLogTableName());
            List<RanChangeSet> ranChangeSets = new ArrayList<>();
            if (hasDatabaseChangeLogTable()) {
                Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Reading from " + databaseChangeLogTableName);
                List<Map<String, ?>> results = queryDatabaseChangeLogTable(database);
                for (Map rs : results) {
                    String fileName = rs.get("FILENAME").toString();
                    String author = rs.get("AUTHOR").toString();
                    String id = rs.get("ID").toString();
                    String md5sum = ((rs.get("MD5SUM") == null) || !databaseChecksumsCompatible) ? null : rs.get
                        ("MD5SUM").toString();
                    String description = (rs.get("DESCRIPTION") == null) ? null : rs.get("DESCRIPTION").toString();
                    String comments = (rs.get("COMMENTS") == null) ? null : rs.get("COMMENTS").toString();
                    Object tmpDateExecuted = rs.get("DATEEXECUTED");
                    Date dateExecuted = null;
                    if (tmpDateExecuted instanceof Date) {
                        dateExecuted = (Date) tmpDateExecuted;
                    } else {
                        DateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                        try {
                            dateExecuted = df.parse((String) tmpDateExecuted);
                        } catch (ParseException e) {
                            // Ignore ParseException and assume dateExecuted == null instead of aborting.
                        }
                    }
                    String tmpOrderExecuted = rs.get("ORDEREXECUTED").toString();
                    Integer orderExecuted = ((tmpOrderExecuted == null) ? null : Integer.valueOf(tmpOrderExecuted));
                    String tag = (rs.get("TAG") == null) ? null : rs.get("TAG").toString();
                    String execType = (rs.get("EXECTYPE") == null) ? null : rs.get("EXECTYPE").toString();
                    ContextExpression contexts = new ContextExpression((String) rs.get("CONTEXTS"));
                    Labels labels = new Labels((String) rs.get("LABELS"));
                    String deploymentId = (String) rs.get("DEPLOYMENT_ID");

                    try {
                        RanChangeSet ranChangeSet = new RanChangeSet(fileName, id, author, CheckSum.parse(md5sum),
                            dateExecuted, tag, ChangeSet.ExecType.valueOf(execType), description, comments, contexts,
                            labels, deploymentId);
                        ranChangeSet.setOrderExecuted(orderExecuted);
                        ranChangeSets.add(ranChangeSet);
                    } catch (IllegalArgumentException e) {
                        Scope.getCurrentScope().getLog(getClass()).severe(LogType.LOG, "Unknown EXECTYPE from database: " +
                            execType);
                        throw e;
                    }
                }
            }

            this.ranChangeSetList = ranChangeSets;
        }
        return Collections.unmodifiableList(ranChangeSetList);
    }

    public List<Map<String, ?>> queryDatabaseChangeLogTable(Database database) throws DatabaseException {
        SelectFromDatabaseChangeLogStatement select = new SelectFromDatabaseChangeLogStatement(new ColumnConfig()
            .setName("*").setComputed(true)).setOrderBy("DATEEXECUTED ASC", "ORDEREXECUTED ASC");
        return ExecutorService.getInstance().getExecutor(database).queryForList(select);
    }

    @Override
    protected void replaceChecksum(ChangeSet changeSet) throws DatabaseException {
        ExecutorService.getInstance().getExecutor(getDatabase()).execute(new UpdateChangeSetChecksumStatement
            (changeSet));

        getDatabase().commit();
        reset();
    }

    @Override
    public RanChangeSet getRanChangeSet(final ChangeSet changeSet) throws DatabaseException, DatabaseHistoryException {
        if (!hasDatabaseChangeLogTable()) {
            return null;
        }

        return super.getRanChangeSet(changeSet);
    }

    @Override
    public void setExecType(ChangeSet changeSet, ChangeSet.ExecType execType) throws DatabaseException {
        Database database = getDatabase();

        ExecutorService.getInstance().getExecutor(database).execute(new MarkChangeSetRanStatement(changeSet, execType));
        getDatabase().commit();
        if (this.ranChangeSetList != null) {
            this.ranChangeSetList.add(new RanChangeSet(changeSet, execType, null, null));
        }

    }

    @Override
    public void removeFromHistory(final ChangeSet changeSet) throws DatabaseException {
        Database database = getDatabase();
        ExecutorService.getInstance().getExecutor(database).execute(new RemoveChangeSetRanStatusStatement(changeSet));
        getDatabase().commit();

        if (this.ranChangeSetList != null) {
            this.ranChangeSetList.remove(new RanChangeSet(changeSet));
        }
    }

    @Override
    public int getNextSequenceValue() throws LiquibaseException {
        if (lastChangeSetSequenceValue == null) {
            if (getDatabase().getConnection() == null) {
                lastChangeSetSequenceValue = 0;
            } else {
                lastChangeSetSequenceValue = ExecutorService.getInstance().getExecutor(getDatabase()).queryForInt(new
                    GetNextChangeSetSequenceValueStatement());
            }
        }

        return ++lastChangeSetSequenceValue;
    }

    /**
     * Tags the database changelog with the given string.
     */
    @Override
    public void tag(final String tagString) throws DatabaseException {
        Database database = getDatabase();
        Executor executor = ExecutorService.getInstance().getExecutor(database);
        int totalRows = ExecutorService.getInstance().getExecutor(database).queryForInt(new
            SelectFromDatabaseChangeLogStatement(new ColumnConfig().setName("COUNT(*)", true)));
        if (totalRows == 0) {
            ChangeSet emptyChangeSet = new ChangeSet(String.valueOf(new Date().getTime()), "liquibase",
                false,false, "liquibase-internal", null, null,
                getDatabase().getObjectQuotingStrategy(), null);
            this.setExecType(emptyChangeSet, ChangeSet.ExecType.EXECUTED);
        }

        executor.execute(new TagDatabaseStatement(tagString));
        getDatabase().commit();

        if (this.ranChangeSetList != null) {
            ranChangeSetList.get(ranChangeSetList.size() - 1).setTag(tagString);
        }
    }

    @Override
    public boolean tagExists(final String tag) throws DatabaseException {
        int count = ExecutorService.getInstance().getExecutor(getDatabase()).queryForInt(new
            SelectFromDatabaseChangeLogStatement(new SelectFromDatabaseChangeLogStatement.ByTag(tag), new
            ColumnConfig().setName("COUNT(*)", true)));
        return count > 0;
    }

    @Override
    public void clearAllCheckSums() throws LiquibaseException {
        Database database = getDatabase();
        UpdateStatement updateStatement = new UpdateStatement(database.getLiquibaseCatalogName(), database
            .getLiquibaseSchemaName(), database.getDatabaseChangeLogTableName());
        updateStatement.addNewColumnValue("MD5SUM", null);
        ExecutorService.getInstance().getExecutor(database).execute(updateStatement);
        database.commit();
    }

    @Override
    public void destroy() throws DatabaseException {
        Database database = getDatabase();
        try {
            if (SnapshotGeneratorFactory.getInstance().has(new Table().setName(database.getDatabaseChangeLogTableName
                ()).setSchema(database.getLiquibaseCatalogName(), database.getLiquibaseSchemaName()), database)) {
                ExecutorService.getInstance().getExecutor(database).execute(new DropTableStatement(database
                    .getLiquibaseCatalogName(), database.getLiquibaseSchemaName(), database
                    .getDatabaseChangeLogTableName(), false));
            }
            reset();
        } catch (InvalidExampleException e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }
}


package liquibase.precondition;

import liquibase.Scope;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.servicelocator.ServiceLocator;

import java.util.HashMap;
import java.util.Map;

public class PreconditionFactory {
    @SuppressWarnings("unchecked")
    private final Map<String, Class<? extends Precondition>> preconditions;

    private static PreconditionFactory instance;

    @SuppressWarnings("unchecked")
    private PreconditionFactory() {
        preconditions = new HashMap<>();
        try {
            for (Precondition precondition : Scope.getCurrentScope().getServiceLocator().findInstances(Precondition.class)) {
                register(precondition);
            }
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

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

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

    public Map<String, Class<? extends Precondition>> getPreconditions() {
        return preconditions;
    }

    public void register(Precondition precondition) {
        try {
            preconditions.put(precondition.getName(), precondition.getClass());
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
    }

    public void unregister(String name) {
        preconditions.remove(name);
    }

    /**
     * Create a new Precondition subclass based on the given tag name.
     */
    public Precondition create(String tagName) {
        Class<?> aClass = preconditions.get(tagName);
        if (aClass == null) {
            return null;
        }
        try {
            return (Precondition) aClass.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}


package liquibase.executor.jvm;

import java.io.StringWriter;
import java.math.BigDecimal;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Calendar;

/**
 * Utility methods for PreparedStatementSetter/Creator and CallableStatementCreator
 * implementations, providing sophisticated parameter management (including support
 * for LOB values).
 *

 *
Used by PreparedStatementCreatorFactory and CallableStatementCreatorFactory,
 * but also available for direct use in custom setter/creator implementations.
 *
 * @author Spring Framework
 * @see PreparedStatementSetter
 * @see SqlParameter
 */
abstract class StatementCreatorUtils {

    /**
     * Set the value for a parameter. The method used is based on the SQL type
     * of the parameter and we can handle complex types like arrays and LOBs.
     *
     * @param ps         the prepared statement or callable statement
     * @param paramIndex index of the parameter we are setting
     * @param param      the parameter as it is declared including type
     * @param inValue    the value to set
     * @throws SQLException if thrown by PreparedStatement methods
     */
    public static void setParameterValue(
            PreparedStatement ps, int paramIndex, SqlParameter param, Object inValue)
            throws SQLException {

        setParameterValueInternal(ps, paramIndex, param.getSqlType(), param.getTypeName(), param.getScale(), inValue);
    }

    /**
     * Set the value for a parameter. The method used is based on the SQL type
     * of the parameter and we can handle complex types like arrays and LOBs.
     *
     * @param ps         the prepared statement or callable statement
     * @param paramIndex index of the parameter we are setting
     * @param sqlType    the SQL type of the parameter
     * @param inValue    the value to set (plain value or a SqlTypeValue)
     * @throws SQLException if thrown by PreparedStatement methods
     */
    public static void setParameterValue(
            PreparedStatement ps, int paramIndex, int sqlType, Object inValue)
            throws SQLException {

        setParameterValueInternal(ps, paramIndex, sqlType, null, null, inValue);
    }

    /**
     * Set the value for a parameter. The method used is based on the SQL type
     * of the parameter and we can handle complex types like arrays and LOBs.
     *
     * @param ps         the prepared statement or callable statement
     * @param paramIndex index of the parameter we are setting
     * @param sqlType    the SQL type of the parameter
     * @param typeName   the type name of the parameter
     *                   (optional, only used for SQL NULL and SqlTypeValue)
     * @param scale      the number of digits after the decimal point
     *                   (for DECIMAL and NUMERIC types)
     * @param inValue    the value to set (plain value or a SqlTypeValue)
     * @throws SQLException if thrown by PreparedStatement methods
     */
    private static void setParameterValueInternal(
            PreparedStatement ps, int paramIndex, int sqlType, String typeName, Integer scale, Object inValue)
            throws SQLException {

        if (inValue == null) {
            if (sqlType == SqlTypeValue.TYPE_UNKNOWN) {
                boolean useSetObject = false;
                try {
                    useSetObject = (ps.getConnection().getMetaData().getDatabaseProductName().indexOf("Informix") != -1);
                }
                catch (Throwable ex) {
//                    logger.debug("Could not check database product name", ex);
                }
                if (useSetObject) {
                    ps.setObject(paramIndex, null);
                } else {
                    ps.setNull(paramIndex, Types.NULL);
                }
            } else if (typeName != null) {
                ps.setNull(paramIndex, sqlType, typeName);
            } else {
                ps.setNull(paramIndex, sqlType);
            }
        } else {  // inValue != null
            if (inValue instanceof SqlTypeValue) {
                ((SqlTypeValue) inValue).setTypeValue(ps, paramIndex, sqlType, typeName);
            } else if ((sqlType == Types.VARCHAR) || (sqlType == -9)) { //-9 is Types.NVARCHAR in java 1.6
                ps.setString(paramIndex, inValue.toString());
            } else if ((sqlType == Types.DECIMAL) || (sqlType == Types.NUMERIC)) {
                if (inValue instanceof BigDecimal) {
                    ps.setBigDecimal(paramIndex, (BigDecimal) inValue);
                } else if (scale != null) {
                    ps.setObject(paramIndex, inValue, sqlType, scale);
                } else {
                    ps.setObject(paramIndex, inValue, sqlType);
                }
            } else if (sqlType == Types.DATE) {
                if (inValue instanceof java.util.Date) {
                    if (inValue instanceof java.sql.Date) {
                        ps.setDate(paramIndex, (java.sql.Date) inValue);
                    } else {
                        ps.setDate(paramIndex, new java.sql.Date(((java.util.Date) inValue).getTime()));
                    }
                } else if (inValue instanceof Calendar) {
                    Calendar cal = (Calendar) inValue;
                    ps.setDate(paramIndex, new java.sql.Date(cal.getTime().getTime()), cal);
                } else {
                    ps.setObject(paramIndex, inValue, Types.DATE);
                }
            } else if (sqlType == Types.TIME) {
                if (inValue instanceof java.util.Date) {
                    if (inValue instanceof java.sql.Time) {
                        ps.setTime(paramIndex, (java.sql.Time) inValue);
                    } else {
                        ps.setTime(paramIndex, new java.sql.Time(((java.util.Date) inValue).getTime()));
                    }
                } else if (inValue instanceof Calendar) {
                    Calendar cal = (Calendar) inValue;
                    ps.setTime(paramIndex, new java.sql.Time(cal.getTime().getTime()), cal);
                } else {
                    ps.setObject(paramIndex, inValue, Types.TIME);
                }
            } else if (sqlType == Types.TIMESTAMP) {
                if (inValue instanceof java.util.Date) {
                    if (inValue instanceof java.sql.Timestamp) {
                        ps.setTimestamp(paramIndex, (java.sql.Timestamp) inValue);
                    } else {
                        ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime()));
                    }
                } else if (inValue instanceof Calendar) {
                    Calendar cal = (Calendar) inValue;
                    ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()), cal);
                } else {
                    ps.setObject(paramIndex, inValue, Types.TIMESTAMP);
                }
            } else if (sqlType == SqlTypeValue.TYPE_UNKNOWN) {
                if (isStringValue(inValue)) {
                    ps.setString(paramIndex, inValue.toString());
                } else if (isDateValue(inValue)) {
                    ps.setTimestamp(paramIndex, new java.sql.Timestamp(((java.util.Date) inValue).getTime()));
                } else if (inValue instanceof Calendar) {
                    Calendar cal = (Calendar) inValue;
                    ps.setTimestamp(paramIndex, new java.sql.Timestamp(cal.getTime().getTime()));
                } else {
                    // Fall back to generic setObject call without SQL type specified.
                    ps.setObject(paramIndex, inValue);
                }
            } else {
                // Fall back to generic setObject call with SQL type specified.
                ps.setObject(paramIndex, inValue, sqlType);
            }
        }
    }

    /**
     * Check whether the given value can be treated as a String value.
     */
    private static boolean isStringValue(Object inValue) {
        return ((inValue instanceof CharSequence) || (inValue instanceof StringWriter));
    }

    /**
     * Check whether the given value is a java.util.Date
     * (but not one of the JDBC-specific subclasses).
     */
    private static boolean isDateValue(Object inValue) {
        return ((inValue instanceof java.util.Date) && !((inValue instanceof java.sql.Date) || (inValue instanceof
            java.sql.Time) || (inValue instanceof java.sql.Timestamp)));
    }

}


/*
 * Copyright 2018 Sirsi Corporation.  All rights reserved.
 */

package liquibase.util;

import liquibase.exception.DateParseException;

import java.util.Calendar;
import java.util.Date;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * Handles 'NOW' and 'TODAY' for Date / Time type columns
 */
public final class NowAndTodayUtil {
    private static final String NOW = "now";
    private static final int NOW_LENGTH = NOW.length();
    private static final Pattern NOW_OFFSET_PATTERN = Pattern.compile("^(now)([+-])(\\d+)([mhdy].*)$");
    private static final String TODAY = "today";
    private static final int TODAY_LENGTH = TODAY.length();
    private static final Pattern TODAY_OFFSET_PATTERN = Pattern.compile("^(today)([+\\-])(\\d+)$");

    /**
     * Private constructor to prevent instantiation.
     */
    private NowAndTodayUtil() {
    }

    /**
     * Checks if date starts with "NOW" or "TODAY".
     *
     * @param value value to test
     * @return true if value starts with "NOW" or "TODAY" (case insensitive), false otherwise.
     */
    public static boolean isNowOrTodayFormat(String value) {
        boolean ret = false;
        if (value != null) {
            String lowerValue = value.toLowerCase();
            if (lowerValue.length() >= NOW_LENGTH && lowerValue.startsWith(NOW)) {
                ret = true;
            } else if (lowerValue.length() >= TODAY_LENGTH && lowerValue.startsWith(TODAY)) {
                ret = true;
            }
        }
        return ret;
    }

    /**
     * Determines value for "NOW" or "TODAY" prefixed value.
     * "NOW" format can simply be "NOW" (case insensitive), or "NOW" followed by a "+" or "=",
     * then an integer, then a unit. "M" or "MINUTE" or "MINUTES", "H" or "HOUR" or "HOURS", "D" or "DAY"
     * or "DAYS", "Y", "YEAR", or 'YEARS".
     * "TODAY" format can be simply "TODAY" (case insensitive), or "TODAY" followed by a "+" or "-",
     * then an integer, to calculate an offset from today.
     *
     * @param value   value to convert to a date
     * @param colType Column type (e.g. "timestamp", "time", "date", or "datetime")
     * @return calculated date, or null if is not a NOW or TODAY prefixed value.
     * @throws DateParseException if colType is not one of "timestamp", "time", "date", or "datetime"
     */
    //CHECKSTYLE:OFF Cyclomatic Complexity
    public static Date doNowOrToday(String value, String colType) throws DateParseException {
        if (!isNowOrTodayFormat(value)) {
            return null;
        }

        Calendar c = Calendar.getInstance();
        String lowerValue = value.toLowerCase();
        if (lowerValue.startsWith(NOW)) {
            parseNow(value, lowerValue, c);
        } else if (lowerValue.startsWith(TODAY)) {
            parseToday(value, lowerValue, c);
        } else {
            throw new DateParseException("Programmer error: " + value + " does not start with 'NOW' or 'TODAY'");
        }

        Date today = c.getTime();

        if (colType == null || colType.length() == 0) {
            throw new DateParseException("Must supply non-null column type when using 'NOW' or 'TODAY' value.");
        } else if (colType.equalsIgnoreCase("timestamp")) {
            return new java.sql.Timestamp(today.getTime());
        } else if (colType.equalsIgnoreCase("time")) {
            // A little odd using TODAY format with a TIME type column, but we'll do it - will get current time...
            return new java.sql.Time(today.getTime());
        } else if (colType.equalsIgnoreCase("date") || colType.equalsIgnoreCase("datetime")) {
            return new java.sql.Date(today.getTime());
        } else {
            throw new DateParseException("Unrecognized colType " + colType
                    + " when using 'NOW' or 'TODAY' value; expected one of date, time, datetime, or timestamp");
        }

    }
    //CHECKSTYLE:ON Cyclomatic Complexity

    /**
     * Parses "NOW" type specification, and applies offset (if any) to calendar value.
     * @param value original value
     * @param lowerValue lower cased value.
     * @param c Calendar with appropriate offset applied.
     * @throws DateParseException if anything is wrong with the NOW specification.
     */
    //CHECKSTYLE:OFF Cyclomatic Complexity
    private static void parseNow(String value, String lowerValue, Calendar c) throws DateParseException {
        if (lowerValue.length() > NOW_LENGTH) {
            Matcher matcher = NOW_OFFSET_PATTERN.matcher(lowerValue);
            if (!matcher.find()) {
                throw new DateParseException("Improper value in 'NOW' value: " + value
                        + ". 'NOW' must be followed by + or -, then numeric offset, then units (h{our{s}}, "
                        + "m{inute{s}}, d{ay{s}}, or y{ears}");
            }
            if (matcher.groupCount() != 4) {
                throw new DateParseException("Improper value in 'NOW' value: " + value + ". Pattern match returned "
                        + matcher.groupCount() + " instead of 4");
            }
            char sign = matcher.group(2).charAt(0);
            if (sign != '+' && sign != '-') {
                throw new DateParseException("Improper sign in 'NOW' value '" + lowerValue + "'");
            }
            String offsetStr = matcher.group(3);
            int offset;
            try {
                offset = Integer.parseInt(offsetStr);
            } catch (NumberFormatException e) {
                throw new DateParseException("Improper offset in 'NOW' value '" + value + "'");
            }
            if (sign == '-') {
                offset = -offset;
            }
            int calendarField;
            String units = matcher.group(4);
            if ("years".startsWith(units)) {
                calendarField = Calendar.YEAR;
            } else if ("days".startsWith(units)) {
                calendarField = Calendar.DATE;
            } else if ("hours".startsWith(units)) {
                calendarField = Calendar.HOUR;
            } else if ("minutes".startsWith(units)) {
                calendarField = Calendar.MINUTE;
            } else {
                throw new DateParseException("Improper units in 'NOW' value: '" + units + "', must be y{ear{s}}, "
                        + "d{ay{s}}, h{our{s}}, or m{inute{s}}");
            }
            c.add(calendarField, offset);
        }
    }
    //CHECKSTYLE:ON Cyclomatic Complexity

    /**
     * Parses "TODAY" type specification, and applies offset (if any) to calendar value.
     * @param value original value
     * @param lowerValue lower cased value.
     * @param c Calendar with appropriate offset applied.
     * @throws DateParseException if anything is wrong with the TODAY specification.
     */
    private static void parseToday(String value, String lowerValue, Calendar c) throws DateParseException {
        if (lowerValue.length() > TODAY_LENGTH) {
            Matcher matcher = TODAY_OFFSET_PATTERN.matcher(lowerValue);
            if (!matcher.find()) {
                throw new DateParseException("Improper value in 'TODAY' value: " + value
                        + ". 'TODAY' must be followed by + or -, then numeric offset");
            }
            if (matcher.groupCount() != 3) {
                throw new DateParseException("Improper value in 'TODAY' value: " + value + ". Pattern match returned "
                        + matcher.groupCount() + " instead of 3");
            }
            char sign = matcher.group(2).charAt(0);
            if (sign != '+' && sign != '-') {
                throw new DateParseException("Improper sign in 'TODAY' value '" + lowerValue + "'");
            }
            String offsetStr = matcher.group(3);
            int offset;
            try {
                offset = Integer.parseInt(offsetStr);
            } catch (NumberFormatException e) {
                throw new DateParseException("Improper offset in 'TODAY' value '" + value + "'");
            }
            if (sign == '-') {
                offset = -offset;
            }
            c.add(Calendar.DATE, offset);
        }
    }
}