package liquibase.diff.output.report;

import liquibase.database.Database;
import liquibase.diff.DiffResult;
import liquibase.diff.Difference;
import liquibase.diff.ObjectDifferences;
import liquibase.diff.StringDiff;
import liquibase.diff.compare.CompareControl;
import liquibase.exception.DatabaseException;
import liquibase.structure.DatabaseObject;
import liquibase.structure.DatabaseObjectComparator;
import liquibase.structure.core.Schema;
import liquibase.util.StringUtil;

import java.io.PrintStream;
import java.util.*;

public class DiffToReport {

    private DiffResult diffResult;
    private PrintStream out;

    public DiffToReport(DiffResult diffResult, PrintStream out) {
        this.diffResult = diffResult;
        this.out = out;
    }

    public void print() throws DatabaseException {
        final DatabaseObjectComparator comparator = new DatabaseObjectComparator();
        out.println("Reference Database: " + diffResult.getReferenceSnapshot().getDatabase());
        out.println("Comparison Database: " + diffResult.getComparisonSnapshot().getDatabase());

        CompareControl.SchemaComparison[] schemas = diffResult.getCompareControl().getSchemaComparisons();
        if ((schemas != null) && (schemas.length > 0)) {
            out.println("Compared Schemas: " + StringUtil.join(Arrays.asList(schemas), ", ", new StringUtil.StringUtilFormatter<CompareControl.SchemaComparison>() {
                @Override
                public String toString(CompareControl.SchemaComparison obj) {
                    String referenceName;
                    String comparisonName;

                    Database referenceDatabase = diffResult.getReferenceSnapshot().getDatabase();
                    Database comparisonDatabase = diffResult.getComparisonSnapshot().getDatabase();

                    if (referenceDatabase.supportsSchemas()) {
                        referenceName = obj.getReferenceSchema().getSchemaName();
                        if (referenceName == null) {
                            referenceName = referenceDatabase.getDefaultSchemaName();
                        }
                    } else if (referenceDatabase.supportsCatalogs()) {
                        referenceName = obj.getReferenceSchema().getCatalogName();
                        if (referenceName == null) {
                            referenceName = referenceDatabase.getDefaultCatalogName();
                        }
                    } else {
                        return "";
                    }

                    if (comparisonDatabase.supportsSchemas()) {
                        comparisonName = obj.getComparisonSchema().getSchemaName();
                        if (comparisonName == null) {
                            comparisonName = comparisonDatabase.getDefaultSchemaName();
                        }
                    } else if (comparisonDatabase.supportsCatalogs()) {
                        comparisonName = obj.getComparisonSchema().getCatalogName();
                        if (comparisonName == null) {
                            comparisonName = comparisonDatabase.getDefaultCatalogName();
                        }
                    } else {
                        return "";
                    }

                    if (referenceName == null) {
                        referenceName = StringUtil.trimToEmpty(referenceDatabase.getDefaultSchemaName());
                    }

                    if (comparisonName == null) {
                        comparisonName = StringUtil.trimToEmpty(comparisonDatabase.getDefaultSchemaName());
                    }

                    if (referenceName.equalsIgnoreCase(comparisonName)) {
                        return referenceName;
                    } else {
                        return referenceName + " -> " + comparisonName;
                    }
                }
            }, true));
        }

        printComparison("Product Name", diffResult.getProductNameDiff(), out);
        printComparison("Product Version", diffResult.getProductVersionDiff(), out);

        TreeSet<Class<? extends DatabaseObject>> types = new TreeSet<>(new Comparator<Class<? extends DatabaseObject>>() {
            @Override
            public int compare(Class<? extends DatabaseObject> o1, Class<? extends DatabaseObject> o2) {
                return o1.getSimpleName().compareTo(o2.getSimpleName());
            }
        });
        types.addAll(diffResult.getCompareControl().getComparedTypes());
        for (Class<? extends DatabaseObject> type : types) {
            if (type.equals(Schema.class) && !diffResult.getComparisonSnapshot().getDatabase().supportsSchemas()) {
                continue;
            }
            printSetComparison("Missing " + getTypeName(type), diffResult.getMissingObjects(type, comparator), out);
            printSetComparison("Unexpected " + getTypeName(type), diffResult.getUnexpectedObjects(type, comparator), out);

            printChangedComparison("Changed " + getTypeName(type), diffResult.getChangedObjects(type, comparator), out);

        }

//        printColumnComparison(diffResult.getColumns().getChanged(), out);
    }

    protected String getTypeName(Class<? extends DatabaseObject> type) {
        return type.getSimpleName().replaceAll("([A-Z])", " $1").trim() + "(s)";
    }

    protected boolean getIncludeSchema() {
        return diffResult.getCompareControl().getSchemaComparisons().length > 1;
    }

    protected void printChangedComparison(String title, Map<? extends DatabaseObject, ObjectDifferences> objects, PrintStream out) {
        out.print(title + ": ");
        if (objects.isEmpty()) {
            out.println("NONE");
        } else {
            out.println();
            for (Map.Entry<? extends DatabaseObject, ObjectDifferences> object : objects.entrySet()) {
                if (object.getValue().hasDifferences()) {
                    out.println("     " + object.getKey());
                    for (Difference difference : object.getValue().getDifferences()) {
                        out.println("          " + difference.toString());
                    }
                }
            }
        }
    }

    protected void printSetComparison(String title, Set<? extends DatabaseObject> objects, PrintStream out) {
        out.print(title + ": ");
        Schema lastSchema = null;
        if (objects.isEmpty()) {
            out.println("NONE");
        } else {
            out.println();
            for (DatabaseObject object : objects) {
                if (getIncludeSchema() && (object.getSchema() != null) && ((lastSchema == null) || !lastSchema.equals
                    (object.getSchema()))) {
                    lastSchema = object.getSchema();
                    String schemaName = object.getSchema().getName();
                    if (schemaName == null) {
                        schemaName = object.getSchema().getCatalogName();
                    }
                    schemaName = includeSchemaComparison(schemaName);

                    out.println("  SCHEMA: " + schemaName);
                }
                out.println("     " + object);
            }
        }
    }

    protected String includeSchemaComparison(String schemaName) {
        String convertedSchemaName = CompareControl.SchemaComparison.convertSchema(schemaName, diffResult.getCompareControl().getSchemaComparisons());

        if ((convertedSchemaName != null) && !convertedSchemaName.equals(schemaName)) {
            schemaName = schemaName + " -> " + convertedSchemaName;
        }
        return schemaName;
    }

//    private void printColumnComparison(SortedSet<Column> changedColumns,
//                                       PrintStream out) {
//        out.print("Changed Columns: ");
//        if (changedColumns.size() == 0) {
//            out.println("NONE");
//        } else {
//            out.println();
//            for (Column column : changedColumns) {
//                out.println("     " + column);
//                Column baseColumn = diffResult.getReferenceSnapshot().getColumn(column.getRelation().getSchema(),
//                        column.getRelation().getName(), column.getName());
//                if (baseColumn != null) {
//                    if (baseColumn.isDataTypeDifferent(column)) {
//                        out.println("           from "
//                                + baseColumn.getType()
//                                + " to "
//                                + diffResult.getComparisonSnapshot().getColumn(column.getRelation().getSchema(), column.getRelation().getName(), column.getName()).getType());
//                    }
//                    if (baseColumn.isNullabilityDifferent(column)) {
//                        Boolean nowNullable = diffResult.getComparisonSnapshot().getColumn(column.getRelation().getSchema(),
//                                column.getRelation().getName(), column.getName())
//                                .isNullable();
//                        if (nowNullable == null) {
//                            nowNullable = Boolean.TRUE;
//                        }
//                        if (nowNullable) {
//                            out.println("           now nullable");
//                        } else {
//                            out.println("           now not null");
//                        }
//                    }
//                }
//            }
//        }
//    }

    protected void printComparison(String title, StringDiff string, PrintStream out) {
        out.print(title + ":");

        if (string == null) {
            out.print("NULL");
            return;
        }

        if (string.areEqual()) {
            out.println(" EQUAL");
        } else {
            String referenceVersion = string.getReferenceVersion();
            if (referenceVersion == null) {
                referenceVersion = "NULL";
            } else {
                referenceVersion = "'" + referenceVersion + "'";
            }

            String targetVersion = string.getTargetVersion();
            if (targetVersion == null) {
                targetVersion = "NULL";
            } else {
                targetVersion = "'" + targetVersion + "'";
            }

            out.println();
            out.println("     Reference:   "
                    + referenceVersion);
            out.println("     Target: " + targetVersion);
        }

    }

}


package liquibase.logging.core;

import liquibase.configuration.AbstractConfigurationContainer;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogLevel;

/**
 * Configuration container for {@link liquibase.logging.LogService} properties
 */
public class DefaultLoggerConfiguration extends AbstractConfigurationContainer {

    public static final String LOG_LEVEL = "level";

    public DefaultLoggerConfiguration() {
        super("liquibase.defaultlogger");

        getContainer().addProperty(LOG_LEVEL, String.class)
                .setDescription("Logging level")
                .setDefaultValue("INFO");
    }

    public String getLogLevelName() {
        return getContainer().getValue(LOG_LEVEL, String.class);
    }

    /**
     * Transforms the strings DEBUG, INFO, WARNING, ERROR and OFF (case-insensitive) into the appropriate LogLevel.
     * @return a value from the LogLevel enum
     */
    public LogLevel getLogLevel() {
        String logLevel = getLogLevelName();

        if ("debug".equalsIgnoreCase(logLevel)) {
            return LogLevel.DEBUG;
        } else if ("info".equalsIgnoreCase(logLevel)) {
            return LogLevel.INFO;
        } else if ("warning".equalsIgnoreCase(logLevel)) {
            return LogLevel.WARNING;
        } else if ("error".equalsIgnoreCase(logLevel) || "severe".equalsIgnoreCase(logLevel)) {
            return LogLevel.SEVERE;
        } else if ("off".equalsIgnoreCase(logLevel)) {
            return LogLevel.OFF;
        } else {
            throw new UnexpectedLiquibaseException("Unknown log level: " + logLevel+".  Valid levels are: debug, info, warning, error, off");
        }
    }
    public DefaultLoggerConfiguration setLogLevel(String name) {
        getContainer().setValue(LOG_LEVEL, name);
        return this;
    }
}


