package liquibase.integration.servlet;

import liquibase.Contexts;
import liquibase.LabelExpression;
import liquibase.Liquibase;
import liquibase.Scope;
import liquibase.configuration.ConfigurationProperty;
import liquibase.configuration.ConfigurationValueProvider;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.DatabaseFactory;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.resource.ClassLoaderResourceAccessor;
import liquibase.resource.CompositeResourceAccessor;
import liquibase.resource.FileSystemResourceAccessor;
import liquibase.resource.ResourceAccessor;
import liquibase.util.NetUtil;
import liquibase.util.StringUtil;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletContext;
import javax.servlet.ServletContextEvent;
import javax.servlet.ServletContextListener;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Enumeration;

/**
 * Servlet listener than can be added to web.xml to allow Liquibase to run on every application server startup.
 * Using this listener allows users to know that they always have the most up to date database, although it will
 * slow down application server startup slightly.
 * See the Liquibase documentation for
 * more information.
 */
public class LiquibaseServletListener implements ServletContextListener {

    private static final String JAVA_COMP_ENV = "java:comp/env";
    private static final String LIQUIBASE_CHANGELOG = "liquibase.changelog";
    private static final String LIQUIBASE_CONTEXTS = "liquibase.contexts";
    private static final String LIQUIBASE_LABELS = "liquibase.labels";
    private static final String LIQUIBASE_DATASOURCE = "liquibase.datasource";
    private static final String LIQUIBASE_HOST_EXCLUDES = "liquibase.host.excludes";
    private static final String LIQUIBASE_HOST_INCLUDES = "liquibase.host.includes";
    private static final String LIQUIBASE_ONERROR_FAIL = "liquibase.onerror.fail";
    private static final String LIQUIBASE_PARAMETER = "liquibase.parameter";
    private static final String LIQUIBASE_SCHEMA_DEFAULT = "liquibase.schema.default";

    private String changeLogFile;
    private String dataSourceName;
    private String contexts;
    private String labels;
    private String defaultSchema;
    private String hostName;
    private ServletValueContainer servletValueContainer; //temporarily saved separately until all lookup moves to liquibaseConfiguration

    public String getChangeLogFile() {
        return changeLogFile;
    }

    public void setChangeLogFile(String changeLogFile) {
        this.changeLogFile = changeLogFile;
    }

    public String getContexts() {
        return contexts;
    }

    public void setContexts(String ctxt) {
        contexts = ctxt;
    }

    public String getLabels() {
        return labels;
    }

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

    public String getDataSource() {
        return dataSourceName;
    }

    /**
     * Sets the name of the data source.
     */
    public void setDataSource(String dataSource) {
        this.dataSourceName = dataSource;
    }

    public String getDefaultSchema() {
        return defaultSchema;
    }

    @Override
    public void contextInitialized(ServletContextEvent servletContextEvent) {
        ServletContext servletContext = servletContextEvent.getServletContext();
        try {
            this.hostName = NetUtil.getLocalHostName();
        }
        catch (Exception e) {
            servletContext.log("Cannot find hostname: " + e.getMessage());
            return;
        }

        InitialContext ic = null;
        String failOnError = null;
        try {
            ic = new InitialContext();

            servletValueContainer = new ServletValueContainer(servletContext, ic);
            LiquibaseConfiguration.getInstance().init(servletValueContainer);

            failOnError = (String) servletValueContainer.getValue(LIQUIBASE_ONERROR_FAIL);
            if (checkPreconditions(servletContext, ic)) {
                executeUpdate(servletContext, ic);
            }

        } catch (Exception e) {
            if (!"false".equals(failOnError)) {
                throw new RuntimeException(e);
            }
        } finally {
            if (ic != null) {
                try {
                    ic.close();
                }
                catch (NamingException e) {
                    // ignore
                }
            }
        }
    }

    /**
     * Checks if the update is supposed to be executed. That depends on several conditions:
     *

     *
if liquibase.shouldRun is false the update will not be executed.

     *
if {@value LiquibaseServletListener#LIQUIBASE_HOST_INCLUDES} contains the current hostname, the the update will be executed.

     *
if {@value LiquibaseServletListener#LIQUIBASE_HOST_EXCLUDES} contains the current hostname, the the update will not be executed.

     *

     */
    private boolean checkPreconditions(ServletContext servletContext, InitialContext ic) {
        GlobalConfiguration globalConfiguration = LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class);
        if (!globalConfiguration.getShouldRun()) {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Liquibase did not run on " + hostName
                    + " because "+ LiquibaseConfiguration.getInstance().describeValueLookupLogic(globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN))
                            + " was set to false");
            return false;
        }

        String machineIncludes = (String) servletValueContainer.getValue(LIQUIBASE_HOST_INCLUDES);
        String machineExcludes = (String) servletValueContainer.getValue(LIQUIBASE_HOST_EXCLUDES);

        boolean shouldRun = false;
        if ((machineIncludes == null) && (machineExcludes == null)) {
            shouldRun = true;
        } else if (machineIncludes != null) {
            for (String machine : machineIncludes.split(",")) {
                machine = machine.trim();
                if (hostName.equalsIgnoreCase(machine)) {
                    shouldRun = true;
                }
            }
        } else if (machineExcludes != null) {
            shouldRun = true;
            for (String machine : machineExcludes.split(",")) {
                machine = machine.trim();
                if (hostName.equalsIgnoreCase(machine)) {
                    shouldRun = false;
                }
            }
        }

        if (globalConfiguration.getShouldRun() && globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN).getWasOverridden()) {
            shouldRun = true;
            servletContext.log("ignoring " + LIQUIBASE_HOST_INCLUDES + " and "
                    + LIQUIBASE_HOST_EXCLUDES + ", since " + LiquibaseConfiguration.getInstance().describeValueLookupLogic(globalConfiguration.getProperty(GlobalConfiguration.SHOULD_RUN))
                    + "=true");
        }
        if (!shouldRun) {
            servletContext.log("LiquibaseServletListener did not run due to "
                    + LIQUIBASE_HOST_INCLUDES + " and/or " + LIQUIBASE_HOST_EXCLUDES + "");
            return false;
        }
        return true;
    }

    /**
     * Executes the Liquibase update.
     */
    private void executeUpdate(ServletContext servletContext, InitialContext ic) throws NamingException, SQLException, LiquibaseException {
        setDataSource((String) servletValueContainer.getValue(LIQUIBASE_DATASOURCE));
        if (getDataSource() == null) {
            throw new RuntimeException("Cannot run Liquibase, " + LIQUIBASE_DATASOURCE + " is not set");
        }

        setChangeLogFile((String) servletValueContainer.getValue(LIQUIBASE_CHANGELOG));
        if (getChangeLogFile() == null) {
            throw new RuntimeException("Cannot run Liquibase, " + LIQUIBASE_CHANGELOG + " is not set");
        }

        setContexts((String) servletValueContainer.getValue(LIQUIBASE_CONTEXTS));
        setLabels((String) servletValueContainer.getValue(LIQUIBASE_LABELS));
        this.defaultSchema = StringUtil.trimToNull((String) servletValueContainer.getValue(LIQUIBASE_SCHEMA_DEFAULT));

        Connection connection = null;
        Database database = null;
        try {
            DataSource dataSource = (DataSource) ic.lookup(this.dataSourceName);

            connection = dataSource.getConnection();

            Thread currentThread = Thread.currentThread();
            ClassLoader contextClassLoader = currentThread.getContextClassLoader();
            ResourceAccessor threadClFO = new ClassLoaderResourceAccessor(contextClassLoader);

            ResourceAccessor clFO = new ClassLoaderResourceAccessor();
            ResourceAccessor fsFO = new FileSystemResourceAccessor();

            database = DatabaseFactory.getInstance().findCorrectDatabaseImplementation(new JdbcConnection(connection));
            database.setDefaultSchemaName(getDefaultSchema());
            Liquibase liquibase = new Liquibase(getChangeLogFile(), new CompositeResourceAccessor(clFO, fsFO, threadClFO), database);

            @SuppressWarnings("unchecked")
            Enumeration<String> initParameters = servletContext.getInitParameterNames();
            while (initParameters.hasMoreElements()) {
                String name = initParameters.nextElement().trim();
                if (name.startsWith(LIQUIBASE_PARAMETER + ".")) {
                    liquibase.setChangeLogParameter(name.substring(LIQUIBASE_PARAMETER.length() + 1), servletValueContainer.getValue(name));
                }
            }

            liquibase.update(new Contexts(getContexts()), new LabelExpression(getLabels()));
        }
        finally {
            if (database != null) {
                database.close();
            } else if (connection != null) {
                connection.close();
            }
        }
    }

    @Override
    public void contextDestroyed(ServletContextEvent servletContextEvent) {
    }

    protected class ServletValueContainer implements ConfigurationValueProvider {

        private ServletContext servletContext;
        private InitialContext initialContext;

        public ServletValueContainer(ServletContext servletContext, InitialContext initialContext) {
            this.servletContext = servletContext;
            this.initialContext = initialContext;
        }

        @Override
        public String describeValueLookupLogic(ConfigurationProperty property) {
            return "JNDI, servlet container init parameter, and system property '"+property.getNamespace()+"."+property.getName()+"'";
        }

        @Override
        public Object getValue(String namespace, String property) {
            return getValue(namespace +"."+property);
        }

        /**
         * Try to read the value that is stored by the given key from
         *

         *
JNDI

         *
the servlet context's init parameters

         *
system properties

         *

         */
        public Object getValue(String prefixAndProperty) {
            // Try to get value from JNDI
            try {
                Context envCtx = (Context) initialContext.lookup(JAVA_COMP_ENV);
                String valueFromJndi = (String) envCtx.lookup(prefixAndProperty);
                return valueFromJndi;
            }
            catch (NamingException e) {
                // Ignore
            }

            // Return the value from the servlet context
            String valueFromServletContext = servletContext.getInitParameter(prefixAndProperty);
            if (valueFromServletContext != null) {
                return valueFromServletContext;
            }

            // Otherwise: Return system property
            return System.getProperty(prefixAndProperty);
        }
    }
}


