/*
// This software is subject to the terms of the Eclipse Public License v1.0
// Agreement, available at the following URL:
// http://www.eclipse.org/legal/epl-v10.html.
// You must accept the terms of that agreement to use this software.
//
// Copyright (C) 2006-2011 Pentaho and others
// Copyright (C) 2006-2007 Cincom Systems, Inc.
// Copyright (C) 2006-2007 JasperSoft
// All Rights Reserved.
*/

package mondrian.gui;

import org.apache.log4j.Logger;

import java.sql.*;
import java.util.*;

/**
 */
public class JdbcMetaData {

    private static final Logger LOGGER = Logger.getLogger(JdbcMetaData.class);

    // E.g. "org.postgresql.Driver"
    String jdbcDriverClassName = null;

    // E.g. "jdbc:postgresql://localhost:5432/hello?user=postgres&password=post"
    String jdbcUsername = null;

    String jdbcConnectionUrl = null;
    String jdbcPassword = null;
    String jdbcSchema = null;
    boolean requireSchema = false;

    Connection conn = null;
    DatabaseMetaData md = null;

    Workbench workbench;

    public static final String LEVEL_SEPARATOR = "->";

    private String errMsg = null;
    private Database db = new Database();

    public JdbcMetaData(
        Workbench wb,
        String jdbcDriverClassName,
        String jdbcConnectionUrl,
        String jdbcUsername,
        String jdbcPassword,
        String jdbcSchema,
        boolean requireSchema)
    {
        this.workbench = wb;
        this.jdbcConnectionUrl = jdbcConnectionUrl;
        this.jdbcDriverClassName = jdbcDriverClassName;
        this.jdbcUsername = jdbcUsername;
        this.jdbcPassword = jdbcPassword;
        this.jdbcSchema = jdbcSchema;
        this.requireSchema = requireSchema;

        if (initConnection() == null) {
            setAllSchemas();
            closeConnection();
        }
    }

    public boolean getRequireSchema() {
        return requireSchema;
    }

    /**
     * @return the workbench i18n converter
     */
    public I18n getResourceConverter() {
        return workbench.getResourceConverter();
    }

    /**
     * Tests database connection. Called from Preferences dialog button test
     * connection.
     */
    public JdbcMetaData(
        String jdbcDriverClassName,
        String jdbcConnectionUrl,
        String jdbcUsername,
        String jdbcPassword)
    {
        this.jdbcConnectionUrl = jdbcConnectionUrl;
        this.jdbcDriverClassName = jdbcDriverClassName;
        this.jdbcUsername = jdbcUsername;
        this.jdbcPassword = jdbcPassword;

        if (initConnection() == null) {
            closeConnection();
        }
    }

    /* Creates a database connection and initializes the meta data details */
    public String initConnection() {
        LOGGER.debug("JdbcMetaData: initConnection");

        try {
            if (jdbcDriverClassName == null
                || jdbcDriverClassName.trim().length() == 0
                || jdbcConnectionUrl == null
                || jdbcConnectionUrl.trim().length() == 0)
            {
                errMsg = getResourceConverter().getFormattedString(
                    "jdbcMetaData.blank.exception",
                    "Driver={0}\nConnection URL={1}\nUse Preferences to set Database Connection parameters first and then open a Schema",
                    jdbcDriverClassName,
                    jdbcConnectionUrl);
                return errMsg;
            }

            Class.forName(jdbcDriverClassName);

            if (jdbcUsername != null && jdbcUsername.length() > 0) {
                conn = DriverManager.getConnection(
                    jdbcConnectionUrl, jdbcUsername, jdbcPassword);
            } else {
                conn = DriverManager.getConnection(jdbcConnectionUrl);
            }

            LOGGER.debug("JDBC connection OPEN");
            md = conn.getMetaData();

            db.productName = md.getDatabaseProductName();
            db.productVersion = md.getDatabaseProductVersion();
            db.catalogName = conn.getCatalog();

            LOGGER.debug("Catalog name = " + db.catalogName);
            LOGGER.debug("Database Product Name: " + db.productName);
            LOGGER.debug("Database Product Version: " + db.productVersion);
            LOGGER.debug("JdbcMetaData: initConnection - no error");
            return null;
        } catch (Exception e) {
            errMsg =
                e.getClass().getSimpleName() + " : " + e.getLocalizedMessage();
            LOGGER.error("Database connection exception : " + errMsg, e);
            return errMsg;
            //e.printStackTrace();
        }
    }