package liquibase.util;

import liquibase.exception.UnexpectedLiquibaseException;

/**
 * Similar to assert logic, but will run regardless of JVM settings. Throws {@link liquibase.exception.UnexpectedLiquibaseException} if assertions fail.
 * Based off org.apache.commons.lang.Validate.
 */
public class Validate {

    /**
     * Throws exception if passed object is null
     */
    public static void notNull(Object object, String failMessage) throws UnexpectedLiquibaseException {
        if (object == null) {
            fail(failMessage);
        }
    }

    /**
     * Throws exception if test is not true.
     */
    public static void isTrue(boolean test, String failMessage) throws UnexpectedLiquibaseException{
        if (!test) {
            fail(failMessage);
        }

    }

    public static void fail(String failMessage) {
        throw new UnexpectedLiquibaseException(failMessage);
    }

    public static UnexpectedLiquibaseException failure(String failMessage) {
        throw new UnexpectedLiquibaseException(failMessage);
    }
}


package liquibase.database.core;

import liquibase.CatalogAndSchema;
import liquibase.Scope;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.DatabaseConnection;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.exception.DatabaseException;
import liquibase.executor.Executor;
import liquibase.executor.ExecutorService;
import liquibase.logging.LogService;
import liquibase.logging.LogType;
import liquibase.statement.SqlStatement;
import liquibase.statement.core.GetViewDefinitionStatement;
import liquibase.statement.core.RawSqlStatement;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Table;
import liquibase.structure.core.View;

import java.math.BigInteger;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * Encapsulates Sybase ASE database support.
 */
public class SybaseDatabase extends AbstractJdbcDatabase {
    public static final String PRODUCT_NAME = "Adaptive Server Enterprise";
    protected Set<String> systemTablesAndViews = new HashSet<>();

    public SybaseDatabase() {
        super.setCurrentDateTimeFunction("GETDATE()");
        systemTablesAndViews.add("syscolumns");
        systemTablesAndViews.add("syscomments");
        systemTablesAndViews.add("sysdepends");
        systemTablesAndViews.add("sysfilegroups");
        systemTablesAndViews.add("sysfiles");
        systemTablesAndViews.add("sysfiles1");
        systemTablesAndViews.add("sysforeignkeys");
        systemTablesAndViews.add("sysfulltextcatalogs");
        systemTablesAndViews.add("sysfulltextnotify");
        systemTablesAndViews.add("sysindexes");
        systemTablesAndViews.add("sysindexkeys");
        systemTablesAndViews.add("sysmembers");
        systemTablesAndViews.add("sysobjects");
        systemTablesAndViews.add("syspermissions");
        systemTablesAndViews.add("sysproperties");
        systemTablesAndViews.add("sysprotects");
        systemTablesAndViews.add("sysreferences");
        systemTablesAndViews.add("systypes");
        systemTablesAndViews.add("sysusers");
        systemTablesAndViews.add("sysquerymetrics");
        systemTablesAndViews.add("syssegments");
        systemTablesAndViews.add("sysconstraints");
    }

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

/*    public void setConnection(Connection connection) {
        super.setConnection(new SybaseConnectionDelegate(connection));
    }
    */

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

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

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

    /**
     * Sybase does not support DDL and meta data in transactions properly,
     * as such we turn off the commit and turn on auto commit.
     */
    @Override
    public boolean supportsDDLInTransaction() {
    	return false;
    }

    @Override
    public Set<String> getSystemViews() {
        return systemTablesAndViews;
    }

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

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

    @Override
    public boolean isCorrectDatabaseImplementation(DatabaseConnection conn) throws DatabaseException {
        String dbProductName = conn.getDatabaseProductName();
        return isSybaseProductName(dbProductName);
    }

    // package private to facilitate testing
    boolean isSybaseProductName(String dbProductName) {
        return
                PRODUCT_NAME.equals(dbProductName)
                || "Sybase SQL Server".equals(dbProductName)
                || "sql server".equals(dbProductName)
                || "ASE".equals(dbProductName);
    }

    @Override
    public String getDefaultDriver(String url) {
        if (url.startsWith("jdbc:xsybase")) {
            return "com.sybase.jdbc4.jdbc.SybDriver";
        } else if (url.startsWith("jdbc:jtds:sybase")) {
            return "net.sourceforge.jtds.jdbc.Driver";
        }
        return null;
    }

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

	@Override
	protected boolean generateAutoIncrementStartWith(BigInteger startWith) {
		// not supported
		return false;
	}

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

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

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

//    protected void dropForeignKeys(Connection conn) throws JDBCException {
//        Statement dropStatement = null;
//        PreparedStatement fkStatement = null;
//        ResultSet rs = null;
//        try {
//            dropStatement = conn.createStatement();
//
//            fkStatement = conn.prepareStatement("select TABLE_NAME, CONSTRAINT_NAME from INFORMATION_SCHEMA.TABLE_CONSTRAINTS where CONSTRAINT_TYPE='FOREIGN KEY' AND TABLE_CATALOG=?");
//            fkStatement.setString(1, getDefaultCatalogName());
//            rs = fkStatement.executeQuery();
//            while (rs.next()) {
//                DropForeignKeyConstraintChange dropFK = new DropForeignKeyConstraintChange();
//                dropFK.setBaseTableName(rs.getString("TABLE_NAME"));
//                dropFK.setConstraintName(rs.getString("CONSTRAINT_NAME"));
//
//                try {
//                    dropStatement.execute(dropFK.generateStatements(this)[0]);
//                } catch (UnsupportedChangeException e) {
//                    throw new JDBCException(e.getMessage());
//                }
//            }
//        } catch (SQLException e) {
//            throw new JDBCException(e);
//        } finally {
//            try {
//                if (dropStatement != null) {
//                    dropStatement.close();
//                }
//                if (fkStatement != null) {
//                    fkStatement.close();
//                }
//                if (rs != null) {
//                    rs.close();
//                }
//            } catch (SQLException e) {
//                throw new JDBCException(e);
//            }
//        }
//
//    }

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

    @Override
    public boolean isSystemObject(DatabaseObject example) {
        if ((example.getSchema() != null) && (example.getSchema().getName() != null)) {
            if ((example instanceof Table) && ("sys".equals(example.getSchema().getName()) || "sybfi".equals(example
                .getSchema().getName()))) {
                return true;
            }
            if ((example instanceof View) && ("sys".equals(example.getSchema().getName()) || "sybfi".equals(example
                .getSchema().getName()))) {
                return true;
            }
        }
        return super.isSystemObject(example);
    }

    public String generateDefaultConstraintName(String tableName, String columnName) {
        return "DF_" + tableName + "_" + columnName;
    }

    @Override
    protected SqlStatement getConnectionSchemaNameCallStatement() {
        return new RawSqlStatement("select user_name()");
    }

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

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

	@Override
	public String getViewDefinition(CatalogAndSchema schema, String viewName) throws DatabaseException {
        schema = schema.customize(this);
        GetViewDefinitionStatement statement = new GetViewDefinitionStatement(schema.getCatalogName(), schema.getSchemaName(), viewName);
        Executor executor = ExecutorService.getInstance().getExecutor(this);
        @SuppressWarnings("unchecked")
        List<String> definitionRows = (List<String>) executor.queryForList(statement, String.class);
        StringBuilder definition = new StringBuilder();
        for (String d : definitionRows) {
        	definition.append(d);
        }
        return definition.toString();
	}

	/**
	 * @return the major version if supported, otherwise -1
	 * @see liquibase.database.AbstractJdbcDatabase#getDatabaseMajorVersion()
	 */
	@Override
    public int getDatabaseMajorVersion() throws DatabaseException {
        if (getConnection() == null) {
            return -1;
        }
        try {
            return getConnection().getDatabaseMajorVersion();
        } catch (UnsupportedOperationException e) {
        	Scope.getCurrentScope().getLog(getClass())
        		.warning(LogType.LOG, "Your JDBC driver does not support getDatabaseMajorVersion(). Consider upgrading it.");
            return -1;
        }
    }

	/**
	 * @return the minor version if supported, otherwise -1
	 * @see liquibase.database.AbstractJdbcDatabase#getDatabaseMinorVersion()
	 */
	@Override
    public int getDatabaseMinorVersion() throws DatabaseException {
        if (getConnection() == null) {
            return -1;
        }

        try {
            return getConnection().getDatabaseMinorVersion();
        } catch (UnsupportedOperationException e) {
        	Scope.getCurrentScope().getLog(getClass())
    			.warning(LogType.LOG, "Your JDBC driver does not support getDatabaseMajorVersion(). Consider upgrading it.");
            return -1;
        }
    }

    @Override
    public String escapeIndexName(String catalogName,String schemaName, String indexName) {
        return indexName;
    }

    @Override
    public String quoteObject(String objectName, Class<? extends DatabaseObject> objectType) {
        if (objectName == null) {
            return null;
        }
        return getQuotingStartCharacter() + objectName + getQuotingEndCharacter();
    }

    @Override
    protected boolean mustQuoteObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
        if (objectName.contains("(")) { //probably a function
            return false;
        }
        return super.mustQuoteObjectName(objectName, objectType);
    }

    @Override
    protected String getQuotingStartCharacter() {
        return "[";
    }

    @Override
    protected String getQuotingEndCharacter() {
        return "]";
    }

    @Override
    public boolean requiresExplicitNullForColumns() {
        /* SAP Adaptive Server Enterprise and, by extension, SQL Anywhere in ASE compatiblity mode have the
         * strange requirement of setting the nullability of a column to NOT NULL if neither NULL nor
         * NOT NULL are specified. See:
         * http://dcx.sap.com/index.html#sqla170/en/html/819378356ce21014a17f8d51529119ee.html
         */
        return true;
    }
}


package liquibase.sql.visitor;

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

public class SqlVisitorFactory {

    @SuppressWarnings("unchecked")
	private final Map<String, Class> tagToClassMap;

    private static final SqlVisitorFactory instance = new SqlVisitorFactory();