package liquibase.sqlgenerator.core;

import liquibase.database.Database;
import liquibase.exception.Warnings;
import liquibase.sqlgenerator.SqlGenerator;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.statement.SqlStatement;

/**
 * Generic template class for an SQL generator able to generate SQL for an object-form
 * {@link liquibase.statement.SqlStatement}.
 *
 * @param <T> an implementation of the {@link liquibase.statement.SqlStatement} specifying which
 *            logical SQL statement an instance of this template class is able to generate SQL for.
 */
public abstract class AbstractSqlGenerator<T extends SqlStatement> implements SqlGenerator<T> {

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

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

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

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

    @Override
    public Warnings warn(T statementType, Database database, SqlGeneratorChain<T> sqlGeneratorChain) {
        return sqlGeneratorChain.warn(statementType, database);
    }

    /**
     * Tries to find out if a given value (part a database-specific SQL statement) is just a simple literal
     * (e.g. 'Jones', 149.99 or false) or if it is a call to a function within the database (e.g.
     * TO_DATE('28.12.2017', 'dd.mm.yyyy') in Oracle DB).
     * This method is often used to determine if we need to quote literals which are Strings, e.g. if we determine
     * "Dr. Jones" to be a literal, then we generate
     *
{@code INSERT INTO customers(last_name) VALUES('Dr. Jones')}

     * but if the value is a function call, we may not quote it, e.g. for "TO_DATE('28.12.2017', 'dd.mm.yyyy')", we
     * need to generate (note the absence of the apostrophes!):
     *
{@code INSERT INTO bookings(booking_date) VALUES (TO_DATE('28.12.2017', 'dd.mm.yyyy'))}

     * @param value The string to test
     * @param database the database object to test against
     * @return true if value looks like a function call, false if it looks like a literal.
     */
    public boolean looksLikeFunctionCall(String value, Database database) {
        // TODO: SYSIBM looks DB2-specific, we should move that out of AbstractSqlGenerator into a DB2-specific class.
        return value.startsWith("\"SYSIBM\"") || value.startsWith("to_date(") ||
            value.equalsIgnoreCase(database.getCurrentDateTimeFunction());
    }

}


package liquibase.database.core;

import java.lang.reflect.Method;
import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.database.OfflineConnection;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.DatabaseException;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.exception.ValidationErrors;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.statement.DatabaseFunction;
import liquibase.statement.SequenceCurrentValueFunction;
import liquibase.statement.SequenceNextValueFunction;
import liquibase.statement.core.RawCallStatement;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Catalog;
import liquibase.structure.core.Index;
import liquibase.structure.core.PrimaryKey;
import liquibase.structure.core.Schema;
import liquibase.util.JdbcUtils;
import liquibase.util.StringUtil;

import java.util.*;

import static java.util.ResourceBundle.getBundle;

/**
 * Encapsulates Oracle database support.
 */
public class OracleDatabase extends AbstractJdbcDatabase {
    public static final String PRODUCT_NAME = "oracle";
    private static ResourceBundle coreBundle = getBundle("liquibase/i18n/liquibase-core");
    protected final int SHORT_IDENTIFIERS_LENGTH = 30;
    protected final int LONG_IDENTIFIERS_LEGNTH = 128;
    protected final int ORACLE_12C_MAJOR_VERSION = 12;

    private Set<String> reservedWords = new HashSet<>();
    private Set<String> userDefinedTypes;
    private Map<String, String> savedSessionNlsSettings;

    private Boolean canAccessDbaRecycleBin;
    private Integer databaseMajorVersion;
    private Integer databaseMinorVersion;

    /**
     * Default constructor for an object that represents the Oracle Database DBMS.
     */
    public OracleDatabase() {
        super.unquotedObjectsAreUppercased=true;
        //noinspection HardCodedStringLiteral
        super.setCurrentDateTimeFunction("SYSTIMESTAMP");
        // Setting list of Oracle's native functions
        //noinspection HardCodedStringLiteral
        dateFunctions.add(new DatabaseFunction("SYSDATE"));
        //noinspection HardCodedStringLiteral
        dateFunctions.add(new DatabaseFunction("SYSTIMESTAMP"));
        //noinspection HardCodedStringLiteral
        dateFunctions.add(new DatabaseFunction("CURRENT_TIMESTAMP"));
        //noinspection HardCodedStringLiteral
        super.sequenceNextValueFunction = "%s.nextval";
        //noinspection HardCodedStringLiteral
        super.sequenceCurrentValueFunction = "%s.currval";
    }
    @Override
    public int getPriority() {
        return PRIORITY_DEFAULT;
    }

