Populator.java

/**
 *
 */
package org.andromda.jdbcmetadata;

import java.io.File;
import java.io.FileInputStream;
import java.net.URL;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import org.andromda.utils.StringUtilsHelper;
import org.apache.commons.dbutils.DbUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;

/**
 * Populate DB metadata info in Objects from a JDBC database connection
 */
public class Populator
{
    private static final Logger LOGGER = Logger.getLogger(Populator.class);
    private String jdbcDriver;
    private String jdbcConnectionUrl;
    private String jdbcUser;
    private String jdbcPassword;
    private List<Table> tables = new ArrayList<Table>();
    private List<Column> columns = new ArrayList<Column>();
    private List<Index> indexes = new ArrayList<Index>();
    private List<ForeignKey> foreignKeys = new ArrayList<ForeignKey>();
    private List<Sequence> sequences = new ArrayList<Sequence>();
    private Schema schema = new Schema();
    private DatabaseMetaData metadata;
    private String tableNamePattern;
    private String tableNameExcludePattern;
    private String columnNamePattern;
    private String columnNameExcludePattern;
    private List<String> columnNameIncludePatterns = new ArrayList<String>();
    private List<String> columnNameExcludePatterns = new ArrayList<String>();
    private List<String> tableNameIncludePatterns = new ArrayList<String>();
    private List<String> tableNameExcludePatterns = new ArrayList<String>();
    private String typeMappingsUri;
    private String wrapperMappingsUri;
    @SuppressWarnings("unused")
    private String expressionMappingsUri;
    private JdbcTypeFinder typeMapper = new JdbcTypeFinder();
    private Properties overrides = new Properties();

    /**
     *
     */
    public Populator()
    {
        // TODO Auto-generated constructor stub
    }

    /**
     * @param args
     */
    public static void main(String[] args)
    {
        Populator pop = new Populator();
        pop.setConfiguration();
        pop.populate();
    }

    /**
     * Connect to DB, populate tables, columns, indexes, keys, sequences
     */
    public void populate()
    {
        populatePattern(this.tableNamePattern, this.tableNameIncludePatterns);
        populatePattern(this.tableNameExcludePattern, this.tableNameExcludePatterns);
        populatePattern(this.columnNamePattern, this.columnNameIncludePatterns);
        populatePattern(this.columnNameExcludePattern, this.columnNameExcludePatterns);
        SqlToModelNameFormatter.loadWordMap();

        // TODO Load from classpath with andromda-core dependency
        this.setTypeMappingsUri("file:${basedir}/../mappings/RevengSqlMappings.xml");
        this.setWrapperMappingsUri("file:${basedir}/../mappings/UML2WrapperMappings.xml");
        this.setExpressionMappingsUri("file:${basedir}/../mappings/DBtoJavaExpressionMappings.xml");
        this.typeMapper.setTypeMappings(this.getTypeMappingsUri(), this.getWrapperMappingsUri());

        Connection conn = this.jdbcConnect();
        long startTime = System.currentTimeMillis();
        //int tableCount = 0;
        if (conn != null)
        {
            try
            {
                //tableCount =
                    populateTables(conn);
                populatePks(conn);
                populateIndexes(conn);
                populateUnique();
                populateColumns(conn);
                populateFks(conn);
                populateSequences(conn);
            }
            catch (SQLException ex)
            {
                LOGGER.error("SQLError on " + conn.toString(), ex);
            }
            DbUtils.closeQuietly(conn);
        }
        logResults(startTime);
    }

    /**
     * Set the configuration values
     */
    // TODO Read configuration from file or command line
    public void setConfiguration()
    {
        this.schema.setSchemaName("PUBLIC");
        this.schema.setCatalog("REVENG");
        // Java regexp .* means match all (match any character . any number of times *)
        // Also used when creating filters for reveng output file.
        // Use default .* when not specified for include patterns
        this.schema.setTableNamePattern(".*");
        this.tableNamePattern = ".*";
        this.columnNamePattern = ".*";
        // .*_VRSN$ and VRSN$ does not exclude CTCT_EVT_VRSN table when running reveng with hibernate tools
        this.tableNameExcludePattern = "ALL_.*,JDO_.*,.*_VRSN$";
        this.columnNameExcludePattern = "VRSN_.*,JDO_.*,CREATE_DTS,UPDT_DTS,CREATE_USR_ID_CD,UPDT_USR_ID_CD,PRGE_DTS";
        // Maven plugin: Get these values from the pom properties
        this.jdbcDriver = "org.h2.Driver";
        this.jdbcConnectionUrl = "jdbc:h2:~/reveng;MODE=ORACLE;AUTO_SERVER=TRUE;AUTO_RECONNECT=TRUE;DB_CLOSE_ON_EXIT=FALSE;DB_CLOSE_DELAY=-1";
        this.jdbcUser = "sa";
        this.jdbcPassword = "sa";
        // TODO set SqlToModelNameFormat config dir, wordMap, overrideMap file locations
        SqlToModelNameFormatter.setInputDirectory("${basedir}");
        readOverrideConfiguration("${basedir}/override.properties");
    }