    @SuppressWarnings("unchecked")
	private SqlVisitorFactory() {
        tagToClassMap = new HashMap<>();
        Class[] visitors = new Class[]{
                PrependSqlVisitor.class,
                AppendSqlVisitor.class,
                RegExpReplaceSqlVisitor.class,
                ReplaceSqlVisitor.class,
        };

        try {
            for (Class<SqlVisitor> visitorClass : visitors) {
                SqlVisitor visitor = visitorClass.getConstructor().newInstance();
                tagToClassMap.put(visitor.getName(), visitorClass);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static SqlVisitorFactory getInstance() {
        return instance;
    }

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

}


package liquibase.util;

import liquibase.Scope;
import liquibase.logging.LogType;

import java.net.InetAddress;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.net.UnknownHostException;
import java.util.Enumeration;

public class NetUtil {

    /**
     * Smarter way to get localhost than InetAddress.getLocalHost.  See http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4665037
     */
    private static InetAddress getLocalHost() throws UnknownHostException, SocketException {
        // Windows Vista returns the IPv6 InetAddress using this method, which is not
        // particularly useful as it has no name or useful address, just "0:0:0:0:0:0:0:1".
        // That is why windows should be treated differently to linux/unix and use the
        // default way of getting the localhost.
        String osName = System.getProperty("os.name");
        if ((osName != null) && osName.toLowerCase().contains("windows")) {
            return InetAddress.getLocalHost();
        }

        InetAddress loopback = null;
        Enumeration<NetworkInterface> e = NetworkInterface.getNetworkInterfaces();
        while (e.hasMoreElements()) {
            NetworkInterface i = e.nextElement();
            if (i.isUp() && !i.isPointToPoint()) {
                Enumeration<InetAddress> ie = i.getInetAddresses();
                while (ie.hasMoreElements()) {
                    InetAddress lch = ie.nextElement();
                    if (lch.isLoopbackAddress()) {
                        loopback = lch;
                    } else if (!lch.isLinkLocalAddress()) {
                        return lch;
                    }
                }
            }
        }
        return loopback;
    }

    /**
     * Returns Local Host IP Address
     * @return local Host IP address
     * @throws UnknownHostException
     * @throws SocketException
     */
    public static String getLocalHostAddress() throws UnknownHostException, SocketException {
        try {
            return getLocalHost().getHostAddress();
        } catch (Exception e) {
            Scope.getCurrentScope().getLog(NetUtil.class).fine(LogType.LOG, "Error getting hostname", e);
            return "unknown";
        }
    }

    /**
     * Returns Local Host Name
     * @return local Host Name
     * @throws UnknownHostException
     * @throws SocketException
     */
    public static String getLocalHostName() throws UnknownHostException, SocketException {
        try {
            return getLocalHost().getHostName();
        } catch (Exception e) {
            Scope.getCurrentScope().getLog(NetUtil.class).fine(LogType.LOG, "Error getting hostname", e);
            return "unknown";
        }
    }

}


package liquibase.sqlgenerator.core;

import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.database.Database;
import liquibase.database.core.AbstractDb2Database;
import liquibase.database.core.Db2zDatabase;
import liquibase.database.core.MSSQLDatabase;
import liquibase.database.core.OracleDatabase;
import liquibase.exception.ValidationErrors;
import liquibase.parser.ChangeLogParserCofiguration;
import liquibase.sql.Sql;
import liquibase.sql.UnparsedSql;
import liquibase.sqlgenerator.SqlGeneratorChain;
import liquibase.statement.core.CreateProcedureStatement;
import liquibase.structure.core.Schema;
import liquibase.structure.core.StoredProcedure;
import liquibase.util.SqlParser;
import liquibase.util.StringClauses;
import liquibase.util.StringUtil;

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

public class CreateProcedureGenerator extends AbstractSqlGenerator<CreateProcedureStatement> {
    @Override
    public ValidationErrors validate(CreateProcedureStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
        ValidationErrors validationErrors = new ValidationErrors();
        validationErrors.checkRequiredField("procedureText", statement.getProcedureText());
        if (statement.getReplaceIfExists() != null) {
            if (database instanceof MSSQLDatabase) {
                if (statement.getReplaceIfExists() && (statement.getProcedureName() == null)) {
                    validationErrors.addError("procedureName is required if replaceIfExists = true");
                }
            } else {
                validationErrors.checkDisallowedField("replaceIfExists", statement.getReplaceIfExists(), null);
            }

        }
        return validationErrors;
    }

    @Override
    public Sql[] generateSql(CreateProcedureStatement statement, Database database, SqlGeneratorChain sqlGeneratorChain) {
        List<Sql> sql = new ArrayList<>();

        String schemaName = statement.getSchemaName();
        if ((schemaName == null) && LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class)
            .getAlwaysOverrideStoredLogicSchema()) {
            schemaName = database.getDefaultSchemaName();
        }

        String procedureText = addSchemaToText(statement.getProcedureText(), schemaName, "PROCEDURE", database);

        if ((statement.getReplaceIfExists() != null) && statement.getReplaceIfExists()) {
            String fullyQualifiedName = database.escapeObjectName(statement.getProcedureName(), StoredProcedure.class);
            if (schemaName != null) {
                fullyQualifiedName = database.escapeObjectName(schemaName, Schema.class) + "." + fullyQualifiedName;
            }
            sql.add(new UnparsedSql("if object_id('" + fullyQualifiedName + "', 'p') is null exec ('create procedure " + fullyQualifiedName + " as select 1 a')"));

            StringClauses parsedSql = SqlParser.parse(procedureText, true, true);
            StringClauses.ClauseIterator clauseIterator = parsedSql.getClauseIterator();
            Object next = "START";
            while ((next != null) && !("create".equalsIgnoreCase(next.toString()) || "alter".equalsIgnoreCase(next
                .toString())) && clauseIterator.hasNext()) {
                next = clauseIterator.nextNonWhitespace();
            }
            clauseIterator.replace("ALTER");

            procedureText = parsedSql.toString();
        }

        procedureText = removeTrailingDelimiter(procedureText, statement.getEndDelimiter());

        if ((database instanceof MSSQLDatabase) && procedureText.toLowerCase().contains("merge") && !procedureText
            .endsWith(";")) { //mssql "AS MERGE" procedures need a trailing ; (regardless of the end delimiter)
            StringClauses parsed = SqlParser.parse(procedureText);
            StringClauses.ClauseIterator clauseIterator = parsed.getClauseIterator();
            boolean reallyMerge = false;
            while (clauseIterator.hasNext()) {
                Object clause = clauseIterator.nextNonWhitespace();
                if ("merge".equalsIgnoreCase((String) clause)) {
                    reallyMerge = true;
                }
            }
            if (reallyMerge) {
                procedureText = procedureText + ";";
            }
        }
        if (database instanceof Db2zDatabase & procedureText.toLowerCase().contains("replace")) {
            procedureText = procedureText.replace("OR REPLACE", "");
            procedureText = procedureText.replaceAll("[\\s]{2,}", " ");
        }
        sql.add(new UnparsedSql(procedureText, statement.getEndDelimiter()));
        surroundWithSchemaSets(sql, statement.getSchemaName(), database);
        return sql.toArray(new Sql[sql.size()]);
    }

    public static String removeTrailingDelimiter(String procedureText, String endDelimiter) {
        if (procedureText == null) {
            return null;
        }
        if (endDelimiter == null) {
            return procedureText;
        }

        endDelimiter = endDelimiter.replace("\\r", "\r").replace("\\n", "\n");
        // DVONE-5036
        String endCommentsTrimmedText = StringUtil.stripSqlCommentsAndWhitespacesFromTheEnd(procedureText);
        // Note: need to trim the delimiter since the return of the above call trims whitespaces, and to match
        // we have to trim the endDelimiter as well.
        String trimmedDelimiter = StringUtil.trimRight(endDelimiter);
        if (endCommentsTrimmedText.endsWith(trimmedDelimiter)) {
            return endCommentsTrimmedText.substring(0, endCommentsTrimmedText.length() - trimmedDelimiter.length());
        } else {
            return procedureText;
        }
    }

    /**
     * Convenience method for when the schemaName is set but we don't want to parse the body
     */
    public static void surroundWithSchemaSets(List<Sql> sql, String schemaName, Database database) {
        if ((StringUtil.trimToNull(schemaName) != null) && !LiquibaseConfiguration.getInstance().getProperty(ChangeLogParserCofiguration.class, ChangeLogParserCofiguration.USE_PROCEDURE_SCHEMA).getValue(Boolean.class)) {
            String defaultSchema = database.getDefaultSchemaName();
            if (database instanceof OracleDatabase) {
                sql.add(0, new UnparsedSql("ALTER SESSION SET CURRENT_SCHEMA=" + database.escapeObjectName(schemaName, Schema.class)));
                sql.add(new UnparsedSql("ALTER SESSION SET CURRENT_SCHEMA=" + database.escapeObjectName(defaultSchema, Schema.class)));
            } else if (database instanceof AbstractDb2Database) {
                sql.add(0, new UnparsedSql("SET CURRENT SCHEMA " + schemaName));
                sql.add(new UnparsedSql("SET CURRENT SCHEMA " + defaultSchema));
            }
        }
    }

    /**
     * Convenience method for other classes similar to this that want to be able to modify the procedure text to add the schema
     */
    public static String addSchemaToText(String procedureText, String schemaName, String keywordBeforeName, Database database) {
        if (schemaName == null) {
            return procedureText;
        }
        if ((StringUtil.trimToNull(schemaName) != null) && LiquibaseConfiguration.getInstance().getProperty(ChangeLogParserCofiguration.class, ChangeLogParserCofiguration.USE_PROCEDURE_SCHEMA).getValue(Boolean.class)) {
            StringClauses parsedSql = SqlParser.parse(procedureText, true, true);
            StringClauses.ClauseIterator clauseIterator = parsedSql.getClauseIterator();
            Object next = "START";
            while ((next != null) && !next.toString().equalsIgnoreCase(keywordBeforeName) && clauseIterator.hasNext()) {
                if (!"PACKAGE".equalsIgnoreCase(keywordBeforeName) && "PACKAGE".equalsIgnoreCase((String) next)) {
                    return procedureText;
                }
                next = clauseIterator.nextNonWhitespace();
            }
            if ((next != null) && clauseIterator.hasNext()) {
                Object procNameClause = clauseIterator.nextNonWhitespace();
                if (procNameClause instanceof String) {
                    String[] nameParts = ((String) procNameClause).split("\\.");
                    String finalName;
                    if (nameParts.length == 1) {
                        finalName = database.escapeObjectName(schemaName, Schema.class) + "." + nameParts[0];
                    } else if (nameParts.length == 2) {
                        finalName = database.escapeObjectName(schemaName, Schema.class) + "." + nameParts[1];
                    } else if (nameParts.length == 3) {
                        finalName = nameParts[0] + "." + database.escapeObjectName(schemaName, Schema.class) + "." + nameParts[2];
                    } else {
                        finalName = (String) procNameClause; //just go with what was there
                    }
                    clauseIterator.replace(finalName);
                }
                procedureText = parsedSql.toString();
            }
        }
        return procedureText;
    }
}


package liquibase.database.core;

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

import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Locale;

public class DerbyDatabase extends AbstractJdbcDatabase {

    protected int driverVersionMajor;
    protected int driverVersionMinor;

    public DerbyDatabase() {
        super.setCurrentDateTimeFunction("CURRENT_TIMESTAMP");
        super.sequenceNextValueFunction = "NEXT VALUE FOR %s";
        super.sequenceCurrentValueFunction = "(SELECT currentvalue FROM sys.syssequences WHERE sequencename = upper('%s'))";
        determineDriverVersion();
        //add reserved words from http://developer.mimer.com/validator/sql-reserved-words.tml
        this.addReservedWords(Arrays.asList("ABSOLUTE", "ACTION", "ADD", "AFTER", "ALL", "ALLOCATE", "ALTER", "AND", "ANY", "ARE", "ARRAY", "AS", "ASC", "ASENSITIVE", "ASSERTION", "ASYMMETRIC", "AT", "ATOMIC", "AUTHORIZATION", "AVG", "BEFORE", "BEGIN", "BETWEEN", "BIGINT", "BINARY", "BIT", "BIT_LENGTH", "BLOB", "BOOLEAN", "BOTH", "BREADTH", "BY", "CALL", "CALLED", "CASCADE", "CASCADED", "CASE", "CAST", "CATALOG", "CHAR", "CHARACTER", "CHARACTER_LENGTH", "CHAR_LENGTH", "CHECK", "CLOB", "CLOSE", "COALESCE", "COLLATE", "COLLATION", "COLUMN", "COMMIT", "CONDITION", "CONNECT", "CONNECTION", "CONSTRAINT", "CONSTRAINTS", "CONSTRUCTOR", "CONTAINS", "CONTINUE", "CONVERT", "CORRESPONDING", "COUNT", "CREATE", "CROSS", "CUBE", "CURRENT", "CURRENT_DATE", "CURRENT_DEFAULT_TRANSFORM_GROUP", "CURRENT_PATH", "CURRENT_ROLE", "CURRENT_TIME", "CURRENT_TIMESTAMP", "CURRENT_TRANSFORM_GROUP_FOR_TYPE", "CURRENT_USER", "CURSOR", "CYCLE", "DATA", "DATE", "DAY", "DEALLOCATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DEFERRABLE", "DEFERRED", "DELETE", "DEPTH", "DEREF", "DESC", "DESCRIBE", "DESCRIPTOR", "DETERMINISTIC", "DIAGNOSTICS", "DISCONNECT", "DISTINCT", "DO", "DOMAIN", "DOUBLE", "DROP", "DYNAMIC", "EACH", "ELEMENT", "ELSE", "ELSEIF", "END", "EQUALS", "ESCAPE", "EXCEPT", "EXCEPTION", "EXEC", "EXECUTE", "EXISTS", "EXIT", "EXTERNAL", "EXTRACT", "FALSE", "FETCH", "FILTER", "FIRST", "FLOAT", "FOR", "FOREIGN", "FOUND", "FREE", "FROM", "FULL", "FUNCTION", "GENERAL", "GET", "GLOBAL", "GO", "GOTO", "GRANT", "GROUP", "GROUPING", "HANDLER", "HAVING", "HOLD", "HOUR", "IDENTITY", "IF", "IMMEDIATE", "IN", "INDICATOR", "INITIALLY", "INNER", "INOUT", "INPUT", "INSENSITIVE", "INSERT", "INT", "INTEGER", "INTERSECT", "INTERVAL", "INTO", "IS", "ISOLATION", "ITERATE", "JOIN", "KEY", "LANGUAGE", "LARGE", "LAST", "LATERAL", "LEADING", "LEAVE", "LEFT", "LEVEL", "LIKE", "LOCAL", "LOCALTIME", "LOCALTIMESTAMP", "LOCATOR", "LOOP", "LOWER", "MAP", "MATCH", "MAX", "MEMBER", "MERGE", "METHOD", "MIN", "MINUTE", "MODIFIES", "MODULE", "MONTH", "MULTISET", "NAMES", "NATIONAL", "NATURAL", "NCHAR", "NCLOB", "NEW", "NEXT", "NO", "NONE", "NOT", "NULL", "NULLIF", "NUMERIC", "OBJECT", "OCTET_LENGTH", "OF", "OLD", "ON", "ONLY", "OPEN", "OPTION", "OR", "ORDER", "ORDINALITY", "OUT", "OUTER", "OUTPUT", "OVER", "OVERLAPS", "PAD", "PARAMETER", "PARTIAL", "PARTITION", "PATH", "POSITION", "PRECISION", "PREPARE", "PRESERVE", "PRIMARY", "PRIOR", "PRIVILEGES", "PROCEDURE", "PUBLIC", "RANGE", "READ", "READS", "REAL", "RECURSIVE", "REF", "REFERENCES", "REFERENCING", "RELATIVE", "RELEASE", "REPEAT", "RESIGNAL", "RESTRICT", "RESULT", "RETURN", "RETURNS", "REVOKE", "RIGHT", "ROLE", "ROLLBACK", "ROLLUP", "ROUTINE", "ROW", "ROWS", "SAVEPOINT", "SCHEMA", "SCOPE", "SCROLL", "SEARCH", "SECOND", "SECTION", "SELECT", "SENSITIVE", "SESSION", "SESSION_USER", "SET", "SETS", "SIGNAL", "SIMILAR", "SIZE", "SMALLINT", "SOME", "SPACE", "SPECIFIC", "SPECIFICTYPE", "SQL", "SQLCODE", "SQLERROR", "SQLEXCEPTION", "SQLSTATE", "SQLWARNING", "START", "STATE", "STATIC", "SUBMULTISET", "SUBSTRING", "SUM", "SYMMETRIC", "SYSTEM", "SYSTEM_USER", "TABLE", "TABLESAMPLE", "TEMPORARY", "THEN", "TIME", "TIMESTAMP", "TIMEZONE_HOUR", "TIMEZONE_MINUTE", "TO", "TRAILING", "TRANSACTION", "TRANSLATE", "TRANSLATION", "TREAT", "TRIGGER", "TRIM", "TRUE", "UNDER", "UNDO", "UNION", "UNIQUE", "UNKNOWN", "UNNEST", "UNTIL", "UPDATE", "UPPER", "USAGE", "USER", "USING", "VALUE", "VALUES", "VARCHAR", "VARYING", "VIEW", "WHEN", "WHENEVER", "WHERE", "WHILE", "WINDOW", "WITH", "WITHIN", "WITHOUT", "WORK", "WRITE", "YEAR", "ZONE"));
    }

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

    @Override
    public String getDefaultDriver(String url) {
        // CORE-1230 - don't shutdown derby network server
        if (url.startsWith("jdbc:derby://")) {
            return "org.apache.derby.jdbc.ClientDriver";
        } else if (url.startsWith("jdbc:derby") || url.startsWith("java:derby")) {
            return "org.apache.derby.jdbc.EmbeddedDriver";
        }
        return null;
    }

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

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

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

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

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

    @Override
    public String correctObjectName(String objectName, Class<? extends DatabaseObject> objectType) {
        if (objectName == null) {
            return null;
        }
        return objectName.toUpperCase(Locale.US);
    }

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

    @Override
    public boolean supportsSequences() {
        return ((driverVersionMajor == 10) && (driverVersionMinor >= 6)) || (driverVersionMajor >= 11);
    }

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

    @Override
    public String getDateLiteral(String isoDate) {
        if (isDateOnly(isoDate)) {
            return "DATE(" + super.getDateLiteral(isoDate) + ")";
        } else if (isTimeOnly(isoDate)) {
            return "TIME(" + super.getDateLiteral(isoDate) + ")";
        } else {
            String dateString = super.getDateLiteral(isoDate);
            int decimalDigits = dateString.length() - dateString.indexOf('.') - 2;
            String padding = "";
            for (int i=6; i> decimalDigits; i--) {
                padding += "0";
            }
            return "TIMESTAMP(" + dateString.replaceFirst("'$", padding+"'") + ")";
        }
    }

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

    @Override
    public String getViewDefinition(CatalogAndSchema schema, String name) throws DatabaseException {
        return super.getViewDefinition(schema, name).replaceFirst("CREATE VIEW \\w+ AS ", "");
    }

    @Override
    public void close() throws DatabaseException {
        String url = getConnection().getURL();
        String driverName = getDefaultDriver(url);
        super.close();
        if ((driverName != null) && driverName.toLowerCase().contains("embedded")) {
            try {
                if (url.contains(";")) {
                    url = url.substring(0, url.indexOf(";")) + ";shutdown=true";
                } else {
                    url += ";shutdown=true";
                }
                Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Shutting down derby connection: " + url);
                // this cleans up the lock files in the embedded derby database folder
                JdbcConnection connection = (JdbcConnection) getConnection();
                ClassLoader classLoader = connection.getWrappedConnection().getClass().getClassLoader();
                Driver driver = (Driver) classLoader.loadClass(driverName).getConstructor().newInstance();
                // this cleans up the lock files in the embedded derby database folder
                driver.connect(url, null);
            } catch (Exception e) {
                if (e instanceof SQLException) {
                    String state = ((SQLException) e).getSQLState();
                    if ("XJ015".equals(state) || "08006".equals(state)) {
                        // "The XJ015 error (successful shutdown of the Derby engine) and the 08006
                        // error (successful shutdown of a single database) are the only exceptions
                        // thrown by Derby that might indicate that an operation succeeded. All other
                        // exceptions indicate that an operation failed."
                        // See http://db.apache.org/derby/docs/dev/getstart/rwwdactivity3.html
                        return;
                    }
                }
                throw new DatabaseException("Error closing derby cleanly", e);
            }
        }
    }

    /**
     * Determine Apache Derby driver major/minor version.
     */
    @SuppressWarnings({ "static-access", "unchecked" })
    protected void determineDriverVersion() {
        try {
// Locate the Derby sysinfo class and query its version info
            Enumeration<Driver> it = DriverManager.getDrivers();
            while (it.hasMoreElements()) {
                Driver driver = it.nextElement();
                if (driver.getClass().getName().contains("derby")) {
                    driverVersionMajor = driver.getMajorVersion();
                    driverVersionMinor = driver.getMinorVersion();
                    return;
                }
            }
//            log.debug("Unable to load/access Apache Derby driver class " + "to check version");
            driverVersionMajor = 10;
            driverVersionMinor = 6;
        } catch (Exception e) {
//            log.debug("Unable to load/access Apache Derby driver class " + "org.apache.derby.tools.sysinfo to check version: " + e.getMessage());
            driverVersionMajor = 10;
            driverVersionMinor = 6;
        }
    }

    @Override
    protected String getConnectionCatalogName() throws DatabaseException {
        if ((getConnection() == null) || (getConnection() instanceof OfflineConnection)) {
            return null;
        }
        try {
            return ExecutorService.getInstance().getExecutor(this).queryForObject(new RawSqlStatement("select current schema from sysibm.sysdummy1"), String.class);
        } catch (Exception e) {
            Scope.getCurrentScope().getLog(getClass()).info(LogType.LOG, "Error getting default schema", e);
        }
        return null;
    }

    @Override
    public boolean supportsCatalogInObjectName(Class<? extends DatabaseObject> type) {
        return true;
    }

    public boolean supportsBooleanDataType() {
        if (getConnection() == null) {
            return false; ///assume not;
        }
        try {
            return (this.getDatabaseMajorVersion() > 10) || ((this.getDatabaseMajorVersion() == 10) && (this
                .getDatabaseMinorVersion() > 7));
        } catch (DatabaseException e) {
            return false; //assume not
        }
    }

    @Override
    public int getMaxFractionalDigitsForTimestamp() {
        // According to
        // https://db.apache.org/derby/docs/10.7/ref/rrefsqlj27620.html
        return 9;
    }
}


package liquibase.util;

import liquibase.Scope;
import liquibase.configuration.GlobalConfiguration;
import liquibase.configuration.LiquibaseConfiguration;
import liquibase.exception.UnexpectedLiquibaseException;
import liquibase.logging.LogType;

import java.io.InputStream;
import java.security.DigestInputStream;
import java.security.MessageDigest;

/**
 * Generates md5-sums based on a string.
 */
public class MD5Util {

    /**
     * Used to build output as Hex
     */
    private static final char[] DIGITS_LOWER = {
        '0', '1', '2', '3', '4', '5', '6', '7',
           '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'
    };

    public static String computeMD5(String input) {
        if (input == null) {
            return null;
        }
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");
            digest.update(input.getBytes(LiquibaseConfiguration.getInstance().getConfiguration(GlobalConfiguration.class).getOutputEncoding()));
        } catch (Exception e) {
            throw new UnexpectedLiquibaseException(e);
        }
        byte[] digestBytes = digest.digest();

        String returnString = new String(encodeHex(digestBytes));

        String inputToLog = input;
        if (inputToLog.length() > 500) {
            inputToLog = inputToLog.substring(0, 500)+"... [truncated in log]";
        }
        Scope.getCurrentScope().getLog(MD5Util.class).fine(LogType.LOG, "Computed checksum for "+inputToLog+" as "+returnString);
        return returnString;

    }

    public static String computeMD5(InputStream stream) {
        MessageDigest digest;
        try {
            digest = MessageDigest.getInstance("MD5");

            DigestInputStream digestStream = new DigestInputStream(stream, digest);
            byte[] buf = new byte[20480];
            while (digestStream.read(buf) != -1) {
                ; //digest is updating
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        byte[] digestBytes = digest.digest();

        String returnString = new String(encodeHex(digestBytes));

        Scope.getCurrentScope().getLog(MD5Util.class).fine(LogType.LOG, "Computed checksum for inputStream as "+returnString);
        return returnString;
    }

    /**
     * Converts an array of bytes into an array of characters representing the hexadecimal values of each byte in order.
     * The returned array will be double the length of the passed array, as it takes two characters to represent any
     * given byte.
     *
     * @param data
     *            a byte[] to convert to Hex characters
     * @return A char[] containing hexadecimal characters
     */
    private static char[] encodeHex(byte[] data) {

        int l = data.length;

        char[] out = new char[l << 1];

        // two characters form the hex value.
        for (int i = 0, j = 0; i < l; i++) {
            out[j++] = DIGITS_LOWER[(0xF0 & data[i]) >>> 4];
            out[j++] = DIGITS_LOWER[0x0F & data[i]];
        }

        return out;
    }

}


package liquibase.snapshot.jvm;

import liquibase.CatalogAndSchema;
import liquibase.database.AbstractJdbcDatabase;
import liquibase.database.Database;
import liquibase.database.ObjectQuotingStrategy;
import liquibase.database.jvm.JdbcConnection;
import liquibase.diff.compare.DatabaseObjectComparatorFactory;
import liquibase.exception.DatabaseException;
import liquibase.snapshot.DatabaseSnapshot;
import liquibase.snapshot.InvalidExampleException;
import liquibase.structure.DatabaseObject;
import liquibase.structure.core.Catalog;
import liquibase.structure.core.Schema;
import liquibase.util.JdbcUtils;

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

public class SchemaSnapshotGenerator extends JdbcSnapshotGenerator {

    public SchemaSnapshotGenerator() {
        super(Schema.class);
    }

    @Override
    protected DatabaseObject snapshotObject(DatabaseObject example, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        Database database = snapshot.getDatabase();
        Schema match = null;

        String catalogName = ((Schema) example).getCatalogName();
        String schemaName = example.getName();
        if (database.supportsSchemas()) {
            if (catalogName == null) {
                catalogName = database.getDefaultCatalogName();
            }
            if (schemaName == null) {
                schemaName = database.getDefaultSchemaName();
            }
        } else {
            if (database.supportsCatalogs()) {
                if ((catalogName == null) && (schemaName != null)) {
                    catalogName = schemaName;
                    schemaName = null;
                }
            } else {
                catalogName = null;
                schemaName = null;
            }
        }

        example = new Schema(catalogName, schemaName);

        // use LEGACY quoting since we're dealing with system objects
        ObjectQuotingStrategy currentStrategy = database.getObjectQuotingStrategy();
        database.setObjectQuotingStrategy(ObjectQuotingStrategy.LEGACY);
        try {
            if (database.supportsSchemas()) {
                for (String tableSchema : getDatabaseSchemaNames(database)) {
                    CatalogAndSchema schemaFromJdbcInfo = toCatalogAndSchema(tableSchema, database);

                    Catalog catalog = new Catalog(schemaFromJdbcInfo.getCatalogName());

                    Schema schema = new Schema(catalog, tableSchema);

                    if (DatabaseObjectComparatorFactory.getInstance().isSameObject(schema, example, snapshot.getSchemaComparisons(), database)) {
                        if (match == null) {
                            match = schema;
                        } else {
                            throw new InvalidExampleException("Found multiple catalog/schemas matching " + ((Schema) example).getCatalogName() + "." + example.getName());
                        }
                    }
                }
            } else { // Database does NOT support schemas
                // If the database supports catalogs, but does not support schema names, then we treat the schema
                // name as equal to the catalog name.
                if (((Schema) example).getCatalog().isDefault()) {
                    match = new Schema(((Schema) example).getCatalog(), catalogName);
                } else {
                    /* Before we confirm the schema/catalog existence, we must first check if the catalog exists. */
                    Catalog catalog = ((Schema) example).getCatalog();
                    String[] dbCatalogNames = getDatabaseCatalogNames(database);
                    for (String candidateCatalogName : dbCatalogNames) {
                        if (catalog.equals(new Catalog(candidateCatalogName))) {
                            match = new Schema(catalog, catalogName);
                        }

                    }

                }
            }
        } catch (SQLException e) {
            throw new DatabaseException(e);
        } finally {
            database.setObjectQuotingStrategy(currentStrategy);
        }

        if ((match != null) && ((match.getName() == null) || match.getName().equalsIgnoreCase(database
            .getDefaultSchemaName()))) {
            match.setDefault(true);
        }
        return match;
    }

    protected CatalogAndSchema toCatalogAndSchema(String tableSchema, Database database) {
        return ((AbstractJdbcDatabase) database).getSchemaFromJdbcInfo(null, tableSchema);
    }

    @Override
    protected void addTo(DatabaseObject foundObject, DatabaseSnapshot snapshot) throws DatabaseException, InvalidExampleException {
        //no other types
    }

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

        ResultSet schemas = null;
        try {
            schemas = ((JdbcConnection) database.getConnection()).getMetaData().getSchemas();
            while (schemas.next()) {
                returnList.add(JdbcUtils.getValueForColumn(schemas, "TABLE_SCHEM", database));
            }
        } finally {
            if (schemas != null) {
                schemas.close();
            }
        }

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

}


package liquibase.util.csv.opencsv;

/**
 Copyright 2005 Bytecode Pty Ltd.

 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at

 http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

import liquibase.util.csv.opencsv.stream.reader.LineReader;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

/**
 * A very simple CSV reader released under a commercial-friendly license.
 *
 * @author Glen Smith
 */
public class CSVReader implements Closeable, Iterable<String[]> {

    public static final boolean DEFAULT_KEEP_CR = false;
    public static final boolean DEFAULT_VERIFY_READER = true;
    /**
     * The default line to start reading.
     */
    public static final int DEFAULT_SKIP_LINES = 0;
    public static final int READ_AHEAD_LIMIT = Character.SIZE / Byte.SIZE;
    private CSVParser parser;
    private int skipLines;
    private BufferedReader br;
    private LineReader lineReader;
    private boolean hasNext = true;
    private boolean linesSkiped;
    private boolean keepCR;
    private boolean verifyReader;

    private long linesRead;
    private long recordsRead;

    /**
     * Constructs CSVReader using a comma for the separator.
     *
     * @param reader the reader to an underlying CSV source.
     */
    public CSVReader(Reader reader) {
        this(reader, CSVParser.DEFAULT_SEPARATOR, CSVParser.DEFAULT_QUOTE_CHARACTER, CSVParser.DEFAULT_ESCAPE_CHARACTER);
    }

    /**
     * Constructs CSVReader with supplied separator.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param separator the delimiter to use for separating entries.
     */
    public CSVReader(Reader reader, char separator) {
        this(reader, separator, CSVParser.DEFAULT_QUOTE_CHARACTER, CSVParser.DEFAULT_ESCAPE_CHARACTER);
    }

    /**
     * Constructs CSVReader with supplied separator and quote char.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param separator the delimiter to use for separating entries
     * @param quotechar the character to use for quoted elements
     */
    public CSVReader(Reader reader, char separator, char quotechar) {
        this(reader, separator, quotechar, CSVParser.DEFAULT_ESCAPE_CHARACTER, DEFAULT_SKIP_LINES, CSVParser.DEFAULT_STRICT_QUOTES);
    }

    /**
     * Constructs CSVReader with supplied separator, quote char and quote handling
     * behavior.
     *
     * @param reader       the reader to an underlying CSV source.
     * @param separator    the delimiter to use for separating entries
     * @param quotechar    the character to use for quoted elements
     * @param strictQuotes sets if characters outside the quotes are ignored
     */
    public CSVReader(Reader reader, char separator, char quotechar, boolean strictQuotes) {
        this(reader, separator, quotechar, CSVParser.DEFAULT_ESCAPE_CHARACTER, DEFAULT_SKIP_LINES, strictQuotes);
    }

    /**
     * Constructs CSVReader.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param separator the delimiter to use for separating entries
     * @param quotechar the character to use for quoted elements
     * @param escape    the character to use for escaping a separator or quote
     */

    public CSVReader(Reader reader, char separator,
                     char quotechar, char escape) {
        this(reader, separator, quotechar, escape, DEFAULT_SKIP_LINES, CSVParser.DEFAULT_STRICT_QUOTES);
    }

    /**
     * Constructs CSVReader.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param separator the delimiter to use for separating entries
     * @param quotechar the character to use for quoted elements
     * @param line      the line number to skip for start reading
     */
    public CSVReader(Reader reader, char separator, char quotechar, int line) {
        this(reader, separator, quotechar, CSVParser.DEFAULT_ESCAPE_CHARACTER, line, CSVParser.DEFAULT_STRICT_QUOTES);
    }

    /**
     * Constructs CSVReader.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param separator the delimiter to use for separating entries
     * @param quotechar the character to use for quoted elements
     * @param escape    the character to use for escaping a separator or quote
     * @param line      the line number to skip for start reading
     */
    public CSVReader(Reader reader, char separator, char quotechar, char escape, int line) {
        this(reader, separator, quotechar, escape, line, CSVParser.DEFAULT_STRICT_QUOTES);
    }

    /**
     * Constructs CSVReader.
     *
     * @param reader       the reader to an underlying CSV source.
     * @param separator    the delimiter to use for separating entries
     * @param quotechar    the character to use for quoted elements
     * @param escape       the character to use for escaping a separator or quote
     * @param line         the line number to skip for start reading
     * @param strictQuotes sets if characters outside the quotes are ignored
     */
    public CSVReader(Reader reader, char separator, char quotechar, char escape, int line, boolean strictQuotes) {
        this(reader, separator, quotechar, escape, line, strictQuotes, false);
    }

    /**
     * Constructs CSVReader with all data entered.
     *
     * @param reader                  the reader to an underlying CSV source.
     * @param separator               the delimiter to use for separating entries
     * @param quotechar               the character to use for quoted elements
     * @param escape                  the character to use for escaping a separator or quote
     * @param line                    the line number to skip for start reading
     * @param strictQuotes            sets if characters outside the quotes are ignored
     * @param ignoreLeadingWhiteSpace it true, parser should ignore white space before a quote in a field
     */
    public CSVReader(Reader reader, char separator, char quotechar, char escape, int line, boolean strictQuotes, boolean ignoreLeadingWhiteSpace) {
        this(reader,
                line,
                new CSVParser(separator, quotechar, escape, strictQuotes, ignoreLeadingWhiteSpace));
    }

    /**
     * Constructs CSVReader with all data entered.
     *
     * @param reader                  the reader to an underlying CSV source.
     * @param separator               the delimiter to use for separating entries
     * @param quotechar               the character to use for quoted elements
     * @param escape                  the character to use for escaping a separator or quote
     * @param line                    the line number to skip for start reading
     * @param strictQuotes            sets if characters outside the quotes are ignored
     * @param ignoreLeadingWhiteSpace if true, parser should ignore white space before a quote in a field
     * @param keepCR                  if true the reader will keep carriage returns, otherwise it will discard them.
     */
    public CSVReader(Reader reader, char separator, char quotechar, char escape, int line, boolean strictQuotes,
                     boolean ignoreLeadingWhiteSpace, boolean keepCR) {
        this(reader, line,
                new CSVParser(separator, quotechar, escape, strictQuotes, ignoreLeadingWhiteSpace), keepCR, DEFAULT_VERIFY_READER);
    }

    /**
     * Constructs CSVReader with supplied CSVParser.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param line      the line number to skip for start reading
     * @param csvParser the parser to use to parse input
     */
    public CSVReader(Reader reader, int line, CSVParser csvParser) {
        this(reader, line, csvParser, DEFAULT_KEEP_CR, DEFAULT_VERIFY_READER);
    }

    /**
     * Constructs CSVReader with supplied CSVParser.
     *
     * @param reader    the reader to an underlying CSV source.
     * @param line      the line number to skip for start reading
     * @param csvParser the parser to use to parse input
     * @param keepCR    true to keep carriage returns in data read, false otherwise
     * @param verifyReader   true to verify reader before each read, false otherwise
     */
    CSVReader(Reader reader, int line, CSVParser csvParser, boolean keepCR, boolean verifyReader) {
        this.br = ((reader instanceof BufferedReader) ? (BufferedReader) reader : new BufferedReader(reader));
        this.lineReader = new LineReader(br, keepCR);
        this.skipLines = line;
        this.parser = csvParser;
        this.keepCR = keepCR;
        this.verifyReader = verifyReader;
    }
    /**
     * @return the CSVParser used by the reader.
     */
    public CSVParser getParser() {
        return parser;
    }

    /**
     * Returns the number of lines in the csv file to skip before processing.  This is
     * useful when there is miscellaneous data at the beginning of a file.
     *
     * @return the number of lines in the csv file to skip before processing.
     */
    public int getSkipLines() {
        return skipLines;
    }

    /**
     * Returns if the reader will keep carriage returns found in data or remove them.
     *
     * @return true if reader will keep carriage returns, false otherwise.
     */
    public boolean keepCarriageReturns() {
        return keepCR;
    }

    /**
     * Reads the entire file into a List with each element being a String[] of
     * tokens.
     *
     * @return a List of String[], with each String[] representing a line of the
     * file.
     * @throws IOException if bad things happen during the read
     */
    public List<String[]> readAll() throws IOException {

        List<String[]> allElements = new ArrayList<>();
        while (hasNext) {
            String[] nextLineAsTokens = readNext();
            if (nextLineAsTokens != null) {
                allElements.add(nextLineAsTokens);
            }
        }
        return allElements;

    }

    /**
     * Reads the next line from the buffer and converts to a string array.
     *
     * @return a string array with each comma-separated element as a separate
     * entry.
     * @throws IOException if bad things happen during the read
     */
    public String[] readNext() throws IOException {

        String[] result = null;
        do {
            String nextLine = getNextLine();
            if (!hasNext) {
                return validateResult(result);
            }
            String[] r = parser.parseLineMulti(nextLine);
            if (r.length > 0) {
                if (result == null) {
                    result = r;
                } else {
                    result = combineResultsFromMultipleReads(result, r);
                }
            }
        } while (parser.isPending());
        return validateResult(result);
    }

    /**
     * Increments the number of records read if the result passed in is not null.
     *
     * @param result
     * @return result that was passed in.
     */
    protected String[] validateResult(String[] result) {
        if (result != null) {
            recordsRead++;
        }
        return result;
    }

    /**
     * For multi line records this method combines the current result with the result from previous read(s).
     * @param buffer - previous data read for this record
     * @param lastRead - latest data read for this record.
     * @return String array with union of the buffer and lastRead arrays.
     */
    protected String[] combineResultsFromMultipleReads(String[] buffer, String[] lastRead) {
        String[] t = new String[buffer.length + lastRead.length];
        System.arraycopy(buffer, 0, t, 0, buffer.length);
        System.arraycopy(lastRead, 0, t, buffer.length, lastRead.length);
        return t;
    }

    /**
     * Reads the next line from the file.
     *
     * @return the next line from the file without trailing newline
     * @throws IOException if bad things happen during the read
     */
    protected String getNextLine() throws IOException {
        if (isClosed()) {
            hasNext = false;
            return null;
        }

        if (!this.linesSkiped) {
            for (int i = 0; i < skipLines; i++) {
                lineReader.readLine();
                linesRead++;
            }
            this.linesSkiped = true;
        }
        String nextLine = lineReader.readLine();
        if (nextLine == null) {
            hasNext = false;
        } else {
            linesRead++;
        }

        return hasNext ? nextLine : null;
    }

    /**
     * Checks to see if the file is closed.
     * @return true if the reader can no longer be read from.
     */
    private boolean isClosed() {
        if (!verifyReader) {
            return false;
        }
        try {
            br.mark(READ_AHEAD_LIMIT);
            int nextByte = br.read();
            br.reset(); // resets stream position, possible because its buffered
            return nextByte == -1; // read() returns -1 at end of stream
        } catch (IOException e) {
            return true;
        }
    }

    /**
     * Closes the underlying reader.
     *
     * @throws IOException if the close fails
     */
    public void close() throws IOException {
        br.close();
    }

    /**
     * Creates an Iterator for processing the csv data.
     * @return an String[] iterator.
     */
    public Iterator<String[]> iterator() {
        try {
            return new CSVIterator(this);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Returns if the CSVReader will verify the reader before each read.
     *

     * By default the value is true which is the functionality for version 3.0.
     * If set to false the reader is always assumed ready to read - this is the functionality
     * for version 2.4 and before.
     *

     * The reason this method was needed was that certain types of Readers would return
     * false for its ready() method until a read was done (namely readers created using Channels).
     * This caused opencsv not to read from those readers.
     *
     * @return true if CSVReader will verify the reader before reads.  False otherwise.
     * @link https://sourceforge.net/p/opencsv/bugs/108/
     * @since 3.3
     */
    public boolean verifyReader() {
        return this.verifyReader;
    }

    /**
     * Used for debugging purposes this method returns the number of lines that has been read from
     * the reader passed into the CSVReader.
     *

     * Given the following data.
     *
     *

     * First line in the file
     * some other descriptive line
     * a,b,c
     *
     * a,"b\nb",c
     *

     *
     * With a CSVReader constructed like so
     *
     *

     * CSVReader c = builder.withCSVParser(new CSVParser())
     *                      .withSkipLines(2)
     *                      .build();
     *

     *
     * The initial call to getLinesRead will be 0.

     * After the first call to readNext() then getLinesRead will return 3 (because header was read).

     * After the second call to read the blank line then getLinesRead will return 4 (still a read).

     * After third call to readNext getLinesRead will return 6 because it took two line reads to retrieve this record.

     * Subsequent calls to readNext (since we are out of data) will not increment the number of lines read.

     *

     * An example of this is in the linesAndRecordsRead() test in CSVReaderTest.
     *
     * @return the number of lines read by the reader (including skip lines).
     * @link https://sourceforge.net/p/opencsv/feature-requests/73/
     * @since 3.6
     */
    public long getLinesRead() {
        return linesRead;
    }

    /**
     * Used for debugging purposes this method returns the number of records that has been read from
     * the CSVReader.
     *

     * Given the following data.
     *

     * First line in the file
     * some other descriptive line
     * a,b,c
     *

     * a,"b\nb",c
     *

     * With a CSVReader constructed like so
     *

     * CSVReader c = builder.withCSVParser(new CSVParser())
     *                      .withSkipLines(2)
     *                      .build();
     *

     * The initial call to getRecordsRead will be 0.

     * After the first call to readNext() then getRecordsRead will return 1.

     * After the second call to read the blank line then getRecordsRead will return 2
     * (a blank line is considered a record with one empty field).

     * After third call to readNext getRecordsRead will return 3 because even though
     * reads to retrieve this record it is still a single record read.

     * Subsequent calls to readNext (since we are out of data) will not increment the number of records read.

     *

     * An example of this is in the linesAndRecordsRead() test in CSVReaderTest.
     *
     * @return the number of records (Array of Strings[]) read by the reader.
     * @link https://sourceforge.net/p/opencsv/feature-requests/73/
     * @since 3.6
     */
    public long getRecordsRead() {
        return recordsRead;
    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.config;

import java.util.Map;

/**
 * This class represents result from a poll of configuration source. The result may be the complete
 * content of the configuration source, or an incremental one.
 *
 * @author awang
 * @author cfregly {@link WatchedUpdateResult}
 */
public class PollResult extends WatchedUpdateResult {
    protected final Object checkPoint;

    /**
     * Create a full result that represents the complete content of the configuration source.
     * @param complete map that contains all the properties
     */
    public static PollResult createFull(Map<String, Object> complete) {
        return new PollResult(complete);
    }

    /**
     * Create a result that represents incremental changes from the configuration
     * source.
     *
     * @param added properties added
     * @param changed properties changed
     * @param deleted properties deleted, in which case the value in the map will be ignored
     * @param checkPoint Object that served as a marker for this incremental change, for example, a time stamp
     *        of the last change.
     */
    public static PollResult createIncremental(Map<String, Object> added,
            Map<String, Object> changed, Map<String, Object> deleted, Object checkPoint) {
        return new PollResult(added, changed, deleted, checkPoint);
    }

    /**
     * @return the check point (marker) for this poll result
     */
    public final Object getCheckPoint() {
        return checkPoint;
    }

    PollResult(Map<String, Object> complete) {
        super(complete);
        this.checkPoint = null;
    }

    PollResult(Map<String, Object> added, Map<String, Object> changed, Map<String, Object> deleted, Object checkPoint) {
        super(added, changed, deleted);
        this.checkPoint = checkPoint;
    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.config;

import java.util.Collection;
import java.util.Map;

import javax.annotation.Nullable;

import com.google.common.base.Function;
import com.google.common.base.Predicate;

/**
 * The default predicate (used by {@link DynamicContextualProperty}) which takes a Function to get the value of
 * key included in the JSON blob as the input string value of {@link DynamicContextualProperty}.
 *
 * @author awang
 *
 */
public class DefaultContextualPredicate implements Predicate<Map<String, Collection<String>>> {

    private final Function<String, String> getValueFromKeyFunction;

    public static final DefaultContextualPredicate PROPERTY_BASED = new DefaultContextualPredicate(new Function<String, String>() {
        @Override
        public String apply(@Nullable String input) {
            return DynamicProperty.getInstance(input).getString();
        }

    });

    public DefaultContextualPredicate(Function<String, String> getValueFromKeyFunction) {
        this.getValueFromKeyFunction = getValueFromKeyFunction;
    }

    /**
     * For each key in the passed in map, this function returns true if
     *
     *
 the value derived from the key using the function (passed in from the constructor) matches any of the value included for the same key in the map
     *
 the above holds true for all keys in the map
     *
     */
    @Override
    public boolean apply(@Nullable Map<String, Collection<String>> input) {
        if (null == input) {
            throw new NullPointerException();
        }
        for (Map.Entry<String, Collection<String>> entry: input.entrySet()) {
            String key = entry.getKey();
            Collection<String> value = entry.getValue();
            if (!value.contains(getValueFromKeyFunction.apply(key))) {
                return false;
            }
        }
        return true;

    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.config;

/**
 * A configuration that polls a {@link PolledConfigurationSource} according to the schedule set by a
 * scheduler. The property values in this configuration will be changed dynamically at runtime if the
 * value changes in the configuration source.
 * <p>
 * This configuration does not allow null as key or value and will throw NullPointerException
 * when trying to add or set properties with empty key or value.
 *
 * @author awang
 *
 */
public class DynamicConfiguration extends ConcurrentMapConfiguration {
    private AbstractPollingScheduler scheduler;
    private PolledConfigurationSource source;

    /**
     * Create an instance and start polling the configuration source.
     *
     * @param source PolledConfigurationSource to poll
     * @param scheduler AbstractPollingScheduler whose {@link AbstractPollingScheduler#schedule(Runnable)} will be
     *                  used to determine the polling schedule
     */
    public DynamicConfiguration(PolledConfigurationSource source, AbstractPollingScheduler scheduler) {
        this();
        startPolling(source, scheduler);
    }

    public DynamicConfiguration() {
        super();
    }

    /**
     * Start polling the configuration source with the specified scheduler.
     *
     * @param source PolledConfigurationSource to poll
     * @param scheduler AbstractPollingScheduler whose {@link AbstractPollingScheduler#schedule(Runnable)} will be
     *                  used to determine the polling schedule
     */
    public synchronized void startPolling(PolledConfigurationSource source, AbstractPollingScheduler scheduler) {
        this.scheduler = scheduler;
        this.source = source;
        init(source, scheduler);
        scheduler.startPolling(source, this);
    }

    /**
     * Initialize the configuration. This method is called in
     * {@link #DynamicConfiguration(PolledConfigurationSource, AbstractPollingScheduler)}
     * and {@link #startPolling(PolledConfigurationSource, AbstractPollingScheduler)}
     * before the initial polling. The default implementation does nothing.
     */
    protected void init(PolledConfigurationSource source, AbstractPollingScheduler scheduler) {
    }

    /**
     * Stops the scheduler
     */
    public synchronized void stopLoading() {
        if (scheduler != null) {
            scheduler.stop();
        }
    }

    public PolledConfigurationSource getSource() {
        return source;
    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.config.source;

import java.io.Closeable;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;

import com.google.common.io.Closeables;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.cache.ChildData;
import org.apache.curator.framework.recipes.cache.PathChildrenCache;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheEvent.Type;
import org.apache.curator.framework.recipes.cache.PathChildrenCacheListener;
import org.apache.zookeeper.KeeperException.NoNodeException;
import org.apache.zookeeper.KeeperException.NodeExistsException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.WatchedUpdateListener;
import com.netflix.config.WatchedUpdateResult;
import com.netflix.config.WatchedConfigurationSource;

/**
 * Implementation of the dynamic {@link WatchedConfigurationSource} for ZooKeeper using Curator.
 *
 * This implementation requires the path to the ZK root parent node that contains
 * the hierarchy of configuration properties.
 * An example is /<my-app>/config
 *
 * Properties are direct ZK child nodes of the root parent ZK node.
 * An example ZK child property node is /<my-app>/config/com.fluxcapacitor.my.property
 *
 * The value is stored in the ZK child property node and can be updated at any time.
 * All servers will receive a ZK Watcher callback and automatically update their value
 * similar to other dynamic configuration sources (ie. DynamoDB, etc.)
 *
 * @author cfregly
 */
public class ZooKeeperConfigurationSource implements WatchedConfigurationSource, Closeable {
    private static final Logger logger = LoggerFactory.getLogger(ZooKeeperConfigurationSource.class);

    private final CuratorFramework client;
    private final String configRootPath;
    private final PathChildrenCache pathChildrenCache;

    private final Charset charset = Charset.forName("UTF-8");

    private List<WatchedUpdateListener> listeners = new CopyOnWriteArrayList<WatchedUpdateListener>();

    /**
     * Creates the pathChildrenCache using the CuratorFramework client and ZK root path node for the config
     *
     * @param Curator client
     * @param path to ZK root parent node for the rest of the configuration properties (ie. /<my-app>/config)
     */
    public ZooKeeperConfigurationSource(CuratorFramework client, String configRootPath) {
        this.client = client;
        this.configRootPath = configRootPath;
        this.pathChildrenCache = new PathChildrenCache(client, configRootPath, true);
    }

    /**
     * Adds a listener to the pathChildrenCache, initializes the cache, then starts the cache-management background thread
     *
     * @throws Exception
     */
    public void start() throws Exception {
    	// create the watcher for future configuration updatess
        pathChildrenCache.getListenable().addListener(new PathChildrenCacheListener() {
            public void childEvent(CuratorFramework aClient, PathChildrenCacheEvent event)
                    throws Exception {
                Type eventType = event.getType();
                ChildData data = event.getData();

                String path = null;
                if (data != null) {
                    path = data.getPath();

                    // scrub configRootPath out of the key name
                    String key = removeRootPath(path);

                    byte[] value = data.getData();
                    String stringValue = new String(value, charset);

                    logger.debug("received update to pathName [{}], eventType [{}]", path, eventType);
                    logger.debug("key [{}], and value [{}]", key, stringValue);

                    // fire event to all listeners
                    Map<String, Object> added = null;
                    Map<String, Object> changed = null;
                    Map<String, Object> deleted = null;
                    if (eventType == Type.CHILD_ADDED) {
                        added = new HashMap<String, Object>(1);
                        added.put(key, stringValue);
                    } else if (eventType == Type.CHILD_UPDATED) {
                        changed = new HashMap<String, Object>(1);
                        changed.put(key, stringValue);
                    } else if (eventType == Type.CHILD_REMOVED) {
                        deleted = new HashMap<String, Object>(1);
                        deleted.put(key, stringValue);
                    }

                    WatchedUpdateResult result = WatchedUpdateResult.createIncremental(added,
                        changed, deleted);

                    fireEvent(result);
                }
            }
        });

        // passing true to trigger an initial rebuild upon starting.  (blocking call)
        pathChildrenCache.start(true);
    }

    @Override
    public Map<String, Object> getCurrentData() throws Exception {
        logger.debug("getCurrentData() retrieving current data.");

        List<ChildData> children = pathChildrenCache.getCurrentData();
        Map<String, Object> all = new HashMap<String, Object>(children.size());
        for (ChildData child : children) {
            String path = child.getPath();
            String key = removeRootPath(path);
            byte[] value = child.getData();

            all.put(key, new String(value, charset));
        }

        logger.debug("getCurrentData() retrieved [{}] config elements.", children.size());

        return all;
    }

    @Override
    public void addUpdateListener(WatchedUpdateListener l) {
        if (l != null) {
            listeners.add(l);
        }
    }

    @Override
    public void removeUpdateListener(WatchedUpdateListener l) {
        if (l != null) {
            listeners.remove(l);
        }
    }

    protected void fireEvent(WatchedUpdateResult result) {
        for (WatchedUpdateListener l : listeners) {
            try {
                l.updateConfiguration(result);
            } catch (Throwable ex) {
                logger.error("Error in invoking WatchedUpdateListener", ex);
            }
        }
    }

    /**
     * This is used to convert a configuration nodePath into a key
     *
     * @param nodePath
     *
     * @return key (nodePath less the config root path)
     */
    private String removeRootPath(String nodePath) {
        return nodePath.replace(configRootPath + "/", "");
    }

    //@VisibleForTesting
    synchronized void setZkProperty(String key, String value) throws Exception {
        final String path = configRootPath + "/" + key;

        byte[] data = value.getBytes(charset);

        try {
            // attempt to create (intentionally doing this instead of checkExists())
            client.create().creatingParentsIfNeeded().forPath(path, data);
        } catch (NodeExistsException exc) {
            // key already exists - update the data instead
            client.setData().forPath(path, data);
        }
    }

    //@VisibleForTesting
    synchronized String getZkProperty(String key) throws Exception {
        final String path = configRootPath + "/" + key;

        byte[] bytes = client.getData().forPath(path);

        return new String(bytes, charset);
    }

    //@VisibleForTesting
    synchronized void deleteZkProperty(String key) throws Exception {
        final String path = configRootPath + "/" + key;

        try {
            client.delete().forPath(path);
        } catch (NoNodeException exc) {
        	// Node doesn't exist - NoOp
            logger.warn("Node doesn't exist", exc);
        }
    }

    public void close() {
        try {
            Closeables.close(pathChildrenCache, true);
        } catch (IOException exc) {
            logger.error("IOException should not have been thrown.", exc);
        }
    }
}


/**
 * Copyright 2014 Netflix, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.netflix.config;

import java.io.File;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.Properties;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.PropertiesConfiguration;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.netflix.config.util.ConfigurationUtils;

import com.netflix.config.sources.URLConfigurationSource;

/**
 * This class helps in loading properties present in a typical web appplication.
 *
 * 1. In a series of application properties file (typically under
 * WEB-INF/conf/*)
 *
 * 2. Every library (JAR) located in the web application's
 * classpath i.e. WEB-INF/lib/*.jar, can contain properties in say
 * META-INF/conf/config.properties
 *
 * This class helps load all the properties into the Default Configuration.
 *
 * In case you would like to load these properties into another Configuration
 * instead, you can use {@link #getProperties()} to obtain the properties and
 * load these into the Configuration of your choice.
 *
 * @author stonse
 *
 */
public class WebApplicationProperties {

	private static final Logger logger = LoggerFactory
			.getLogger(WebApplicationProperties.class);

	static File appConfFolder = new File("."); // folder where the
												// application properties
												// files are
	static String baseConfigFileName = "config";

	static boolean loadLibraryProperties = true;

	static String libraryPropertiesResourceRelativePath = ClasspathPropertiesConfiguration.propertiesResourceRelativePath;

	/**
	 * Initialize. The application should call this method once the Application
	 * Folder is set using {@link #setAppConfFolder(File, String)} method.
	 *
	 * @param appConfFolderFromConfig
	 * @param baseConfigFileName
	 * @param loadLibraryProperties
	 * @param propertiesResourceRelativePath
	 */
	public static void initialize(File appConfFolderFromConfig,
			String baseConfigFileName, boolean loadLibraryProperties,
			String propertiesResourceRelativePath) {
		WebApplicationProperties.appConfFolder = appConfFolderFromConfig;
		WebApplicationProperties.baseConfigFileName = baseConfigFileName;
		WebApplicationProperties.loadLibraryProperties = loadLibraryProperties;
		WebApplicationProperties.libraryPropertiesResourceRelativePath = propertiesResourceRelativePath;
		initialize();
	}

	/**
	 * Initialize.
	 * The application should call this method once the Application Folder is set using {@link #setAppConfFolder(File, String)} method.
	 */
	public static void initialize() {
		try {
			initApplicationProperties();
		} catch (Exception e) {
			logger.error("Unable to load Application Properties", e);
			System.err.println("Unable to load Application Properties");
			e.printStackTrace();
		}

		// should we load the properties in the JAR files?
		if (loadLibraryProperties){
			try {
				initClasspathPropertiesConfiguration();
			} catch (Exception e) {
				logger.error("Unable to load Library Properties", e);
				System.err.println("Unable to load Library Properties");
				e.printStackTrace();
			}
		}

		logger.debug("Properties loaded:" + getProperties());
	}

	protected static void initClasspathPropertiesConfiguration() {
		ClasspathPropertiesConfiguration.initialize();
	}

	protected static void initApplicationProperties()
			throws ConfigurationException, MalformedURLException {
		File appPropFile = new File(appConfFolder + "/" + baseConfigFileName + ".properties");
		File appEnvPropOverrideFile = new File(appConfFolder + "/" + baseConfigFileName +
		 getEnvironment() + ".properties");

		// TODO awang, how do we add this to archaius default config?
		PropertiesConfiguration appConf = new PropertiesConfiguration(
				appPropFile);
		// apply env overrides
		PropertiesConfiguration overrideConf = new PropertiesConfiguration(appEnvPropOverrideFile);
		Properties overrideprops = ConfigurationUtils.getProperties(overrideConf);
		for (Object prop: overrideprops.keySet()){
			appConf.setProperty(""+prop, overrideprops.getProperty(""+prop));
		}
		String path = appPropFile.toURI().toURL().toString();
		System.setProperty(URLConfigurationSource.CONFIG_URL, path);
		ConfigurationManager.loadPropertiesFromConfiguration(appConf);

	}

	private static String getEnvironment() {
		String env = System.getProperty("env");
		return env;
	}

	public static File getAppConfFolder() {
		return appConfFolder;
	}

	/**
	 * The folder where the application's properties files are located.
	 *
	 * @param appConfFolderFromConfig
	 *            the folder as a File object where the
	 *            application's properties files are located
	 * @param baseConfigFileName
	 *            the "base" name of the properties file. For e.g. if you have
	 *            config.properties, config-test.properties, then the value to
	 *            pass in is "config"
	 */
	public static void setAppConfFolder(File appConfFolderFromConfig, String baseConfigFileName) {
		WebApplicationProperties.appConfFolder = appConfFolderFromConfig;
		WebApplicationProperties.baseConfigFileName = baseConfigFileName;
	}

	/**
	 * Should we load the Library Properties
	 */
	public static boolean shouldLoadLibraryProperties() {
		return loadLibraryProperties;
	}

	/**
	 * Set this if you would like the {@link ClasspathPropertiesConfiguration}
	 * to scan the JAR files in the classpath and load those proerties into the
	 * default Configuration
	 *
	 * @param loadLibraryProperties
	 */
	public static void setLoadLibraryProperties(boolean loadLibraryProperties) {
		WebApplicationProperties.loadLibraryProperties = loadLibraryProperties;
	}

	/**
	 * Returns the relative Resource Path of the properties files in the JAR files
	 */
	public static String getLibraryPropertiesResourceRelativePath() {
		return libraryPropertiesResourceRelativePath;
	}

	/**
	 * Please see {@link ClasspathPropertiesConfiguration} on Library Properties.
	 * This methods sets the relativeResource path
	 * @param libraryPropertiesResourceRelativePath
	 */
	public static void setLibraryPropertiesResourceRelativePath(
			String libraryPropertiesResourceRelativePath) {
		WebApplicationProperties.libraryPropertiesResourceRelativePath = libraryPropertiesResourceRelativePath;
	}

	/**
	 * Returns all the properties presently available
	 */
	public static Properties getProperties() {
		return ConfigurationUtils.getProperties(ConfigurationManager.getConfigInstance());
	}
}