    @Override
    public void setConnection(DatabaseConnection conn) {
        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,
        // HardCodedStringLiteral
        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,
        // HardCodedStringLiteral
        reservedWords.addAll(Arrays.asList("GROUP", "USER", "SESSION", "PASSWORD", "RESOURCE", "START", "SIZE", "UID", "DESC", "ORDER")); //more reserved words not returned by driver

        Connection sqlConn = null;
        if (!(conn instanceof OfflineConnection)) {
            try {
                /*
                 * Don't try to call getWrappedConnection if the conn instance is
                 * is not a JdbcConnection. This happens for OfflineConnection.
                 * see https://liquibase.jira.com/browse/CORE-2192
                 */
                if (conn instanceof JdbcConnection) {
                    sqlConn = ((JdbcConnection) conn).getWrappedConnection();
                }
            } catch (Exception e) {
                throw new UnexpectedLiquibaseException(e);
            }

            if (sqlConn != null) {
                try {
                    //noinspection HardCodedStringLiteral
                    reservedWords.addAll(Arrays.asList(sqlConn.getMetaData().getSQLKeywords().toUpperCase().split(",\\s*")));
                } catch (SQLException e) {
                    //noinspection HardCodedStringLiteral
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Could get sql keywords on OracleDatabase: " + e.getMessage());
                    //can not get keywords. Continue on
                }
                try {
                    Method method = sqlConn.getClass().getMethod("setRemarksReporting", Boolean.TYPE);
                    method.setAccessible(true);
                    method.invoke(sqlConn, true);
                } catch (Exception e) {
                    //noinspection HardCodedStringLiteral
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Could not set remarks reporting on OracleDatabase: " + e.getMessage());

                    //cannot set it. That is OK
                }

                Statement statement = null;
                ResultSet resultSet = null;
                try {
                    statement = sqlConn.createStatement();
                    //noinspection HardCodedStringLiteral
                    resultSet = statement.executeQuery("SELECT value FROM v$parameter WHERE name = 'compatible'");
                    String compatibleVersion = null;
                    if (resultSet.next()) {
                        //noinspection HardCodedStringLiteral
                        compatibleVersion = resultSet.getString("value");
                    }
                    if (compatibleVersion != null) {
                        Matcher majorVersionMatcher = Pattern.compile("(\\d+)\\.(\\d+)\\..*").matcher(compatibleVersion);
                        if (majorVersionMatcher.matches()) {
                            this.databaseMajorVersion = Integer.valueOf(majorVersionMatcher.group(1));
                            this.databaseMinorVersion = Integer.valueOf(majorVersionMatcher.group(2));
                        }
                    }
                } catch (SQLException e) {
                    @SuppressWarnings("HardCodedStringLiteral") String message = "Cannot read from v$parameter: "+e.getMessage();

                    //noinspection HardCodedStringLiteral
                    Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Could not set check compatibility mode on OracleDatabase, assuming not running in any sort of compatibility mode: " + message);
                } finally {
                    JdbcUtils.close(resultSet, statement);
                }

            }
        }
        super.setConnection(conn);
    }

    @Override
    public String getShortName() {
        //noinspection HardCodedStringLiteral
        return "oracle";
    }

    @Override
    protected String getDefaultDatabaseProductName() {
        //noinspection HardCodedStringLiteral
        return "Oracle";
    }

    @Override
    public int getDatabaseMajorVersion() throws DatabaseException {
        if (databaseMajorVersion == null) {
            return super.getDatabaseMajorVersion();
        } else {
            return databaseMajorVersion;
        }
    }

    @Override
    public int getDatabaseMinorVersion() throws DatabaseException {
        if (databaseMinorVersion == null) {
            return super.getDatabaseMinorVersion();
        } else {
            return databaseMinorVersion;
        }
    }

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

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

    @Override
    public String getJdbcSchemaName(CatalogAndSchema schema) {
        return correctObjectName((schema.getCatalogName() == null) ? schema.getSchemaName() : schema.getCatalogName(), Schema.class);
    }

    @Override
    public String generatePrimaryKeyName(String tableName) {
        if (tableName.length() > 27) {
            //noinspection HardCodedStringLiteral
            return "PK_" + tableName.toUpperCase(Locale.US).substring(0, 27);
        } else {
            //noinspection HardCodedStringLiteral
            return "PK_" + tableName.toUpperCase(Locale.US);
        }
    }

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

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

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

    /**
     * Oracle supports catalogs in liquibase terms
     *
     * @return false
     */
    @Override
    public boolean supportsSchemas() {
        return false;
    }

    @Override
    protected String getConnectionCatalogName() throws DatabaseException {
        if (getConnection() instanceof OfflineConnection) {
            return getConnection().getCatalog();
        }
        try {
            //noinspection HardCodedStringLiteral
            return ExecutorService.getInstance().getExecutor(this).queryForObject(new RawCallStatement("select sys_context( 'userenv', 'current_schema' ) from dual"), String.class);
        } catch (Exception e) {
            //noinspection HardCodedStringLiteral
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Error getting default schema", e);
        }
        return null;
    }

    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        return PRODUCT_NAME.equalsIgnoreCase(conn.getDatabaseProductName());
    }

    @Override
    public String getDefaultDriver(String url) {
        //noinspection HardCodedStringLiteral
        if (url.startsWith("jdbc:oracle")) {
            return "oracle.jdbc.OracleDriver";
        }
        return null;
    }

    @Override
    public String getDefaultCatalogName() {//NOPMD
        return (super.getDefaultCatalogName() == null) ? null : super.getDefaultCatalogName().toUpperCase(Locale.US);
    }

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

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

     *
     * Implementation restriction:

     * Currently, only the following subsets of ISO8601 are supported:

     *

     *
YYYY-MM-DD

     *
YYYY-MM-DDThh:mm:ss

     *

     */
    @Override
    public String getDateLiteral(String isoDate) {
        String normalLiteral = super.getDateLiteral(isoDate);

        if (isDateOnly(isoDate)) {
            StringBuffer val = new StringBuffer();
            //noinspection HardCodedStringLiteral
            val.append("TO_DATE(");
            val.append(normalLiteral);
            //noinspection HardCodedStringLiteral
            val.append(", 'YYYY-MM-DD')");
            return val.toString();
        } else if (isTimeOnly(isoDate)) {
            StringBuffer val = new StringBuffer();
            //noinspection HardCodedStringLiteral
            val.append("TO_DATE(");
            val.append(normalLiteral);
            //noinspection HardCodedStringLiteral
            val.append(", 'HH24:MI:SS')");
            return val.toString();
        } else if (isTimestamp(isoDate)) {
            StringBuffer val = new StringBuffer(26);
            //noinspection HardCodedStringLiteral
            val.append("TO_TIMESTAMP(");
            val.append(normalLiteral);
            //noinspection HardCodedStringLiteral
            val.append(", 'YYYY-MM-DD HH24:MI:SS.FF')");
            return val.toString();
        } else if (isDateTime(isoDate)) {
            int seppos = normalLiteral.lastIndexOf('.');
            if (seppos != -1) {
                normalLiteral = normalLiteral.substring(0, seppos) + "'";
            }
            StringBuffer val = new StringBuffer(26);
            //noinspection HardCodedStringLiteral
            val.append("TO_DATE(");
            val.append(normalLiteral);
            //noinspection HardCodedStringLiteral
            val.append(", 'YYYY-MM-DD HH24:MI:SS')");
            return val.toString();
        }
        //noinspection HardCodedStringLiteral
        return "UNSUPPORTED:" + isoDate;
    }

    @Override
    public boolean isSystemObject(DatabaseObject example) {
        if (example == null) {
            return false;
        }

        if (this.isLiquibaseObject(example)) {
            return false;
        }

        if (example instanceof Schema) {
            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
            if ("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName())|| "XDB".equals(example.getName())) {
                return true;
            }
            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
            if ("SYSTEM".equals(example.getSchema().getCatalogName()) || "SYS".equals(example.getSchema().getCatalogName()) || "CTXSYS".equals(example.getSchema().getCatalogName()) || "XDB".equals(example.getSchema().getCatalogName())) {
                return true;
            }
        } else if (isSystemObject(example.getSchema())) {
            return true;
        }
        if (example instanceof Catalog) {
            //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
            if (("SYSTEM".equals(example.getName()) || "SYS".equals(example.getName()) || "CTXSYS".equals(example.getName()) || "XDB".equals(example.getName()))) {
                return true;
            }
        } else if (example.getName() != null) {
            //noinspection HardCodedStringLiteral
            if (example.getName().startsWith("BIN$")) { //oracle deleted table
                boolean filteredInOriginalQuery = this.canAccessDbaRecycleBin();
                if (!filteredInOriginalQuery) {
                    filteredInOriginalQuery = StringUtil.trimToEmpty(example.getSchema().getName()).equalsIgnoreCase(this.getConnection().getConnectionUserName());
                }

                if (filteredInOriginalQuery) {
                    return !((example instanceof PrimaryKey) || (example instanceof Index) || (example instanceof
                        liquibase.statement.UniqueConstraint));
                } else {
                    return true;
                }
            } else //noinspection HardCodedStringLiteral
                if (example.getName().startsWith("AQ$")) { //oracle AQ tables
                return true;
            } else //noinspection HardCodedStringLiteral
                if (example.getName().startsWith("DR$")) { //oracle index tables
                return true;
            } else //noinspection HardCodedStringLiteral
                    if (example.getName().startsWith("SYS_IOT_OVER")) { //oracle system table
                return true;
            } else //noinspection HardCodedStringLiteral,HardCodedStringLiteral
                        if ((example.getName().startsWith("MDRT_") || example.getName().startsWith("MDRS_")) && example.getName().endsWith("$")) {
                // CORE-1768 - Oracle creates these for spatial indices and will remove them when the index is removed.
                return true;
            } else //noinspection HardCodedStringLiteral
                            if (example.getName().startsWith("MLOG$_")) { //Created by materliaized view logs for every table that is part of a materialized view. Not available for DDL operations.
                return true;
            } else //noinspection HardCodedStringLiteral
                    if (example.getName().startsWith("RUPD$_")) { //Created by materialized view log tables using primary keys. Not available for DDL operations.
                return true;
            } else //noinspection HardCodedStringLiteral
                        if (example.getName().startsWith("WM$_")) { //Workspace Manager backup tables.
                return true;
            } else //noinspection HardCodedStringLiteral
                        if ("CREATE$JAVA$LOB$TABLE".equals(example.getName())) { //This table contains the name of the Java object, the date it was loaded, and has a BLOB column to store the Java object.
                return true;
            } else //noinspection HardCodedStringLiteral
                    if ("JAVA$CLASS$MD5$TABLE".equals(example.getName())) { //This is a hash table that tracks the loading of Java objects into a schema.
                return true;
            } else //noinspection HardCodedStringLiteral
                    if (example.getName().startsWith("ISEQ$$_")) { //System-generated sequence
                return true;
            } else //noinspection HardCodedStringLiteral
                if (example.getName().startsWith("USLOG$")) { //for update materialized view
                return true;
            } else if (example.getName().startsWith("SYS_FBA")) { //for Flashback tables
                return true;
            }
        }

        return super.isSystemObject(example);
    }

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

    @Override
    public boolean supportsAutoIncrement() {
        // Oracle supports Identity beginning with version 12c
        boolean isAutoIncrementSupported = false;

        try {
            if (getDatabaseMajorVersion() >= 12) {
                isAutoIncrementSupported = true;
            }

            // Returning true will generate create table command with 'IDENTITY' clause, example:
            // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) GENERATED BY DEFAULT AS IDENTITY NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));

            // While returning false will continue to generate create table command without 'IDENTITY' clause, example:
            // CREATE TABLE AutoIncTest (IDPrimaryKey NUMBER(19) NOT NULL, TypeID NUMBER(3) NOT NULL, Description NVARCHAR2(50), CONSTRAINT PK_AutoIncTest PRIMARY KEY (IDPrimaryKey));

        } catch (DatabaseException ex) {
            isAutoIncrementSupported = false;
        }

        return isAutoIncrementSupported;
    }