    public void closeConnection() {
        if (conn == null) {
            return;
        }

        md = null;
        try {
            conn.close();
            LOGGER.debug("JDBC connection CLOSE");
        } catch (Exception e) {
            LOGGER.error(e);
        }
        conn = null;
    }

    /**
     * Check to see if the schemaName is in the list of allowed jdbc schemas
     *
     * @param schemaName the name of the schmea
     * @return true if found, or if jdbcSchema is null
     */
    private boolean inJdbcSchemas(String schemaName) {
        if (jdbcSchema == null || jdbcSchema.trim().length() == 0) {
            return true;
        }

        String schemas[] = jdbcSchema.split("[,;]");
        for (String schema : schemas) {
            if (schema.trim().equalsIgnoreCase(schemaName)) {
                return true;
            }
        }

        return false;
    }

    /* list all schemas in the currently connected database */
    public List<String> listAllSchemas() {
        LOGGER.debug("JdbcMetaData: listAllSchemas");

        if (initConnection() != null) {
            return null;
        }

        List<String> schemaNames = new ArrayList<String>();
        ResultSet rs = null;
        try {
            rs = md.getSchemas();

            while (rs.next()) {
                String schemaName = rs.getString("TABLE_SCHEM");
                schemaNames.add(schemaName);
            }
        } catch (Exception e) {
            LOGGER.debug(
                "Exception : Database does not support schemas." + e
                    .getMessage());
            return null;
        } finally {
            try {
                rs.close();
                closeConnection();
            } catch (Exception e) {
                // ignore
            }
        }


        return schemaNames;
    }

    /* set all schemas in the currently connected database */
    private void setAllSchemas() {
        LOGGER.debug("JdbcMetaData: setAllSchemas");

        ResultSet rs = null;
        boolean gotSchema = false;
        try {
            rs = md.getSchemas();
            while (rs.next()) {
                String schemaName = rs.getString("TABLE_SCHEM");
                if (inJdbcSchemas(schemaName)) {
                    DbSchema dbs = new DbSchema();
                    dbs.name = schemaName;
                    LOGGER.debug("JdbcMetaData: setAllTables - " + dbs.name);
                    setAllTables(dbs);
                    db.addDbSchema(dbs);
                    gotSchema = true;
                }
            }
        } catch (Exception e) {
            LOGGER.debug(
                "Exception : Database does not support schemas." + e
                    .getMessage());
        } finally {
            try {
                rs.close();
            } catch (Exception e) {
                // ignore
            }
        }

        if (!gotSchema) {
            LOGGER.debug(
                "JdbcMetaData: setAllSchemas - tables with no schema name");
            DbSchema dbs = new DbSchema();
            dbs.name = null;    //tables with no schema name
            setAllTables(dbs);
            db.addDbSchema(dbs);
        }
    }