    /**
     * Populate the UML default overrides for global/table/column configuration, for things like
     * sequence name, hibernate type, java type, insertable/updatable/nullable, transient, version. temporal type
     * @param propertyFileName file containing table.column.parameter=value entries
     * @return Map of String key = String value
     */
    public Properties readOverrideConfiguration(String propertyFileName)
    {
        if (StringUtils.isNotBlank(propertyFileName))
        {
            if (!propertyFileName.endsWith(".properties"))
            {
                propertyFileName += ".properties";
            }
            File propertyFile = new File(propertyFileName);
            // Check first if absolute pathname file exists
            if (propertyFile.exists())
            {
                try
                {
                    this.overrides.load(new FileInputStream(propertyFileName));
                }
                catch (Exception e)
                {
                    LOGGER.warn(propertyFileName, e);
                }
            }
            // Next try loading from classpath
            if (this.overrides.isEmpty())
            {
                try
                {
                    URL url = ClassLoader.getSystemResource(propertyFileName);
                    this.overrides.load(url.openStream());
                }
                catch (Exception e)
                {
                    LOGGER.warn(propertyFileName, e);
                }
            }
        }
        return this.overrides;
    }

    /**
     * Populate include and exclude pattern lists by parsing delimited string
     * @param pattern
     * @param patternList
     */
    @SuppressWarnings("static-method")
    public void populatePattern(String pattern, List<String> patternList)
    {
        if (StringUtils.isNotBlank(pattern) && patternList != null)
        {
            patternList.addAll(Arrays.asList(StringUtils.split(pattern, ",|")));
        }
    }

    /**
     * @return JDBC Connection
     */
    public Connection jdbcConnect()
    {
        long startTime = System.currentTimeMillis();
        if (this.jdbcDriver == null || this.jdbcConnectionUrl == null)
        {
            LOGGER.error(
                "JDBC Parameters not set, jdbcDriver=" + this.jdbcDriver + " jdbcConnectionUrl=" + this.jdbcConnectionUrl);
        }
        Connection connection = null;
        try
        {
            Class.forName(this.jdbcDriver);
            connection = DriverManager.getConnection(this.jdbcConnectionUrl,
                    this.jdbcUser, this.jdbcPassword);
        }
        catch (Exception ex)
        {
            LOGGER.error("Connection error", ex);
        }
        if (connection != null)
        {
            LOGGER.info(
                "Connected, TIME --> " + ((System.currentTimeMillis() - startTime) / 1000.0) + "[s]");
        }
        return connection;
    }

    /**
     * @param conn
     * @return Number of tables
     * @throws SQLException
     */
    public int populateTables(Connection conn) throws SQLException
    {
        int tableCount = 0;
        long startTime = System.currentTimeMillis();
        this.metadata = conn.getMetaData();
        ResultSet tableRs = this.metadata.getTables(
            //this.schema.getCatalog(),
            null,
            this.schema.getSchemaName(),
            //this.schema.getTableNamePattern(),
            null,
            new String[] {"TABLE"});
        //List<Table> tables = new ArrayList<Table>();
        //Pattern pTableEx = Pattern.compile(this.tableNameExcludePattern, Pattern.CASE_INSENSITIVE);
        //Pattern pTableIn = Pattern.compile(this.tableNameExcludePattern, Pattern.CASE_INSENSITIVE);
        while (tableRs.next())
        {
            Table table = new Table();
            String tableName = tableRs.getString("TABLE_NAME");
            table.setName(tableName);
            if (!isExcluded(tableName, this.tableNameIncludePatterns, this.tableNameExcludePatterns))
            {
                String umlName = getOverride(tableName, null, "umlName");
                if (StringUtils.isBlank(umlName))
                {
                    umlName = SqlToModelNameFormatter.toClassName(tableName);
                }
                table.setUmlName(umlName);
                String extend = getOverride(tableName, null, "extends");
                if (StringUtils.isNotBlank(extend))
                {
                    table.setExtend(extend);
                    LOGGER.info("Table " + table.getName() + " extends" + extend);
                }
                String implement = getOverride(tableName, null, "implements");
                if (StringUtils.isNotBlank(implement))
                {
                    table.setImplement(implement);
                    LOGGER.info("Table " + table.getName() + " implements" + implement);
                }
                table.setDescription(StringUtils.replaceChars(tableRs.getString("REMARKS"), "<>&", ""));
                table.setType(tableRs.getString("TABLE_TYPE"));
                table.setSchema(this.schema);
                tableCount++;
                LOGGER.debug("Table " + table.getName() + " class=" + table.getUmlName());
            }
            else
            {
                table.setExcluded(true);
                LOGGER.debug("Excluded Table " + table.getName());
            }
            this.tables.add(table);
        }
        LOGGER.info(
            "Populated " + tableCount + " tables in " +((System.currentTimeMillis() - startTime) / 1000.0) + "[s]");
        return tableCount;
    }