//    public Set<UniqueConstraint> findUniqueConstraints(String schema) throws DatabaseException {
//        Set<UniqueConstraint> returnSet = new HashSet<UniqueConstraint>();
//
//        List<Map> maps = new Executor(this).queryForList(new RawSqlStatement("SELECT UC.CONSTRAINT_NAME, UCC.TABLE_NAME, UCC.COLUMN_NAME FROM USER_CONSTRAINTS UC, USER_CONS_COLUMNS UCC WHERE UC.CONSTRAINT_NAME=UCC.CONSTRAINT_NAME AND CONSTRAINT_TYPE='U' ORDER BY UC.CONSTRAINT_NAME"));
//
//        UniqueConstraint constraint = null;
//        for (Map map : maps) {
//            if (constraint == null || !constraint.getName().equals(constraint.getName())) {
//                returnSet.add(constraint);
//                Table table = new Table((String) map.get("TABLE_NAME"));
//                constraint = new UniqueConstraint(map.get("CONSTRAINT_NAME").toString(), table);
//            }
//        }
//        if (constraint != null) {
//            returnSet.add(constraint);
//        }
//
//        return returnSet;
//    }

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

    @Override
    public int getDataTypeMaxParameters(String dataTypeName) {
        //noinspection HardCodedStringLiteral
        if ("BINARY_FLOAT".equals(dataTypeName.toUpperCase())) {
            return 0;
        }
        //noinspection HardCodedStringLiteral
        if ("BINARY_DOUBLE".equals(dataTypeName.toUpperCase())) {
            return 0;
        }
        return super.getDataTypeMaxParameters(dataTypeName);
    }

    public String getSystemTableWhereClause(String tableNameColumn) {
        List<String> clauses = new ArrayList<String>(Arrays.asList("BIN$",
                "AQ$",
                "DR$",
                "SYS_IOT_OVER",
                "MLOG$_",
                "RUPD$_",
                "WM$_",
                "ISEQ$$_",
                "USLOG$",
                "SYS_FBA"));

        for (int i = 0;i<clauses.size(); i++) {
            clauses.set(i, tableNameColumn+" NOT LIKE '"+clauses.get(i)+"%'");
            }
        return "("+ StringUtil.join(clauses, " AND ")+")";
    }

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

    public Set<String> getUserDefinedTypes() {
        if (userDefinedTypes == null) {
            userDefinedTypes = new HashSet<>();
            if ((getConnection() != null) && !(getConnection() instanceof OfflineConnection)) {
                try {
                    try {
                        //noinspection HardCodedStringLiteral
                        userDefinedTypes.addAll(ExecutorService.getInstance().getExecutor(this).queryForList(new RawSqlStatement("SELECT DISTINCT TYPE_NAME FROM ALL_TYPES"), String.class));
                    } catch (DatabaseException e) { //fall back to USER_TYPES if the user cannot see ALL_TYPES
                        //noinspection HardCodedStringLiteral
                        userDefinedTypes.addAll(ExecutorService.getInstance().getExecutor(this).queryForList(new RawSqlStatement("SELECT TYPE_NAME FROM USER_TYPES"), String.class));
                    }
                } catch (DatabaseException e) {
                    //ignore error
                }
            }
        }

        return userDefinedTypes;
    }

    @Override
    public String generateDatabaseFunctionValue(DatabaseFunction databaseFunction) {
        //noinspection HardCodedStringLiteral
        if ((databaseFunction != null) && "current_timestamp".equalsIgnoreCase(databaseFunction.toString())) {
            return databaseFunction.toString();
        }
        if((databaseFunction instanceof SequenceNextValueFunction) || (databaseFunction instanceof
            SequenceCurrentValueFunction)){
            String quotedSeq = super.generateDatabaseFunctionValue(databaseFunction);
            // replace "myschema.my_seq".nextval with "myschema"."my_seq".nextval
            return quotedSeq.replaceFirst("\"([^\\.\"]+)\\.([^\\.\"]+)\"","\"$1\".\"$2\"");

        }

        return super.generateDatabaseFunctionValue(databaseFunction);
    }

    @Override
    public ValidationErrors validate() {
        ValidationErrors errors = super.validate();
        DatabaseConnection connection = getConnection();
        if ((connection == null) || (connection instanceof OfflineConnection)) {
            //noinspection HardCodedStringLiteral
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Cannot validate offline database");
            return errors;
        }

        if (!canAccessDbaRecycleBin()) {
            errors.addWarning(getDbaRecycleBinWarning());
        }

        return errors;

    }

    public String getDbaRecycleBinWarning() {
        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral,
        // HardCodedStringLiteral
        //noinspection HardCodedStringLiteral,HardCodedStringLiteral,HardCodedStringLiteral
        return "Liquibase needs to access the DBA_RECYCLEBIN table so we can automatically handle the case where " +
        "constraints are deleted and restored. Since Oracle doesn't properly restore the original table names " +
        "referenced in the constraint, we use the information from the DBA_RECYCLEBIN to automatically correct this" +
        " issue.\n" +
        "\n" +
        "The user you used to connect to the database (" + getConnection().getConnectionUserName() +
        ") needs to have \"SELECT ON SYS.DBA_RECYCLEBIN\" permissions set before we can perform this operation. " +
         "Please run the following SQL to set the appropriate permissions, and try running the command again.\n" +
         "\n" +
         "     GRANT SELECT ON SYS.DBA_RECYCLEBIN TO " + getConnection().getConnectionUserName() + ";";
    }

    public boolean canAccessDbaRecycleBin() {
        if (canAccessDbaRecycleBin == null) {
            DatabaseConnection connection = getConnection();
            if ((connection == null) || (connection instanceof OfflineConnection)) {
                return false;
            }

            Statement statement = null;
            try {
                statement = ((JdbcConnection) connection).createStatement();
                @SuppressWarnings("HardCodedStringLiteral") ResultSet resultSet = statement.executeQuery("select 1 from dba_recyclebin where 0=1");
                resultSet.close(); //don't need to do anything with the result set, just make sure statement ran.
                this.canAccessDbaRecycleBin = true;
            } catch (Exception e) {
                //noinspection HardCodedStringLiteral
                if ((e instanceof SQLException) && e.getMessage().startsWith("ORA-00942")) { //ORA-00942: table or view does not exist
                    this.canAccessDbaRecycleBin = false;
                } else {
                    //noinspection HardCodedStringLiteral
                    Scope.getCurrentScope().getLog(getClass()).warning(LogType.LOG, "Cannot check dba_recyclebin access", e);
                    this.canAccessDbaRecycleBin = false;
                }
            } finally {
                JdbcUtils.close(null, statement);
            }
        }

        return canAccessDbaRecycleBin;
    }

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

    /** Tests if the given String would be a valid identifier in Oracle DBMS. In Oracle, a valid identifier has
     * the following form (case-insensitive comparison):
     * 1st character: A-Z
     * 2..n characters: A-Z0-9$_#
     * The maximum length of an identifier differs by Oracle version and object type.
     */
    public boolean isValidOracleIdentifier(String identifier, Class<? extends DatabaseObject> type) {
        if ((identifier == null) || (identifier.length() < 1))
            return false;

        if (!identifier.matches("^(i?)[A-Z][A-Z0-9\\$\\_\\#]*$"))
            return false;

        /*
         * @todo It seems we currently do not have a class for tablespace identifiers, and all other classes
         * we do know seem to be supported as 12cR2 long identifiers, so:
         */
        return (identifier.length() <= LONG_IDENTIFIERS_LEGNTH);
    }

    /**
     * Returns the maximum number of bytes (NOT: characters) for an identifier. For Oracle <=12c Release 20, this
     * is 30 bytes, and starting from 12cR2, up to 128 (except for tablespaces, PDB names and some other rather rare
     * object types).
     * @return the maximum length of an object identifier, in bytes
     */
    public int getIdentifierMaximumLength() {
        try {
            if (getDatabaseMajorVersion() < ORACLE_12C_MAJOR_VERSION) {
                return SHORT_IDENTIFIERS_LENGTH;
            }
            else if ((getDatabaseMajorVersion() == ORACLE_12C_MAJOR_VERSION) && (getDatabaseMinorVersion() <= 1)) {
                return SHORT_IDENTIFIERS_LENGTH;
            } else {
                return LONG_IDENTIFIERS_LEGNTH;
            }
        } catch (DatabaseException ex) {
            throw new UnexpectedLiquibaseException("Cannot determine the Oracle database version number", ex);
        }

    }

}