    /* set all tables in the currently connected database */
    private void setAllTables(DbSchema dbs) {
        LOGGER.debug("JdbcMetaData: Loading schema: '" + dbs.name + "'");
        ResultSet rs = null;
        try {
            // Tables and views can be used
            try {
                rs = md.getTables(
                    null, dbs.name, null, new String[]{"TABLE", "VIEW"});
            } catch (Exception e) {
                // this is a workaround for databases that throw an exception
                // when views are requested.
                rs = md.getTables(null, dbs.name, null, new String[]{"TABLE"});
            }
            while (rs.next()) {
                // Oracle 10g Driver returns bogus BIN$ tables that cause
                // exceptions
                String tbname = rs.getString("TABLE_NAME");
                if (!tbname.matches("(?!BIN\\$).+")) {
                    continue;
                }

                DbTable dbt = null;

                /* Note: Imported keys are foreign keys which are primary keys
                 * of in some other tables; Exported keys are primary keys which
                 * are referenced as foreign keys in other tables.
                 */
                try {
                    ResultSet rs_fks = md.getImportedKeys(null, dbs.name, tbname);
                    try {
                        if (rs_fks.next()) {
                            dbt = new FactTable();
                            do {
                                ((FactTable) dbt).addFks(
                                    rs_fks.getString("FKCOLUMN_NAME"),
                                    rs_fks.getString("pktable_name"));
                            } while (rs_fks.next());
                        } else {
                            dbt = new DbTable();
                        }
                    } finally {
                        try {
                            rs_fks.close();
                        } catch (Exception e) {
                            // ignore
                        }
                    }
                } catch (Exception e) {
                  // this fails in some cases (Redshift)
                  LOGGER.warn("unable to process foreign keys", e);
                  if (dbt == null) {
                    dbt = new FactTable();
                  }
                }
                dbt.schemaName = dbs.name;
                dbt.name = tbname;
                setPKey(dbt);
                // Lazy loading
                // setColumns(dbt);
                dbs.addDbTable(dbt);
                db.addDbTable(dbt);
            }
        } catch (Exception e) {
            LOGGER.error("setAllTables", e);
        } finally {
            try {
                rs.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    /**
     * Gets the Primary key name for a given table name.
     * This key may be a  composite key made of multiple columns.
     */
    private void setPKey(DbTable dbt) {
        ResultSet rs = null;
        try {
            rs = md.getPrimaryKeys(null, dbt.schemaName, dbt.name);
            if (rs.next()) {
                //   // a column may have been given a primary key name
                //===dbt.pk = rs.getString("PK_NAME");
                // We need the column name which is primary key for the given
                // table.
                dbt.pk = rs.getString("column_name");
            }
        } catch (Exception e) {
            LOGGER.error("setPKey", e);
        } finally {
            try {
                rs.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    private void setColumns(String schemaName, String tableName) {
        LOGGER.debug(
            "setColumns: <" + tableName + "> in schema <" + schemaName + ">");
        DbTable dbt = db.getTable(schemaName, tableName);
        if (dbt == null) {
            LOGGER.debug(
                "No table with name: <"
                + tableName
                + "> in schema <"
                + schemaName
                + ">");
            return;
        }
        if (initConnection() != null) {
            return;
        }
        try {
            setColumns(dbt);
            LOGGER.debug("got " + dbt.colsDataType.size() + " columns");
        } finally {
            closeConnection();
        }
    }

    /**
     * Gets all columns for a given table name.
     *
     * Assumes that the caller has acquired a connection using
     * {@link #initConnection()}.
     */
    private void setColumns(DbTable dbt) {
        ResultSet rs = null;
        try {
            rs = md.getColumns(null, dbt.schemaName, dbt.name, null);
            while (rs.next()) {
                DbColumn col = new DbColumn();

                col.dataType = rs.getInt("DATA_TYPE");
                col.name = rs.getString("COLUMN_NAME");
                col.typeName = rs.getString("TYPE_NAME");
                col.columnSize = rs.getInt("COLUMN_SIZE");
                col.decimalDigits = rs.getInt("DECIMAL_DIGITS");

                dbt.addColsDataType(col);
            }
        } catch (Exception e) {
            LOGGER.error("setColumns", e);
        } finally {
            try {
                rs.close();
            } catch (Exception e) {
                // ignore
            }
        }
    }

    ////////////////////////////////////////////////////////////////////////////
    // The following functions provide an interface to JdbcMetaData class to
    // retrieve the meta data details

    public List<String> getAllSchemas() {
        return db.getAllSchemas();
    }

    /**
     * Returns all tables in a given schema.
     */
    public List<String> getAllTables(String schemaName) {
        return db.getAllTables(schemaName);
    }

    /**
     * Returns all tables in given schema minus the given table name.
     */
    public List<String> getAllTables(String schemaName, String minusTable) {
        if (minusTable == null) {
            return getAllTables(schemaName);
        } else {
            List<String> allTablesMinusOne = new ArrayList<String>();
            for (String s : getAllTables(schemaName)) {
                if (s.endsWith(minusTable)) {
                    // startsWith and endsWith cannot be compared with
                    // null argument, throws exception
                    if ((schemaName == null) || s.startsWith(schemaName)) {
                        continue;
                    }
                }
                allTablesMinusOne.add(s);
            }
            return allTablesMinusOne;
        }
    }

    /* get all possible cases of fact tables in a schema */
    public List<String> getFactTables(String schemaName) {
        return db.getFactTables(schemaName);
    }

    /**
     * Gets all possible cases of dimension tables which are linked to given
     * fact table by foreign keys.
     */
    public List<String> getDimensionTables(
        String schemaName,
        String factTable)
    {
        List<String> dimeTables = new ArrayList<String>();
        if (factTable == null) {
            return dimeTables;
        } else {
            return db.getDimensionTables(schemaName, factTable);
        }
    }

    public boolean isTableExists(String schemaName, String tableName) {
        if (tableName == null) {
            return true;
        } else {
            return db.tableExists(schemaName, tableName);
        }
    }

    public boolean isColExists(
        String schemaName, String tableName, String colName)
    {
        if (tableName == null || colName == null) {
            return true;
        } else {
            if (!db.hasColumns(schemaName, tableName)) {
                setColumns(schemaName, tableName);
            }

            return db.colExists(schemaName, tableName, colName);
        }
    }

    /* get all foreign keys in given fact table */
    public List<String> getFactTableFKs(String schemaName, String factTable) {
        List<String> fks = new ArrayList<String>();
        if (factTable == null) {
            return fks;
        } else {
            return db.getFactTableFKs(schemaName, factTable);
        }
    }

    public String getTablePK(String schemaName, String tableName) {
        if (tableName == null) {
            return null;
        } else {
            return db.getTablePK(schemaName, tableName);
        }
    }

    /**
     * Gets all columns of given table in schema.
     * column string is formatted.
     */
    public List<String> getAllColumns(String schemaName, String tableName) {
        List<String> allcols = new ArrayList<String>();

        if (tableName == null) {
            List<String> allTables = getAllTables(schemaName);

            for (int i = 0; i < allTables.size(); i++) {
                String tab = allTables.get(i);
                List<String> cols;
                if (tab.indexOf(LEVEL_SEPARATOR) == -1) {
                    cols = getAllColumns(schemaName, tab);
                } else {
                    String[] names = tab.split(LEVEL_SEPARATOR);
                    cols = getAllColumns(names[0], names[1]);
                }
                for (int j = 0; j < cols.size(); j++) {
                    String col = cols.get(j);
                    allcols.add(tab + LEVEL_SEPARATOR + col);
                }
            }
            return allcols;
        } else {
            if (!db.hasColumns(schemaName, tableName)) {
                setColumns(schemaName, tableName);
            }
            return db.getAllColumns(schemaName, tableName);
        }
    }

    /**
     * Returns all columns of given table in schema.
     * Column string is formatted.
     */
    public List<DbColumn> getAllDbColumns(String schemaName, String tableName) {
        List<DbColumn> allcols = new ArrayList<DbColumn>();

        if (tableName == null) {
            List<String> allTables = getAllTables(schemaName);

            for (int i = 0; i < allTables.size(); i++) {
                String tab = allTables.get(i);
                List<DbColumn> cols;
                if (tab.indexOf(LEVEL_SEPARATOR) == -1) {
                    cols = getAllDbColumns(schemaName, tab);
                } else {
                    String[] names = tab.split(LEVEL_SEPARATOR);
                    cols = getAllDbColumns(names[0], names[1]);
                }
                allcols.addAll(cols);
            }
            return allcols;
        } else {
            if (!db.hasColumns(schemaName, tableName)) {
                setColumns(schemaName, tableName);
            }
            return db.getAllDbColumns(schemaName, tableName);
        }
    }

    // get column data type of given table and its col
    public int getColumnDataType(
        String schemaName, String tableName, String colName)
    {
        if (tableName == null || colName == null) {
            return -1;
        } else {
            if (!db.hasColumns(schemaName, tableName)) {
                setColumns(schemaName, tableName);
            }
            return db.getColumnDataType(schemaName, tableName, colName);
        }
    }

    /**
     * Gets column definition of given table and its col.
     *
     * @param schemaName Schema name
     * @param tableName Table name
     * @param colName Column name
     * @return Column definition
     */
    public DbColumn getColumnDefinition(
        String schemaName, String tableName, String colName)
    {
        if (tableName == null || colName == null) {
            return null;
        } else {
            if (!db.hasColumns(schemaName, tableName)) {
                setColumns(schemaName, tableName);
            }
            return db.getColumnDefinition(schemaName, tableName, colName);
        }
    }

    public String getDbCatalogName() {
        return db.catalogName;
    }

    public String getDatabaseProductName() {
        return db.productName;
    }

    public String getJdbcConnectionUrl() {
        return jdbcConnectionUrl;
    }

    public String getErrMsg() {
        return errMsg;
    }

    public static void main(String[] args) {
        if (args.length < 2) {
            throw new RuntimeException(
                "need at least 2 args: driver class and jdbcUrl");
        }

        String driverClass = args[0];
        String jdbcUrl = args[1];

        String username = null;
        String password = null;

        if (args.length > 2) {
            if (args.length != 4) {
                throw new RuntimeException(
                    "need 4 args: including user name and password");
            }
            username = args[2];
            password = args[3];
        }

        JdbcMetaData sb = new JdbcMetaData(
            null, driverClass, jdbcUrl, username, password, "", false);

        List<String> foundSchemas = sb.getAllSchemas();
        System.out.println("allSchemas = " + foundSchemas);

        for (String schemaName : foundSchemas) {
            List<String> foundTables = sb.getAllTables(schemaName);

            if (foundTables != null && foundTables.size() > 0) {
                System.out.println("schema = " + schemaName);
                for (String tableName : foundTables) {
                    System.out.println("\t" + tableName);

                    List<String> foundColumns = sb.getAllColumns(
                        schemaName, tableName);

                    for (String columnName : foundColumns) {
                        System.out.println("\t\t" + columnName);
                    }
                }
            }
        }
    }

    /**
     * Database metadata.
     */
    class Database {
        String catalogName = ""; // database name.
        String productName = "Unknown";
        String productVersion = "";

        // list of all schemas in database
        Map<String, DbSchema> schemas = new TreeMap<String, DbSchema>();
            //ordered collection, allows duplicates and null
        Map<String, TableTracker> tables = new TreeMap<String, TableTracker>();
            // list of all tables in all schemas in database

        List<String> allSchemas;

        private void addDbSchema(DbSchema dbs) {
            schemas.put(
                dbs.name != null
                    ? dbs.name
                    : "", dbs);
        }

        class TableTracker {
            List<DbTable> namedTable = new ArrayList<DbTable>();

            public void add(DbTable table) {
                namedTable.add(table);
            }

            public int count() {
                return namedTable.size();
            }
        }

        private void addDbTable(DbTable dbs) {
            TableTracker tracker = tables.get(dbs.name);

            if (tracker == null) {
                tracker = new TableTracker();
                tables.put(dbs.name, tracker);
            }
            tracker.add(dbs);
        }

        private boolean schemaNameEquals(String a, String b) {
            return (a != null && a.equals(b));
        }

        private DbSchema getSchema(String schemaName) {
            return schemas.get(
                schemaName != null
                    ? schemaName
                    : "");
        }

        private List<String> getAllSchemas() {
            if (allSchemas == null) {
                allSchemas = new ArrayList<String>();

                allSchemas.addAll(schemas.keySet());
            }
            return allSchemas;
        }

        private boolean tableExists(String sname, String tableName) {
            return getTable(sname, tableName) != null;
        }

        private DbTable getTable(String sname, String tableName) {
            if (sname == null || sname.equals("")) {
                TableTracker t = tables.get(tableName);
                if (t != null) {
                    return t.namedTable.get(0);
                } else {
                    return null;
                }
            } else {
                DbSchema s = schemas.get(sname);

                if (s == null) {
                    return null;
                }

                return s.getTable(tableName);
            }
        }

        private boolean hasColumns(String schemaName, String tableName) {
            DbTable table = getTable(schemaName, tableName);
            if (table != null) {
                return table.hasColumns();
            }
            return false;
        }

        private boolean colExists(
            String sname, String tableName, String colName)
        {
            DbTable t = getTable(sname, tableName);

            if (t == null) {
                return false;
            }

            return t.getColumn(colName) != null;
        }

        private List<String> getAllTables(String sname) {
            return getAllTables(sname, false);
        }

        private List<String> getFactTables(String sname) {
            return getAllTables(sname, true);
        }

        private List<String> getAllTables(String sname, boolean factOnly) {
            List<String> v = new ArrayList<String>();

            if (sname == null || sname.equals("")) {
                // return a list of "schemaname -> table name" string objects
                for (TableTracker tt : tables.values()) {
                    for (DbTable t : tt.namedTable) {
                        if (!factOnly || (factOnly && t instanceof FactTable)) {
                            if (t.schemaName == null) {
                                v.add(t.name);
                            } else {
                                v.add(t.schemaName + LEVEL_SEPARATOR + t.name);
                            }
                        }
                    }
                }
            } else {
                // return a list of "tablename" string objects
                DbSchema s = getSchema(sname);

                if (s != null) {
                    for (DbTable t : s.tables.values()) {
                        if (!factOnly || (factOnly && t instanceof FactTable)) {
                            v.add(t.name);
                        }
                    }
                }
            }
            return v;
        }

        /* get all foreign keys in given fact table */
        private List<String> getFactTableFKs(String sname, String factTable) {
            List<String> f = new ArrayList<String>();

            if (sname == null || sname.equals("")) {
                TableTracker tracker = tables.get(factTable);

                if (tracker == null) {
                    return f;
                }

                // return a list of "schemaname -> table name -> fk col" string
                // objects if schema is not given
                boolean duplicate = tracker.count() > 1;

                for (DbTable t : tracker.namedTable) {
                    if (t instanceof FactTable) {
                        if (duplicate) {
                            for (String fk : ((FactTable) t).fks.keySet()) {
                                if (t.schemaName == null) {
                                    f.add(t.name + LEVEL_SEPARATOR + fk);
                                } else {
                                    f.add(
                                        t.schemaName
                                        + LEVEL_SEPARATOR
                                        + t.name
                                        + LEVEL_SEPARATOR
                                        + fk);
                                }
                            }
                        } else {
                            f.addAll(((FactTable) t).fks.keySet());
                        }
                    }
                }
            } else {
                DbSchema s = getSchema(sname);

                if (s == null) {
                    return f;
                }

                DbTable t = s.getTable(factTable);

                if (t == null) {
                    return f;
                }

                // return a list of "fk col name" string objects if schema is
                // given
                if (t instanceof FactTable && t.name.equals(factTable)) {
                    f.addAll(((FactTable) t).fks.keySet());
                }
            }
            return f;
        }

        private List<String> getDimensionTables(
            String sname, String factTable)
        {
            List<String> f = new ArrayList<String>();

            if (sname == null || sname.equals("")) {
                TableTracker tracker = tables.get(factTable);

                if (tracker == null) {
                    return f;
                }

                // return a list of "schemaname -> table name -> fk col" string
                // objects if schema is not given
                boolean duplicate = tracker.count() > 1;

                for (DbTable t : tracker.namedTable) {
                    if (t instanceof FactTable) {
                        if (duplicate) {
                            for (String fkt : ((FactTable) t).fks.values()) {
                                if (t.schemaName == null) {
                                    f.add(t.name + LEVEL_SEPARATOR + fkt);
                                } else {
                                    f.add(
                                        t.schemaName
                                        + LEVEL_SEPARATOR
                                        + t.name
                                        + LEVEL_SEPARATOR
                                        + fkt);
                                }
                            }
                        } else {
                            f.addAll(((FactTable) t).fks.keySet());
                        }
                    }
                }
            } else {
                DbSchema s = getSchema(sname);

                if (s == null) {
                    return f;
                }

                DbTable t = s.getTable(factTable);

                if (t == null) {
                    return f;
                }

                // return a list of "fk col name" string objects if schema is
                // given
                if (t instanceof FactTable && t.name.equals(factTable)) {
                    f.addAll(((FactTable) t).fks.values());
                }
            }
            return f;
        }

        private String getTablePK(String sname, String tableName) {
            if (sname == null || sname.equals("")) {
                TableTracker tracker = tables.get(tableName);

                if (tracker == null) {
                    return null;
                }

                // return a list of "schemaname -> table name ->
                // dimension table name" string objects if schema is not given
                return tracker.namedTable.get(0).pk;
            } else {
                DbTable t = getTable(sname, tableName);

                if (t == null) {
                    return null;
                }

                return t.pk;
            }
        }

        private List<String> getAllColumns(String sname, String tableName) {
            List<String> f = new ArrayList<String>();

            if (sname == null || sname.equals("")) {
                TableTracker tracker = tables.get(tableName);

                if (tracker == null) {
                    return f;
                }

                // return a list of "schemaname -> table name -> cols"
                // string objects if schema is not given
                boolean duplicate = tracker.count() > 1;

                for (DbTable t : tracker.namedTable) {
                    for (Map.Entry<String, DbColumn> c : t.colsDataType
                        .entrySet())
                    {
                        StringBuffer sb = new StringBuffer();

                        if (t.schemaName != null && !duplicate) {
                            sb.append(t.schemaName).append(LEVEL_SEPARATOR);
                        }
                        sb.append(t.name)
                            .append(LEVEL_SEPARATOR)
                            .append(c.getKey())
                            .append(" - ")
                            .append(c.getValue().displayType());

                        f.add(sb.toString());
                    }
                }
            } else {
                DbTable t = getTable(sname, tableName);

                if (t == null) {
                    return f;
                }
                // return a list of "col name" string objects if schema is given
                f.addAll(t.colsDataType.keySet());
            }
            return f;
        }

        private List<DbColumn> getAllDbColumns(String sname, String tableName) {
            List<DbColumn> f = new ArrayList<DbColumn>();

            if (sname == null || sname.equals("")) {
                TableTracker tracker = tables.get(tableName);

                if (tracker == null) {
                    return f;
                }

                for (DbTable t : tracker.namedTable) {
                    for (Map.Entry<String, DbColumn> c : t.colsDataType
                        .entrySet())
                    {
                        f.add(c.getValue());
                    }
                }
            } else {
                DbTable t = getTable(sname, tableName);

                if (t == null) {
                    return f;
                }

                for (Map.Entry<String, DbColumn> c : t.colsDataType
                    .entrySet())
                {
                    f.add(c.getValue());
                }
            }
            return f;
        }

        private int getColumnDataType(
            String sname, String tableName, String colName)
        {
            DbColumn result = getColumnDefinition(sname, tableName, colName);

            if (result == null) {
                return -1;
            }

            return result.dataType;
        }

        private DbColumn getColumnDefinition(
            String sname, String tableName, String colName)
        {
            DbTable t = getTable(sname, tableName);

            if (t == null) {
                return null;
            }
            return t.colsDataType.get(colName);
        }
    }

    class DbSchema {
        String name;
        /**
         * ordered collection, allows duplicates and null
         */
        final Map<String, DbTable> tables = new TreeMap<String, DbTable>();

        private DbTable getTable(String tableName) {
            return tables.get(tableName);
        }

        private void addDbTable(DbTable dbt) {
            tables.put(dbt.name, dbt);
        }
    }

    public class DbColumn {
        public String name;
        public int dataType;
        public String typeName;
        public int columnSize;
        public int decimalDigits;

        public String displayType() {
            StringBuffer sb = new StringBuffer();
            switch (dataType) {
            case Types.ARRAY:
                sb.append("ARRAY(" + columnSize + ")");
                break;
            case Types.BIGINT:
                sb.append("BIGINT");
                break;
            case Types.BINARY:
                sb.append("BINARY(" + columnSize + ")");
                break;
            case Types.BLOB:
                sb.append("BLOB(" + columnSize + ")");
                break;
            case Types.BIT:
                sb.append("BIT");
                break;
            case Types.BOOLEAN:
                sb.append("BOOLEAN");
                break;
            case Types.CHAR:
                sb.append("CHAR");
                break;
            case Types.CLOB:
                sb.append("CLOB(" + columnSize + ")");
                break;
            case Types.DATE:
                sb.append("DATE");
                break;
            case Types.DECIMAL:
                sb.append("DECIMAL(" + columnSize + ", " + decimalDigits + ")");
                break;
            case Types.DISTINCT:
                sb.append("DISTINCT");
                break;
            case Types.DOUBLE:
                sb.append("DOUBLE(" + columnSize + ", " + decimalDigits + ")");
                break;
            case Types.FLOAT:
                sb.append("FLOAT(" + columnSize + ", " + decimalDigits + ")");
                break;
            case Types.INTEGER:
                sb.append("INTEGER(" + columnSize + ")");
                break;
            case Types.JAVA_OBJECT:
                sb.append("JAVA_OBJECT(" + columnSize + ")");
                break;
            /*
             * No Java 1.6 SQL types for now
            case Types.LONGNVARCHAR:
                sb.append("LONGNVARCHAR(" + columnSize + ")");
                break;
            case Types.LONGVARBINARY:
                sb.append("LONGVARBINARY(" + columnSize + ")");
                break;
            case Types.LONGVARCHAR:
                sb.append("LONGVARCHAR(" + columnSize + ")");
                break;
            case Types.NCHAR:
                sb.append("NCHAR(" + columnSize + ")");
                break;
            case Types.NCLOB:
                sb.append("NCLOB(" + columnSize + ")");
                break;
             */
            case Types.NULL:
                sb.append("NULL");
                break;
            case Types.NUMERIC:
                sb.append("NUMERIC(" + columnSize + ", " + decimalDigits + ")");
                break;
            /*
             * No Java 1.6 SQL types for now
            case Types.NVARCHAR:
                sb.append("NCLOB(" + columnSize + ")");
                break;
             */
            case Types.OTHER:
                sb.append("OTHER");
                break;
            case Types.REAL:
                sb.append("REAL(" + columnSize + ", " + decimalDigits + ")");
                break;
            case Types.REF:
                sb.append("REF");
                break;
            /*
             * No Java 1.6 SQL types for now
            case Types.ROWID:
                sb.append("ROWID");
                break;
             */
            case Types.SMALLINT:
                sb.append("SMALLINT(" + columnSize + ")");
                break;
            /*
             * No Java 1.6 SQL types for now
           case Types.SQLXML:
                sb.append("SQLXML(" + columnSize + ")");
                break;
             */
            case Types.STRUCT:
                sb.append("STRUCT");
                break;
            case Types.TIME:
                sb.append("TIME");
                break;
            case Types.TIMESTAMP:
                sb.append("TIMESTAMP");
                break;
            case Types.TINYINT:
                sb.append("TINYINT(" + columnSize + ")");
                break;
            case Types.VARBINARY:
                sb.append("VARBINARY(" + columnSize + ")");
                break;
            case Types.VARCHAR:
                sb.append("VARCHAR(" + columnSize + ")");
                break;
            }
            return sb.toString();
        }
    }

    class DbTable {
        String schemaName;
        String name;
        String pk;
        /**
         * sorted map key=column, value=data type of column
         */
        final Map<String, DbColumn> colsDataType =
            new TreeMap<String, DbColumn>();

        private void addColsDataType(DbColumn columnDefinition) {
            colsDataType.put(columnDefinition.name, columnDefinition);
        }

        private DbColumn getColumn(String cname) {
            return colsDataType.get(cname);
        }

        private boolean hasColumns() {
            return colsDataType.size() > 0;
        }
    }

    class FactTable extends DbTable {
        /**
         * Sorted map key = foreign key col, value=primary key table associated
         * with this fk.
         */
        final Map<String, String> fks = new TreeMap<String, String>();

        private void addFks(String fk, String pkt) {
            fks.put(fk, pkt);
        }
    }
}

// End JdbcMetaData.java