    /**
     * @param conn
     * @throws SQLException
     */
    public void populateColumns(Connection conn) throws SQLException
    {
        for (Table table : this.tables)
        {
            if (!table.isExcluded())
            {
                List<Column> tableColumns = new ArrayList<Column>();
                final ResultSet columnRs = this.metadata.getColumns(null,
                    this.schema.getSchemaName(), table.getName(), null);
                while (columnRs.next())
                {
                    final String columnName = columnRs.getString("COLUMN_NAME");
                    //Column column = new Column();
                    //column.setColumnName(columnName);
                    //column.setTable(table);
                    Column column = findColumnByName(table, columnName);
                    column.setSeqNum(columnRs.getInt("ORDINAL_POSITION"));
                    if (StringUtils.isBlank(column.getUmlName()))
                    {
                        String umlName = getOverride(table.getName() + '.' + columnName, null, "umlName");
                        if (StringUtils.isNotBlank(umlName))
                        {
                            column.setUmlName(umlName);
                        }
                        else
                        {
                            // If column is part of table name, remove table name from UML name
                            umlName = SqlToModelNameFormatter.toAttributeName(columnName);
                            if (StringUtils.capitalize(umlName).startsWith(table.getUmlName()))
                            {
                                umlName = StringUtils.uncapitalize(umlName.substring(table.getUmlName().length()));
                            }
                            column.setUmlName(umlName);
                        }
                    }
                    String dbName = table.getName() + "." + column.getColumnName();
                    String umlColumnName = table.getUmlName() + "." + column.getUmlName();
                    // Yes I know it's denormalized, but it's easier because we always need the table name with the column
                    column.setTableName(table.getName());
                    if (!isExcluded(columnName, this.columnNameIncludePatterns, this.columnNameExcludePatterns))
                    {
                        //logger.info("Column " + column.getColumnName().toUpperCase() + " exclude=" + this.columnNameExcludePattern.toUpperCase()
                        //        + " matches=" + column.getColumnName().toUpperCase().matches(this.columnNameExcludePattern.toUpperCase()));
                        column.setSqlType(columnRs.getString("TYPE_NAME"));
                        column.setJavaType(columnRs.getString("DATA_TYPE"));
                        column.setColSize(Integer.valueOf(columnRs.getString("COLUMN_SIZE")).intValue());
                        column.setDecPlaces(Integer.valueOf(columnRs.getString("DECIMAL_DIGITS")));
                        if (!column.isSetNullable())
                        {
                            column.setNullable(columnRs.getInt("NULLABLE") != DatabaseMetaData.attributeNoNulls ? true : false);
                        }
                        column.setDefaultValue(columnRs.getString("COLUMN_DEF"));
                        String definition = column.getSqlType() + "(" + column.getColSize()
                            + (JdbcTypeFinder.isNumeric(column.getSqlType()) ? ", " + column.getDecPlaces() : "")
                            + ")" + (!column.isNullable() ? " NOT NULL" : "");
                        // Set the hibernate type directly. i.e. for yes_no or other custom type
                        String defaultValue = this.getOverride(dbName, umlColumnName, "default");
                        if (!StringUtils.isBlank(defaultValue))
                        {
                            column.setJavaDefault(defaultValue);
                        }
                        if (StringUtils.isNotBlank(column.getDefaultValue()))
                        {
                            definition += " DEFAULT " + column.getDefaultValue();
                            if (StringUtils.isBlank(column.getJavaDefault()))
                            {
                                String javaExpression = ExpressionTypeFinder.getJavaExpression(column.getDefaultValue().replaceAll("'", ""),
                                    column.getJdbcType(), column.isNullable(), column.getColSize(), column.getPkTable() != null,
                                    column.isUnique());
                                // SQL default has single quotes for String types
                                column.setJavaDefault(javaExpression);
                                LOGGER.info("Column " + dbName + " JavaDefault=" + column.getJavaDefault());
                            }
                        }
                        column.setSqlDefinition(definition);
                        column.setAutoincrement(columnRs.getString("IS_AUTOINCREMENT").equals("YES") ? true : false);
                        String umlType = this.getOverride(dbName, umlColumnName, "umlType");
                        if (umlType == null)
                        {
                            column.setUmlType(this.typeMapper.getUmlType(column.getSqlType(), column.isNullable(), column.getColSize(),
                                    column.getDecPlaces(), column.getPkTable() != null, column.isUnique()));
                        }
                        else
                        {
                            column.setUmlType(umlType);
                        }
                        column.setDescription(StringUtils.replaceChars(columnRs.getString("REMARKS"), "<>&", ""));
                        //column.setUmlName(SqlToModelNameFormatter.toAttributeName(columnName));
                        //column.setNavigable(this.getBooleanOverride(dbName, umlColumnName, "navigable", true));
                        // Set the hibernate type directly. i.e. for yes_no or other custom type
                        column.setHibernateType(this.getOverride(dbName, umlColumnName, "hibernateType"));
                        LOGGER.info("Column " + dbName + " definition=" + column.getSqlDefinition()
                            + " umlName=" + umlColumnName + " umlType=" + column.getUmlType());
                    }
                    else
                    {
                        column.setExcluded(true);
                        LOGGER.info("Excluded Column " + dbName);
                    }
                    tableColumns.add(column);
                }
                Collections.sort(tableColumns);
                table.setColumns(tableColumns);
                this.columns.addAll(tableColumns);
            }
        }
    }

    /**
     * @param dbName
     * @param umlColumnName
     * @param property
     * @param defaultValue 
     * @return rtnValue
     */
    public boolean getBooleanOverride(String dbName, String umlColumnName, String property, boolean defaultValue)
    {
        String stringDefault = getOverride(dbName, umlColumnName, property);
        if (StringUtils.isNotBlank(stringDefault))
        {
            if (stringDefault.equalsIgnoreCase("false") || stringDefault.equalsIgnoreCase("f") || stringDefault.equalsIgnoreCase("0"))
            {
                defaultValue=false;
            }
            else if (stringDefault.equalsIgnoreCase("true") || stringDefault.equalsIgnoreCase("t") || stringDefault.equalsIgnoreCase("1"))
            {
                defaultValue=true;
            }
        }
        return defaultValue;
    }