package liquibase.plugin;

import liquibase.Scope;
import liquibase.servicelocator.ServiceLocator;

import java.util.*;

/**
 * Convenience base class for all factories that find correct {@link Plugin} implementations.
 */
public abstract class AbstractPluginFactory<T extends Plugin> implements PluginFactory {

    private Collection<T> allInstances;

    protected AbstractPluginFactory() {

    }

    protected abstract Class<T> getPluginClass();

    /**
     * Returns the priority of the given object based on the passed args array.
     * The args are created as as part of the custom public getPlugin method in implementations are are passed through {@link #getPlugin(Object...)}
     */
    protected abstract int getPriority(T obj, Object... args);

    /**
     * Finds the plugin for which {@link #getPriority(Plugin, Object...)}  returns the highest value for the given scope and args.
     * This method is called by a public implementation-specific methods.
     * Normally this does not need to be overridden, instead override {@link #getPriority(Plugin, Object...)} to compute the priority of each object for the scope and arguments passed to this method.
     *
     * @return null if no plugins are found or have a priority greater than zero.
     */
    protected T getPlugin(final Object... args) {
        TreeSet<T> applicable = new TreeSet<>((o1, o2) -> {
            Integer o1Priority = getPriority(o1, args);
            Integer o2Priority = getPriority(o2, args);

            int i = o2Priority.compareTo(o1Priority);
            if (i == 0) {
                return o1.getClass().getName().compareTo(o2.getClass().getName());
            }
            return i;
        });

        for (T plugin : findAllInstances()) {
            if (getPriority(plugin, args) >= 0) {
                applicable.add(plugin);
            }
        }

        if (applicable.size() == 0) {
            return null;
        }

        return applicable.iterator().next();

    }

    public void register(T plugin){
        this.findAllInstances();
        this.allInstances.add(plugin);
    }

    /**
     * Finds implementations of the given interface or class and returns instances of them.
     * Standard implementation uses {@link ServiceLoader} to find implementations and caches results in {@link #allInstances} which means the same objects are always returned.
     * If the instances should not be treated as singletons, clone the objects before returning them from {@link #getPlugin(Object...)}.
     */
    protected synchronized Collection<T> findAllInstances() {
        if (this.allInstances == null) {
            this.allInstances = new ArrayList<>();

            ServiceLocator serviceLocator = Scope.getCurrentScope().getServiceLocator();
            this.allInstances.addAll(serviceLocator.findInstances(getPluginClass()));
        }

        return this.allInstances;
    }

    protected void removeInstance(T instance) {
        if (this.allInstances == null) {
            return;
        }
        this.allInstances.remove(instance);
    }

}


package liquibase.configuration;

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

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

/**
 * Contains the definition and current value of a given configuration property.
 */
public class ConfigurationProperty {

    private final String namespace;
    private final String name;
    private final Class type;
    private List<String> aliases = new ArrayList<>();

    private Object value;
    private String description;
    private Object defaultValue;
    private boolean wasOverridden;

    public ConfigurationProperty(String namespace, String propertyName, Class type) {
        this.namespace = namespace;
        this.name = propertyName;
        this.type = type;
    }

    /**
     * Initialize this property with values in the given ConfigurationProvers. If the configurationValueProviders do not contain
     * a default value, the property is initialized with the value set by {@link #setDefaultValue(Object)}.
     * If multiple configurationValueProviders contain values, the first in the list wins.
     */
    protected void init(ConfigurationValueProvider[] configurationValueProviders) {
        Object containerValue = null;

        for (ConfigurationValueProvider container : configurationValueProviders) {
            containerValue = container.getValue(namespace, name);
            for (String alias : aliases) {
                if (containerValue != null) {
                    break;
                }
                containerValue = container.getValue(namespace, alias);
            }
        }

        if (containerValue == null) {
            value = defaultValue;
        } else {
            try {
                value = valueOf(containerValue);
                wasOverridden = true;
            } catch (NumberFormatException e) {
                throw new UnexpectedLiquibaseException("Error parsing "+containerValue+" as a "+type.getSimpleName());
            }
        }
    }

    /**
     * Returns the property name.
     */
    public String getName() {
        return name;
    }

    /**
     * Returns the namespace used by this property's {@link ConfigurationContainer}
     */
    public String getNamespace() {
        return namespace;
    }

    /**
     * Returns the type of value stored in this property
     */
    public Class getType() {
        return type;
    }

    /**
     * Converts an object of a different type to the type used by this property. If types are not convertible, an exception is thrown.
     */
    protected Object valueOf(Object value) {
        if (value == null) {
            return value;
        } else if (type.isAssignableFrom(value.getClass())) {
            return value;
        } else if (value instanceof String) {
            if (type.equals(Boolean.class)) {
                return Boolean.valueOf((String) value);
            } else if (type.equals(Integer.class)) {
                return Integer.valueOf((String) value);
            } else if (type.equals(BigDecimal.class)) {
                return new BigDecimal((String) value);
            } else if (type.equals(Long.class)) {
            	return Long.valueOf((String) value);
            } else if (type.equals(List.class)) {
                return StringUtil.splitAndTrim((String) value, ",");
            } else {
                throw new UnexpectedLiquibaseException("Cannot parse property "+value.getClass().getSimpleName()+" to a "+type.getSimpleName());
            }
        } else {
            throw new UnexpectedLiquibaseException("Could not convert "+value.getClass().getSimpleName()+" to a "+type.getSimpleName());
        }
    }

    /**
     * Returns the value currently stored in this property without any casting.
     */
    public Object getValue() {
        return value;
    }

    /**
     * Returns the value currently stored in this property cast to the given type.
     */
    public <T> T getValue(Class<T> type) {
        if (!this.type.isAssignableFrom(type)) {
            throw new UnexpectedLiquibaseException("Property "+name+" on is of type "+this.type.getSimpleName()+", not "+type.getSimpleName());
        }

        return (T) value;
    }

    /**
     * Overwrites the value currently stored in this property. It he passed type is not compatible with the defined type, an exception is thrown.
     */
    public void setValue(Object value) {
        if ((value != null) && !type.isAssignableFrom(value.getClass())) {
            throw new UnexpectedLiquibaseException("Property "+name+" on is of type "+type.getSimpleName()+", not "+value.getClass().getSimpleName());
        }

        this.value = value;
        wasOverridden = true;
    }

    /**
     * Adds an alias for this property. An alias is an alternate to the "name" field that can be used by the ConfigurationProvers to look up starting values.
     */
    public ConfigurationProperty addAlias(String... aliases) {
        if (aliases != null) {
            this.aliases.addAll(Arrays.asList(aliases));
        }

        return this;
    }

    /**
     * Returns a human-readable definition of this property
     */
    public String getDescription() {
        return description;
    }

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

    /**
     * Returns the default value to use if no ConfigurationProviders override it.
     */
    public Object getDefaultValue() {
        return defaultValue;
    }

    /**
     * Sets the default value to use if no ConfigurationProviders override it. Throws an exception if the given object is not compatible with the defined type.
     */
    public ConfigurationProperty setDefaultValue(Object defaultValue) {
        if ((defaultValue != null) && !type.isAssignableFrom(defaultValue.getClass())) {
            if ((type == Long.class) && (defaultValue instanceof Integer)) {
                return setDefaultValue(((Integer) defaultValue).longValue());
            }
            throw new UnexpectedLiquibaseException("Property "+name+" on is of type "+type.getSimpleName()+", not "+defaultValue.getClass().getSimpleName());
        }

        this.defaultValue = defaultValue;

        return this;
    }

    /**
     * Returns true if the value has been set by a ConfigurationValueProvider or by {@link #setValue(Object)}
     */
    public boolean getWasOverridden() {
        return wasOverridden;
    }
}


package liquibase.sqlgenerator.core;

import liquibase.change.AddColumnConfig;
import liquibase.change.ColumnConfig;
import liquibase.database.Database;
import liquibase.database.core.FirebirdDatabase;
import liquibase.exception.ValidationErrors;
import liquibase.sql.Sql;
import liquibase.sql.UnparsedSql;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.statement.core.CreateIndexStatement;
import liquibase.structure.core.Index;
import liquibase.util.StringUtil;

import java.util.Arrays;
import java.util.Iterator;
import java.util.List;

/**
 * Firebird-specific CREATE INDEX SQL generator.
 */
public class CreateIndexGeneratorFirebird extends CreateIndexGenerator {

    // Just a little value object for passing a complex result of the column analysis
    private final class ColumnAnalysisResult {
        private String columnExpression;
        private boolean foundAscColumns;
        private boolean foundDescColumns;
        private int numComputedCols;
        private int numRegularCols;

        public String getColumnExpression() {
            return columnExpression;
        }

        public void setColumnExpression(String columnExpression) {
            this.columnExpression = columnExpression;
        }

        public boolean isFoundAscColumns() {
            return foundAscColumns;
        }

        public void setFoundAscColumns(boolean foundAscColumns) {
            this.foundAscColumns = foundAscColumns;
        }

        public boolean isFoundDescColumns() {
            return foundDescColumns;
        }

        public void setFoundDescColumns(boolean foundDescColumns) {
            this.foundDescColumns = foundDescColumns;
        }

        public int getNumComputedCols() {
            return numComputedCols;
        }

        public void setNumComputedCols(int numComputedCols) {
            this.numComputedCols = numComputedCols;
        }

        public int getNumRegularCols() {
            return numRegularCols;
        }

        public void setNumRegularCols(int numRegularCols) {
            this.numRegularCols = numRegularCols;
        }
    }

    /**
     * Informs the SqlGeneratorFactory that we are the preferred generator for CREATE INDEX statements in a
     * Firebird database.
     * @return The PRIORITY_DATABASE priority
     */
    @Override
    public int getPriority() {
        return PRIORITY_DATABASE;
    }

    /**
     * Informs the SqlGeneratorFactory that we this class works on Firebird SQL databases only.
     * @param statement The SqlStatement object (ignored in this case)
     * @param database The database object to be compared
     * @return true if database is a Firebird database, else in other cases.
     */
    @Override
    public boolean supports(CreateIndexStatement statement, Database database) {
        return database instanceof FirebirdDatabase;
    }

    /**
     * Generate a CREATE INDEX SQL statement for Firebird databases.
     * @param statement A CreateIndexStatement with the desired properties of the SQL to be generated
     * @param database A database object (must be of FirebirdDatabase type, or we will error out)
     * @param sqlGeneratorChain The other generators in the current chain (ignored by this implementation)
     * @return An array with one entry containing the generated CREATE INDEX statement for Firebird.
     */
    @Override
    public Sql[] generateSql(CreateIndexStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain)  {
        /*
         * According to https://firebirdsql.org/refdocs/langrefupd20-create-table.html#langrefupd20-ct-using-index ,
         * Firebird automatically creates indexes for PRIMARY KEY, UNIQUE KEY and FOREIGN KEY constraints,
         * so we should not duplicate that functionality (=we should not issue CREATE INDEX statements for them)
         */
        List<String> associatedWith = StringUtil.splitAndTrim(statement.getAssociatedWith(), ",");
        if ((associatedWith != null) && (associatedWith.contains(Index.MARK_PRIMARY_KEY) || associatedWith.contains
            (Index.MARK_UNIQUE_CONSTRAINT) || associatedWith.contains(Index.MARK_FOREIGN_KEY))) {
            return new Sql[0];
        }

        StringBuffer buffer = new StringBuffer();

        buffer.append("CREATE ");

        // If the statement wants a UNIQUE index, issue a CREATE UNIQUE ... INDEX statement.
        if ((statement.isUnique() != null) && statement.isUnique()) {
            buffer.append("UNIQUE ");
        }

        /*
         * Examine the direction (ASCending or DESCending) of all columns of the planned index. If _all_ of them
         * are DESC, issue a CREATE DESCENDING INDEX statement. If all of them are ASC, we do not need to do anything
         * special (ASCENDING is the default). However, since Firebird does not seem to support mixed ASC/DESC
         * columns within a single index, we must error out.
         *
         * The syntax we want to create is this:
         * CREATE [UNIQUE] [ASC[ENDING] | DESC[ENDING]] INDEX index_name ON table_name
          * {(col [, col …]) | COMPUTED BY (<expression>)};
         */
        ColumnAnalysisResult result = analyseColumns(statement, database);

        if (result.isFoundDescColumns())
            buffer.append("DESCENDING ");

        buffer.append("INDEX ");

        if (statement.getIndexName() != null) {
            String indexSchema = statement.getTableSchemaName();
            buffer.append(database.escapeIndexName(statement.getTableCatalogName(), indexSchema,
                    statement.getIndexName())).append(" ");
        }

        buffer.append("ON ");
        // append table name
        buffer.append(database.escapeTableName(statement.getTableCatalogName(), statement.getTableSchemaName(),
                statement.getTableName()));

        if (result.getNumComputedCols() > 0)
            buffer.append("COMPUTED BY ");

        buffer.append(String.format("(%s)", result.getColumnExpression()));

        return new Sql[]{new UnparsedSql(buffer.toString(), getAffectedIndex(statement))};
    }