    /**
     * @param dbName
     * @param umlColumnName
     * @param property
     * @param defaultValue 
     * @return rtnValue
     */
    public String getStringOverride(String dbName, String umlColumnName, String property, String defaultValue)
    {
        String stringDefault = getOverride(dbName, umlColumnName, property);
        if (StringUtils.isNotBlank(stringDefault))
        {
            defaultValue=stringDefault;
        }
        return defaultValue;
    }

    /**
     * @param dbName
     * @param umlName
     * @param property
     * @return rtnValue
     */
    public String getOverride(String dbName, String umlName, String property)
    {
        String rtnValue = null;
        if (umlName != null)
        {
            rtnValue = this.overrides.getProperty(umlName + "." + property, null);
        }
        if (StringUtils.isBlank(rtnValue))
        {
            rtnValue = this.overrides.getProperty(dbName + "." + property, null);
            if (StringUtils.isNotBlank(rtnValue))
            {
                rtnValue = rtnValue.trim();
                LOGGER.info("overrides DB " + dbName + " property " + property + "=" + rtnValue);
            }
            else
            // Check default property value
            {
                rtnValue = this.overrides.getProperty("default." + property, null);
                if (StringUtils.isNotBlank(rtnValue))
                {
                    rtnValue = rtnValue.trim();
                    LOGGER.info("overrides DB " + dbName + " default property " + property + "=" + rtnValue);
                }
            }
        }
        else
        {
            if (StringUtils.isNotBlank(rtnValue) && rtnValue != null)
            {
                rtnValue = rtnValue.trim();
            }
            LOGGER.info("overrides Attribute " + umlName + " property " + property + "=" + rtnValue);
        }
        /*if (StringUtils.isBlank(rtnValue))
        {
            LOGGER.info("overrides Not Found for " + umlName + " or " + dbName + " property " + property);
        }*/
        return rtnValue;
    }

    /**
     * @param name Column name to check for inclusion/exclusion against patterns
     * @param includePatterns
     * @param excludePatterns
     * @return true if column or table is excluded based on include/exclude patterns
     */
    public boolean isExcluded(String name, List<String> includePatterns, List<String> excludePatterns)
    {
        if (includePatterns == null)
        {
            includePatterns = new ArrayList<String>();
        }
        if (excludePatterns == null)
        {
            excludePatterns = new ArrayList<String>();
        }
        boolean excluded = true;
        if (StringUtils.isBlank(name))
        {
            return excluded;
        }
        final String checkName = name.toUpperCase();
        // Verify that name matches any included pattern
        if (includePatterns.isEmpty())
        {
            excluded = false;
        }
        for (String nameIncludePattern : includePatterns)
        {
            if ((StringUtils.isBlank(nameIncludePattern) ||
                checkName.matches(nameIncludePattern.toUpperCase())))
            {
                excluded = false;
                break;
            }
        }
        // Verify that name does not match any excluded pattern
        if (!excluded && !excludePatterns.isEmpty())
        {
            for (String nameExcludePattern : excludePatterns)
            {
                if (checkName.matches(nameExcludePattern.toUpperCase()))
                {
                    excluded = true;
                    break;
                }
            }
        }
        return excluded;
    }

    /**
     * @param conn
     * @throws SQLException
     */
    public void populateIndexes(Connection conn) throws SQLException
    {
        for (Table table : this.tables)
        {
            if (!table.isExcluded())
            {
                Index index = null;
                final ResultSet indexRs = this.metadata.getIndexInfo(null,
                    this.schema.getSchemaName(), table.getName(), false, true);
                while (indexRs.next())
                {
                    final String indexName = indexRs.getString("INDEX_NAME");
                    // results ordered by index name, seq
                    if (index == null || !indexName.equals(index.getName()))
                    {
                        index = new Index();
                        index.setName(indexName);
                        index.setUnique(!indexRs.getBoolean("NON_UNIQUE"));
                        index.setTable(table);
                        this.indexes.add(index);
                        // Bidirectional - add reference on both sides
                        table.getIndexes().add(index);
                    }
                    IndexColumn indexCol = new IndexColumn();
                    final String columnName = indexRs.getString("COLUMN_NAME");
                    indexCol.setSeqNum(indexRs.getInt("ORDINAL_POSITION"));
                    Column column = findColumnByName(table, columnName);
                    indexCol.setColumn(column);
                    index.getColumns().add(indexCol);
                    LOGGER.info("Index " + table.getName() + "." + column.getColumnName() + " seq=" + indexCol.getSeqNum()
                        + " name=" + index.getName() + " unique=" + index.isUnique());
                }
            }
        }
    }

    /**
     * Iterates through all table indexes to find unique or pk indexes with only one column
     * @throws SQLException
     */
    public void populateUnique() throws SQLException
    {
        for (Table table : this.tables)
        {
            if (!table.isExcluded())
            {
                if (table.getPkColumns().size() == 1)
                {
                    Column column = table.getPkColumns().iterator().next();
                    column.setUnique(true);
                    LOGGER.info("populatePkUnique " + table.getName() + "." + column.getColumnName());
                }
                for (Index index : table.getIndexes())
                {
                    if (index.isUnique() && index.getColumns().size() == 1)
                    {
                        Column column = index.getColumns().iterator().next().getColumn();
                        column.setUnique(true);
                        LOGGER.info("populateUnique " + table.getName() + "." + column.getColumnName());
                    }
                }
            }
        }
    }

    /**
     * @param conn
     * @throws SQLException
     */
    public void populatePks(Connection conn) throws SQLException
    {
        for (Table table : this.tables)
        {
            if (!table.isExcluded())
            {
                final ResultSet pkRs = this.metadata.getPrimaryKeys(null,
                    this.schema.getSchemaName(), table.getName());
                while (pkRs.next())
                {
                    //PkColumn pkColumn = new PkColumn();
                    table.setPkName(pkRs.getString("PK_NAME"));
                    final String columnName = pkRs.getString("COLUMN_NAME");
                    //pkColumn.setSeqNum(pkRs.getInt("KEY_SEQ"));
                    Column column = findColumnByName(table, columnName);
                    column.setPkSeqNum(pkRs.getInt("KEY_SEQ"));
                    table.getPkColumns().add(column);
                    String umlName = getOverride(table.getName() + '.' + columnName, null, "umlName");
                    if (StringUtils.isNotBlank(umlName))
                    {
                        column.setUmlName(umlName);
                    }
                    else
                    {
                        // If column is part of table PK, set UML name back to original full name
                        column.setUmlName(SqlToModelNameFormatter.toAttributeName(column.getColumnName()));
                    }
                    column.setNullable(false); // For use in findBy a
                    column.setPkTable(table);
                    LOGGER.info("PK " + table.getName() + "." + column.getColumnName() + " seq=" + column.getPkSeqNum()
                        + " pk=" + column.getColumnName());
                }
            }
        }
    }

    /**
     * @param conn
     * @throws SQLException
     */
    public void populateFks(Connection conn) throws SQLException
    {
        for (Table table : this.tables)
        {
            if (!table.isExcluded())
            {
                ForeignKey fk = null;
                // primary key columns that are referenced by the given table's
                // foreign key columns (the primary keys imported by a table)
                final ResultSet keysRs = this.metadata.getImportedKeys(null,
                    this.schema.getSchemaName(), table.getName());
                while (keysRs.next())
                {
                    final String fkName = keysRs.getString("FK_NAME");
                    final String pkName = keysRs.getString("PK_NAME");
                    //final String fkTableName = keysRs.getString("FKTABLE_NAME");
                    final String fkColumnName = keysRs.getString("FKCOLUMN_NAME");
                    final String pkTableName = keysRs.getString("PKTABLE_NAME");
                    final String pkColumnName = keysRs.getString("PKCOLUMN_NAME");
                    // results ordered by key name, seq. If key or table name changes, create new FK
                    if (fk == null || !fkName.equals(fk.getFkName()))
                    {
                        fk = new ForeignKey();
                        fk.setFkName(fkName);
                        fk.setPkName(pkName);
                        fk.setFkTable(table);
                        fk.setReferencedTable(findTableByName(pkTableName));
                        fk.setUpdateRule(getCascadeType(keysRs.getInt("UPDATE_RULE")));
                        fk.setDeleteRule(getCascadeType(keysRs.getInt("DELETE_RULE")));
                        this.foreignKeys.add(fk);
                        table.getForeignKeys().add(fk);
                    }
                    FKColumn fkSeq = new FKColumn();
                    fkSeq.setSeqNum(keysRs.getInt("KEY_SEQ"));
                    Column column = findColumnByName(table, fkColumnName);
                    Column refColumn = findColumnByName(findTableByName(pkTableName), pkColumnName);
                    fkSeq.setFkColumn(column);
                    fkSeq.setFkRefColumn(refColumn);
                    fkSeq.setForeignKey(fk);
                    fk.getColumns().add(fkSeq);
                    column.getFkSequences().add(fkSeq);
                    refColumn.getFkRefSequences().add(fkSeq);
                    this.setNavAgg(table, fk);
                    LOGGER.info("FK " + table.getName() + "." + column.getColumnName() + " seq=" + fkSeq.getSeqNum()
                        + " name=" + fk.getFkName() + " pkColumn=" + fk.getReferencedTable().getName() + "." + fkSeq.getFkRefColumn().getColumnName());
                }
            }
        }
    }