    /**
     * Analyzes the column list for the statement and returns a ColumnAnalysisResult which contains:
     * - The final column list (or computed expression)
     * - How many regular columns were found
     * - How many computed expressions were found
     * - If any column had an ASCending sorting
     * - If any column had a DESCending sorting
     * @param statement the CreateIndexStatement to analyse
     * @param database the Database object (FirebirdDatabase is expected)
     * @return the result of the analysis
     */
    private ColumnAnalysisResult analyseColumns(CreateIndexStatement statement,
                                                Database database)  {
        ColumnAnalysisResult result = new ColumnAnalysisResult();

        StringBuffer idxColsBuf = new StringBuffer();
        result.foundAscColumns = false;
        result.foundDescColumns = false;
        result.numComputedCols = 0;
        result.numRegularCols = 0;

        Iterator<AddColumnConfig> iterator = Arrays.asList(statement.getColumns()).iterator();

        while (iterator.hasNext()) {
            AddColumnConfig column = iterator.next();
            boolean columnExpressionIsComputed;
            if (column.getComputed() == null) {
                /* We absolutely must know whether we have a column identifier or something else (expression index)
                 * in front of us, because the correct syntax depends on it.
                 */
                columnExpressionIsComputed = applyIsComputedExpressionHeuristic(column, database);
            } else {
                columnExpressionIsComputed = column.getComputed();
            }

            if (columnExpressionIsComputed) {
                idxColsBuf.append(column.getName()); // Use the expression as-is
                result.setNumComputedCols(result.getNumComputedCols()+1);
            } else {
                result.setNumRegularCols((result.getNumRegularCols())+1);
                idxColsBuf.append(database.escapeColumnName(statement.getTableCatalogName(), statement.getTableSchemaName(), statement.getTableName(), column.getName()));
            }

            if ((column.getDescending() != null) && column.getDescending()) {
                result.setFoundDescColumns(true);
            } else {
                result.setFoundAscColumns(true);
            }

            if (iterator.hasNext()) {
                idxColsBuf.append(", ");
            }
        }

        result.setColumnExpression(idxColsBuf.toString());
        return result;
    }

    @Override
    public ValidationErrors validate(CreateIndexStatement createIndexStatement, Database database, SqlGeneratorChain sqlGeneratorChain) {
        ValidationErrors errors = super.validate(createIndexStatement, database, sqlGeneratorChain);
        errors.checkRequiredField("name", createIndexStatement.getIndexName());

        ColumnAnalysisResult result = analyseColumns(createIndexStatement, database);

        // Error out if we cannot build the CREATE INDEX statement due to syntax problems.
        if (result.isFoundAscColumns() && result.isFoundDescColumns()) {
            errors.addError("Firebird cannot create indexes with mixed ASCending / DESCending columns.");
        }
        if ((result.getNumComputedCols() > 0) && (result.getNumRegularCols() > 0)) {
            errors.addError("Firebird cannot create indexes with both computed expressions and regular columns.");
        }
        if (result.getNumComputedCols() > 1) {
            errors.addError("Firebird cannot create indexes on more than 1 computed expression.");
        }

        return errors;
    }

    /**
         * An imperfect heuristic to determine if an expression is more likely a column name or a computed expression.
         * @return true if it is more likely an expression, false if it is more likely a column name (identifier).
         */

    private boolean applyIsComputedExpressionHeuristic(ColumnConfig column, Database database) {
        String expr = column.getName();

        /*
         * https://firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-structure-identifiers.html
         * says the following about what makes a valid identifier in Firebird:
         * - At most 31 chars
         * - Starts with a 7-bit character
         * - After that, letters, digits, underscores or dollar signs are valid characters
         */
        String regex = "^(?i)[ABCDEFGHIJKLMNOPQRSTUVWXYZ]" // Starting character
                + "[ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$]{0,30}$"; // following characters
        if (!expr.matches(regex))
            return true;

        /* At this point, we know that expr at least has the form of an identifier. If it is a function, it must
         * be in the list of database functions.
         */
        if (database.isFunction(expr))
            return true;
        else
            return false;
    }
}


package liquibase.util;

import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;

import java.io.*;
import java.nio.charset.Charset;

/**
 * Utilities for working with streams.
 */
public class StreamUtil {

    public static String getLineSeparator() {
        return LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputLineSeparator();
    }

    public static void copy(InputStream inputStream, OutputStream outputStream) throws IOException {
        byte[] bytes = new byte[1024];
        int r = inputStream.read(bytes);
        while (r > 0) {
            outputStream.write(bytes, 0, r);
            r = inputStream.read(bytes);
        }
    }

    public static byte[] readStream(InputStream stream) throws IOException {
        try (ByteArrayOutputStream buffer = new ByteArrayOutputStream()) {

            copy(stream, buffer);
            buffer.flush();

            return buffer.toByteArray();
        }
    }

    /**
     * Calls {@link #readStreamAsString(InputStream, String)} with {@link Scope#getFileEncoding()} as the encoding
     */
    public static String readStreamAsString(InputStream stream) throws IOException {
        return readStreamAsString(stream, null);
    }

    /**
     * Returns the given stream as a string using the given encoding.
     * If encoding is null, use {@link Scope#getFileEncoding()}
     */
    public static String readStreamAsString(InputStream stream, String encoding) throws IOException {
        StringBuilder result = new StringBuilder();

        try (Reader reader = readStreamWithReader(stream, encoding)) {

            char[] buffer = new char[2048];
            int read;
            while ((read = reader.read(buffer)) > -1) {
                result.append(buffer, 0, read);
            }
            return result.toString();
        }
    }

    public static Reader readStreamWithReader(InputStream stream, String encoding) throws IOException {
        BomAwareInputStream encodingAwareStream = new BomAwareInputStream(stream);
        Charset detectedEncoding = encodingAwareStream.getDetectedCharset();

        if (encoding == null) {
            if (detectedEncoding != null) {
                encoding = detectedEncoding.name();
            }
        } else {
            String canonicalEncodingName = Charset.forName(encoding).name();

            if (detectedEncoding != null && canonicalEncodingName.startsWith("UTF") && !canonicalEncodingName.equals(detectedEncoding.name())) {
                throw new IllegalArgumentException("Expected encoding was '"
                        + encoding + "' but a BOM was detected for '"
                        + detectedEncoding + "'");
            }
        }

        return new InputStreamReader(encodingAwareStream, ObjectUtil.defaultIfNull(encoding, Scope.getCurrentScope().getFileEncoding().toString()));
    }

}


package liquibase.serializer.core.yaml;