    /**
     * @param table
     * @param fk
     */
    public void setNavAgg(Table table, ForeignKey fk)
    {
        String pkTable = StringUtils.uncapitalize(fk.getReferencedTable().getUmlName());
        String fkTable = StringUtils.uncapitalize(fk.getFkTable().getUmlName());
        boolean one2One = isOne2One(fk);
        fk.setOneToOne(one2One);
        if (!one2One)
        {
            fkTable = StringUtilsHelper.pluralize(fkTable);
        }
        String umlPKName = fk.getReferencedTable().getUmlName() + '.' + fkTable;
        String umlFKName = fk.getFkTable().getUmlName() + '.' + pkTable;
        fk.setFkUmlPropertyName(pkTable);
        // TODO Customize override associationEnd property name
        String navigable = getOverride(umlPKName, null, "navigable");
        if (StringUtils.isNotBlank(navigable))
        {
            try
            {
                fk.setNavigableFK(Boolean.valueOf(navigable).booleanValue());
            }
            catch (Exception ex)
            {
                LOGGER.error("not boolean value for navigable property on " + umlPKName, ex);
            }
        }
        String aggType = getOverride(umlPKName, null, "aggregation");
        if (StringUtils.isNotBlank(aggType))
        {
            // Change aggregation type from default 'none' to 'shared' or 'composite'
            fk.setAggregationFK(AggregationType.fromString(aggType.toUpperCase()));
        }

        fk.setPkUmlPropertyName(fkTable);
        navigable = getOverride(umlFKName, null, "navigable");
        if (StringUtils.isNotBlank(navigable))
        {
            try
            {
                fk.setNavigablePK(Boolean.valueOf(navigable).booleanValue());
            }
            catch (Exception ex)
            {
                LOGGER.error("not boolean value for navigable property on " + umlFKName, ex);
            }
        }
        aggType = getOverride(umlFKName, null, "aggregation");
        if (StringUtils.isNotBlank(aggType))
        {
            // Change aggregation type from default 'none' to 'shared' or 'composite'
            fk.setAggregationPK(AggregationType.fromString(aggType.toUpperCase()));
        }
        LOGGER.info("setNavAgg one2one=" + one2One + " PK property=" + umlFKName + " nav=" + fk.isNavigablePK() + " agg=" + fk.getAggregationPK()
                + "  FK=" + umlPKName + " nav=" + fk.isNavigableFK() + " agg=" + fk.getAggregationFK());
    }

    /**
     * The relationship between to tables is 1:1 if all of the table FK columns
     * are also PK columns, and correspond to the referenced table PK columns
     * @param fk
     * @return true if 1:1 relationship
     */
    public boolean isOne2One(ForeignKey fk)
    {
        boolean one2one = false;
        // 1:1 tables must have the same PK columns
        //LOGGER.info("isOne2One RefPKTable: " + fk.getReferencedTable().getUmlName() + "=" + fk.getReferencedTable().getPkColumns().size() + " FkTable " + fk.getFkTable().getUmlName() + "=" + fk.getColumns().size());
        if (fk.getFkTable().getPkColumns().size() ==
            fk.getColumns().size())
        {
            one2one = true;
            int colNum = 0;
            for (FKColumn fkColumn : fk.getColumns())
            {
                // If every FK column corresponds to a PK column OneToOne=true
                Column pkColumn = fk.getFkTable().getPkColumns().get(colNum);
                if (!fkColumn.getFkColumn().equals(pkColumn))
                {
                    one2one = false;
                }
                /*LOGGER.info("isOne2One pkColumn=" + pkColumn);
                LOGGER.info("isOne2One fkColumn=" + fkColumn.getFkColumn());
                LOGGER.info("isOne2One fkRefColumn=" + fkColumn.getFkRefColumn());
                LOGGER.info("isOne2One one2one=" + one2one + " " + colNum + " fk=" + fkColumn.getFkColumn().getTable().getUmlName() + "." + fkColumn.getFkColumn().getColumnName() + colNum +
                    " pk=" + pkColumn.getTable().getUmlName() + "." + pkColumn.getColumnName());*/
                colNum++;
            }
        }
        // one2one is always false if no FK columns or any column not part of PK
        if (one2one)
        {
            LOGGER.info("isOne2One one2one=" + one2one + " fk=" + fk.getFkTable().getUmlName()
                    + " pk=" + fk.getReferencedTable().getUmlName());
        }
        return one2one;
    }

    // From databaseMetaData source
    /** 0 */
    public static final int importedKeyCascade = 0;
    /** 1 */
    public static final int importedKeyRestrict = 1;
    /** 2 */
    public static final int importedKeySetNull = 2;
    /** 3 */
    public static final int importedKeyNoAction = 3;
    /** 4 */
    public static final int importedKeySetDefault = 4;
    /**
     * @param type
     * @return CascadeType
     */
    public CascadeType getCascadeType(int type)
    {
        // Default RESTRICT if not set
        if (type == 0)
        {
            return CascadeType.CASCADE;
        }
        /*else if (type == 1)
        {
            return CascadeType.RESTRICT;
        }*/
        else if (type == 2)
        {
            return CascadeType.SET_NULL;
        }
        else if (type == 3)
        {
            return CascadeType.NO_ACTION;
        }
        else if (type == 4)
        {
            return CascadeType.SET_DEFAULT;
        }
        else
        {
            return CascadeType.RESTRICT;
        }
    }

    /**
     * @param conn
     * @throws SQLException
     */
    public void populateSequences(Connection conn) throws SQLException
    {
        // Get system tables with the word 'SEQUENCE'
        final ResultSet seqTablesRs = this.metadata.getTables(null,
            null, "%SEQUENCE%", new String[] { "SYSTEM TABLE" });
        while (seqTablesRs.next())
        {
            final String tableName = seqTablesRs.getString("TABLE_NAME");
            final String tableCat = seqTablesRs.getString("TABLE_CAT");
            final String tableSchema = seqTablesRs.getString("TABLE_SCHEM");
            final Table table = findTableByName(tableName);
            if (table == null || !table.isExcluded())
            {
                String cols = " Columns: ";
                String query = "SELECT ";
                String nameColumn = null;
                // Get column called 'NAME' from SEQUENCES table
                final ResultSet tableRs = this.metadata.getColumns(tableCat,
                    tableSchema, tableName, null);
                while (tableRs.next())
                {
                    final String columnName = tableRs.getString("COLUMN_NAME");
                    cols += columnName + ", ";
                    if (columnName.contains("NAME"))
                    {
                        query += columnName;
                        nameColumn = columnName;
                    }
                }
                LOGGER.debug("System Table " + tableName + " catalog=" + tableCat + " schema=" + tableSchema + cols);
                // Create and execute query to select names from sequence table
                query += " FROM " + tableSchema + "." + tableName;
                Statement statement = null;
                try
                {
                    statement = conn.createStatement();
                    ResultSet seqRs = statement.executeQuery(query);
                    while (seqRs.next())
                    {
                        Sequence seq = new Sequence();
                        final String seqName = seqRs.getString(nameColumn);
                        seq.setName(seqName);
                        this.sequences.add(seq);
                        LOGGER.info("Added Sequence " + seqName);
                    }
                } catch (SQLException e ) {
                    e.printStackTrace();
                } finally {
                    if (statement != null) { statement.close(); }
                }
            }
        }
        /*// If there is only one sequence in the DB, it must be used as generator for all numeric PK
        if (this.sequences.size() == 1)
        {
            for (Table table : this.tables)
            {
                if (table.getPkColumns().size() == 1)
                {
                    for (Column column : table.getPkColumns())
                    {
                        // Check if column type is numeric
                        if (column.getJavaType())
                        column.set
                    }
                }
            }
        }*/
    }

    /**
     * Return the Table object reference with the given name
     * @param tableName Name to be found
     * @return Table object with a given name
     */
    public Table findTableByName(String tableName)
    {
        for (Table table : this.tables)
        {
            if (table.getName().toUpperCase().equalsIgnoreCase(tableName))
            {
                return table;
            }
        }
        return null;
    }

    /**
     * Return the Column object reference with the given name on a table
     * @param table Table containing column
     * @param columnName Column name to be found
     * @return Table object with a given name
     */
    public Column findColumnByName(Table table, String columnName)
    {
        for (Column column : this.columns)
        {
            if (column.getColumnName().toUpperCase().equalsIgnoreCase(columnName)
                && column.getTable().getName().toUpperCase().equalsIgnoreCase(table.getName()))
            {
                return column;
            }
        }
        // Column does not yet exist on this table - create and add it
        Column column = new Column();
        column.setColumnName(columnName);
        column.setTableName(table.getName());
        column.setTable(table);
        table.getColumns().add(column);
        this.columns.add(column);
        return column;
    }

    /**
     * @return the tables
     */
    public List<Table> getTables()
    {
        return this.tables;
    }

    /**
     * @param tables the tables to set
     */
    public void setTables(List<Table> tables)
    {
        this.tables = tables;
    }

    /**
     * @return the columns
     */
    public List<Column> getColumns()
    {
        return this.columns;
    }

    /**
     * @param columns the columns to set
     */
    public void setColumns(List<Column> columns)
    {
        this.columns = columns;
    }

    /**
     * @return the indexes
     */
    public List<Index> getIndexes()
    {
        return this.indexes;
    }

    /**
     * @param indexes the indexes to set
     */
    public void setIndexes(List<Index> indexes)
    {
        this.indexes = indexes;
    }

    /**
     * @return the foreignKeys
     */
    public List<ForeignKey> getForeignKeys()
    {
        return this.foreignKeys;
    }

    /**
     * @param foreignKeys the foreignKeys to set
     */
    public void setForeignKeys(List<ForeignKey> foreignKeys)
    {
        this.foreignKeys = foreignKeys;
    }

    /**
     * @return the sequences
     */
    public List<Sequence> getSequences()
    {
        return this.sequences;
    }

    /**
     * @param sequences the sequences to set
     */
    public void setSequences(List<Sequence> sequences)
    {
        this.sequences = sequences;
    }

    /**
     * @return the schema
     */
    public Schema getSchema()
    {
        return this.schema;
    }

    /**
     * @param schema the schema to set
     */
    public void setSchema(Schema schema)
    {
        this.schema = schema;
    }

    /**
     * @return the metadata
     */
    public DatabaseMetaData getMetadata()
    {
        return this.metadata;
    }

    /**
     * @param metadata the metadata to set
     */
    public void setMetadata(DatabaseMetaData metadata)
    {
        this.metadata = metadata;
    }

    /**
     * @return the tableNamePattern
     */
    public String getTableNamePattern()
    {
        return this.tableNamePattern;
    }

    /**
     * @param tableNamePattern the tableNamePattern to set
     */
    public void setTableNamePattern(String tableNamePattern)
    {
        this.tableNamePattern = tableNamePattern;
    }

    /**
     * @return the tableNameExcludePattern
     */
    public String getTableNameExcludePattern()
    {
        return this.tableNameExcludePattern;
    }

    /**
     * @param tableNameExcludePattern the tableNameExcludePattern to set
     */
    public void setTableNameExcludePattern(String tableNameExcludePattern)
    {
        this.tableNameExcludePattern = tableNameExcludePattern;
    }

    /**
     * @return the columnNamePattern
     */
    public String getColumnNamePattern()
    {
        return this.columnNamePattern;
    }