import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.serializer.LiquibaseSerializable;
import liquibase.serializer.SnapshotSerializer;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.statement.DatabaseFunction;
import liquibase.statement.SequenceCurrentValueFunction;
import liquibase.statement.SequenceNextValueFunction;
import liquibase.structure.DatabaseObject;
import liquibase.structure.DatabaseObjectCollection;
import liquibase.structure.DatabaseObjectComparator;
import liquibase.util.ISODateFormat;
import liquibase.util.StringUtil;
import org.yaml.snakeyaml.nodes.Node;
import org.yaml.snakeyaml.nodes.Tag;
import org.yaml.snakeyaml.representer.Represent;

import java.io.BufferedWriter;
import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.math.BigInteger;
import java.util.*;

public class YamlSnapshotSerializer extends YamlSerializer implements SnapshotSerializer {

    private boolean alreadySerializingObject;

    @Override
    public void write(DatabaseSnapshot snapshot, OutputStream out) throws IOException {
        BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()));
        writer.write(serialize(snapshot, true));
    }

//    @Override
//    public String serialize(LiquibaseSerializable object, boolean pretty) {
//        if (object instanceof DatabaseObject) {
//            if (alreadySerializingObject) {
//                return ((DatabaseObject) object).getObjectTypeName()+"#"+((DatabaseObject) object).getSnapshotId();
//            } else {
//                alreadySerializingObject = true;
//                String string = super.serialize(object, pretty);
//                alreadySerializingObject = false;
//                return string;
//            }
//        }
//        return super.serialize(object, pretty);
//    }

    @Override
    protected Object toMap(final LiquibaseSerializable object) {
        if (object instanceof DatabaseObject) {
            if (alreadySerializingObject) {
                String snapshotId = ((DatabaseObject) object).getSnapshotId();
                if (snapshotId == null) {
                    String name = ((DatabaseObject) object).getName();
                    Object table = ((DatabaseObject) object).getAttribute("table", Object.class);
                    if (table == null) {
                        table = ((DatabaseObject) object).getAttribute("relation", Object.class);
                    }
                    if (table != null) {
                        name = table.toString() + "." + name;
                    }

                    if (((DatabaseObject) object).getSchema() != null) {
                        name = ((DatabaseObject) object).getSchema().toString() + "." + name;
                    }

                    throw new UnexpectedLiquibaseException("Found a null snapshotId for " + StringUtil.lowerCaseFirst(object.getClass().getSimpleName()) + " " + name);
                }
                return ((DatabaseObject) object).getClass().getName() + "#" + snapshotId;
            } else {
                alreadySerializingObject = true;
                Object map = super.toMap(object);
                alreadySerializingObject = false;
                return map;
            }
        }
        if (object instanceof DatabaseObjectCollection) {
            SortedMap<String, Object> returnMap = new TreeMap<>();
            for (Map.Entry<Class<? extends DatabaseObject>, Set<? extends DatabaseObject>> entry : ((DatabaseObjectCollection) object).toMap().entrySet()) {
                ArrayList value = new ArrayList(entry.getValue());
                Collections.sort(value, new DatabaseObjectComparator());
                returnMap.put(entry.getKey().getName(), value);
            }
            return returnMap;
        }
        return super.toMap(object);
    }

    protected LiquibaseRepresenter getLiquibaseRepresenter() {
        return new SnapshotLiquibaseRepresenter();
    }

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

    public static class SnapshotLiquibaseRepresenter extends LiquibaseRepresenter {

        protected void init() {
            multiRepresenters.put(DatabaseFunction.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(SequenceNextValueFunction.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(SequenceCurrentValueFunction.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(java.util.Date.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(java.sql.Date.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(Integer.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(BigInteger.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(Number.class, new TypeStoringAsStringRepresenter());
            multiRepresenters.put(Enum.class, new TypeStoringAsStringRepresenter());
        }

        private class TypeStoringAsStringRepresenter implements Represent {
            @Override
            public Node representData(Object data) {
                String value;
                if (data instanceof Date) {
                    value = new ISODateFormat().format((Date) data);
                } else if (data instanceof Enum) {
                    value = ((Enum) data).name();
                } else {
                    value = data.toString();
                }

                return representScalar(Tag.STR, value + "!{" + data.getClass().getName() + "}");
            }
        }
    }

}


package liquibase.exception;

import liquibase.changelog.ChangeSet;
import liquibase.database.Database;
import liquibase.util.StringUtil;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public class ValidationErrors {

    protected List<String> errorMessages = new ArrayList<>();
    protected List<String> warningMessages = new ArrayList<>();

    public boolean hasErrors() {
        return !errorMessages.isEmpty();
    }

    public void checkRequiredField(String requiredFieldName, Object value) {
        if (value == null) {
            addError(requiredFieldName + " is required");
        } else if ((value instanceof Collection) && ((Collection) value).isEmpty()) {
            addError(requiredFieldName + " is empty");
        } else if ((value instanceof Object[]) && (((Object[]) value).length == 0)) {
            addError(requiredFieldName + " is empty");
        }
    }

    /**
     *
Checks if a field is forbidden in combination with a given Database (most often because that database
     * does not implement the features required by the field/value combination). If a "forbidden" use is detected,
     * a validation error is added to the current list of ValidationErrors.

     * Note:
     *

     *
if value==null, the field is ALLOWED for all DBs

     *
if the disallowedDatabases list does not at least contain 1 entry, the field is NOT allowed

     *

     *
     * @param disallowedFieldName field whose value is checked
     * @param value               value that might be disallowed
     * @param database            database the object/value combination is checked against
     * @param disallowedDatabases a list of "forbidden" databases that do not allow this field/value combination
     */
    @SafeVarargs
    public final void checkDisallowedField(String disallowedFieldName, Object value, Database database,
                                           Class<? extends Database>... disallowedDatabases) {
        boolean isDisallowed = false;
        if ((disallowedDatabases == null) || (disallowedDatabases.length == 0)) {
            isDisallowed = true;
        } else {
            for (Class<? extends Database> databaseClass : disallowedDatabases) {
                if (databaseClass.isAssignableFrom(database.getClass())) {
                    isDisallowed = true;
                }
            }
        }

        if (isDisallowed && (value != null)) {
            addError(disallowedFieldName + " is not allowed on "+(database == null?"unknown":database.getShortName()));
        }
    }

    public ValidationErrors addError(String message) {
        errorMessages.add(message);
        return this;
    }

    public List<String> getErrorMessages() {
        return errorMessages;
    }

    public ValidationErrors addWarning(String message) {
        warningMessages.add(message);
        return this;
    }

    public List<String> getWarningMessages() {
        return warningMessages;
    }

    public ValidationErrors addAll(ValidationErrors validationErrors) {
        if (validationErrors == null) {
            return this;
        }
        this.errorMessages.addAll(validationErrors.getErrorMessages());
        return this;
    }

    public void addAll(ValidationErrors validationErrors, ChangeSet changeSet) {
        for (String message : validationErrors.getErrorMessages()) {
            this.errorMessages.add(message+", "+changeSet);
        }
        for (String message : validationErrors.getWarningMessages()) {
            this.warningMessages.add(message+", "+changeSet);
        }
    }

    @Override
    public String toString() {
        if (getErrorMessages().isEmpty()) {
            return "No errors";
        }
        return StringUtil.join(getErrorMessages(), "; ");
    }

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

    @Override
    public boolean equals(Object obj) {
        if ((obj == null) || !(obj instanceof ValidationErrors)) {
            return false;
        }
        return this.toString().equals(obj.toString());
    }

    public List<String> getRequiredErrorMessages() {
        List<String> requiredErrorMessages = new ArrayList<>();
        for (String message : errorMessages) {
            if (message.contains("is required")) {
                requiredErrorMessages.add(message);
            }
        }
        return Collections.unmodifiableList(requiredErrorMessages);
    }

    public List<String> getUnsupportedErrorMessages() {
        List<String> unsupportedErrorMessages = new ArrayList<>();
        for (String message : errorMessages) {
            if (message.contains(" is not allowed on ")) {
                unsupportedErrorMessages.add(message);
            }
        }
        return Collections.unmodifiableList(unsupportedErrorMessages);
    }
}