    /**
     * @param columnNamePattern the columnNamePattern to set
     */
    public void setColumnNamePattern(String columnNamePattern)
    {
        this.columnNamePattern = columnNamePattern;
    }

    /**
     * @return the columnNameExcludePatterns
     */
    public List<String> getColumnNameExcludePatterns()
    {
        return this.columnNameExcludePatterns;
    }

    /**
     * @param columnNameExcludePattern the columnNameExcludePattern to set
     */
    public void setColumnNameExcludePattern(String columnNameExcludePattern)
    {
        this.columnNameExcludePattern = columnNameExcludePattern;
    }

    /**
     * Log output for each table
     * @param startTime
     */
    public void logResults(long startTime)
    {
        int excludedCount = 0;
        for (Table table : this.tables)
        {
            LOGGER.info("Table " + table.getName() + (table.isExcluded() ? " excluded" : " class=" + table.getUmlName()));
            LOGGER.info("Columns=" + table.getColumns().size() + " Indexes=" + table.getIndexes().size() + " PKColumns=" + table.getPkColumns().size() + " FKs=" + table.getForeignKeys().size());
            if (table.isExcluded())
            {
                excludedCount++;
            }
        }
        LOGGER.info("Completed " + this.tables.size() + " tables Excluded " + excludedCount + " in " +
            ((System.currentTimeMillis() - startTime) / 1000.0) + "[s]");
    }

    /**
     * @return the columnNameIncludePatterns
     */
    public List<String> getColumnNameIncludePatterns()
    {
        return this.columnNameIncludePatterns;
    }

    /**
     * @param columnNameIncludePatterns the columnNameIncludePatterns to set
     */
    public void setColumnNameIncludePatterns(List<String> columnNameIncludePatterns)
    {
        this.columnNameIncludePatterns = columnNameIncludePatterns;
    }

    /**
     * @return the tableNameIncludePatterns
     */
    public List<String> getTableNameIncludePatterns()
    {
        return this.tableNameIncludePatterns;
    }

    /**
     * @param tableNameIncludePatterns the tableNameIncludePatterns to set
     */
    public void setTableNameIncludePatterns(List<String> tableNameIncludePatterns)
    {
        this.tableNameIncludePatterns = tableNameIncludePatterns;
    }

    /**
     * @return the tableNameExcludePatterns
     */
    public List<String> getTableNameExcludePatterns()
    {
        return this.tableNameExcludePatterns;
    }

    /**
     * @param tableNameExcludePatterns the tableNameExcludePatterns to set
     */
    public void setTableNameExcludePatterns(List<String> tableNameExcludePatterns)
    {
        this.tableNameExcludePatterns = tableNameExcludePatterns;
    }

    /**
     * @return the columnNameExcludePattern
     */
    public String getColumnNameExcludePattern()
    {
        return this.columnNameExcludePattern;
    }

    /**
     * @param columnNameExcludePatterns the columnNameExcludePatterns to set
     */
    public void setColumnNameExcludePatterns(List<String> columnNameExcludePatterns)
    {
        this.columnNameExcludePatterns = columnNameExcludePatterns;
    }

    /**
     * @return the jdbcDriver
     */
    public String getJdbcDriver()
    {
        return this.jdbcDriver;
    }

    /**
     * @param jdbcDriver the jdbcDriver to set
     */
    public void setJdbcDriver(String jdbcDriver)
    {
        this.jdbcDriver = jdbcDriver;
    }

    /**
     * @return the jdbcConnectionUrl
     */
    public String getJdbcConnectionUrl()
    {
        return this.jdbcConnectionUrl;
    }

    /**
     * @param jdbcConnectionUrl the jdbcConnectionUrl to set
     */
    public void setJdbcConnectionUrl(String jdbcConnectionUrl)
    {
        this.jdbcConnectionUrl = jdbcConnectionUrl;
    }

    /**
     * @return the jdbcUser
     */
    public String getJdbcUser()
    {
        return this.jdbcUser;
    }

    /**
     * @param jdbcUser the jdbcUser to set
     */
    public void setJdbcUser(String jdbcUser)
    {
        this.jdbcUser = jdbcUser;
    }

    /**
     * @return the jdbcPassword
     */
    public String getJdbcPassword()
    {
        return this.jdbcPassword;
    }

    /**
     * @param jdbcPassword the jdbcPassword to set
     */
    public void setJdbcPassword(String jdbcPassword)
    {
        this.jdbcPassword = jdbcPassword;
    }

    /**
     * @return TypeMappingsUri
     */
    public String getTypeMappingsUri()
    {
        return this.typeMappingsUri;
    }

    /**
     * @param typeMappingsUri
     */
    public void setTypeMappingsUri(String typeMappingsUri)
    {
        this.typeMappingsUri = typeMappingsUri;
    }

    /**
     * @return WrapperMappingsUri
     */
    public String getWrapperMappingsUri()
    {
        return this.wrapperMappingsUri;
    }

    /**
     * @param wrapperMappingsUri
     */
    public void setWrapperMappingsUri(String wrapperMappingsUri)
    {
        this.wrapperMappingsUri = wrapperMappingsUri;
    }

    /**
     * @param expressionMappingsUri
     */
    public void setExpressionMappingsUri(String expressionMappingsUri)
    {
        this.expressionMappingsUri = expressionMappingsUri;
    }
}