public class Database {
    private final String databaseName;
    private final String schema;
    private String description;
    private final Map<String, Table> tables = new CaseInsensitiveMap<Table>();
    private final Map<String, View> views = new CaseInsensitiveMap<View>();
    private final Map<String, Table> remoteTables = new CaseInsensitiveMap<Table>(); // key: schema.tableName value: RemoteTable
    private final DatabaseMetaData meta;
    private final Connection connection;
    private final String connectTime = new SimpleDateFormat("EEE MMM dd HH:mm z yyyy").format(new Date());
    private Set<String> sqlKeywords;
    private Pattern invalidIdentifierPattern;
    private final Logger logger = Logger.getLogger(getClass().getName());
    private final boolean fineEnabled = logger.isLoggable(Level.FINE);

    public Database(Config config, Connection connection, DatabaseMetaData meta, String name, String schema, Properties properties, SchemaMeta schemaMeta) throws SQLException, MissingResourceException {
        this.connection = connection;
        this.meta = meta;
        databaseName = name;
        this.schema = schema;
        description = config.getDescription();

        initTables(meta, properties, config);
        if (config.isViewsEnabled())
            initViews(meta, properties, config);

        initCheckConstraints(properties);
        initTableIds(properties);
        initIndexIds(properties);
        initTableComments(properties);
        initTableColumnComments(properties);
        initViewComments(properties);
        initViewColumnComments(properties);

        connectTables();
        updateFromXmlMetadata(schemaMeta);
    }

    public String getName() {
        return databaseName;
    }

    public String getSchema() {
        return schema;
    }

    /**
     * Details of the database type that's running under the covers.
     *
     * @return null if a description wasn't specified.
     */
    public String getDescription() {
        return description;
    }

    public Collection<Table> getTables() {
        return tables.values();
    }

    /**
     * Return a {@link Map} of all {@link Table}s keyed by their name.
     *
     * @return
     */
    public Map<String, Table> getTablesByName() {
    	return tables;
    }

    public Collection<View> getViews() {
        return views.values();
    }

    public Collection<Table> getRemoteTables() {
        return remoteTables.values();
    }

    public Connection getConnection() {
        return connection;
    }

    public DatabaseMetaData getMetaData() {
        return meta;
    }

    public String getConnectTime() {
        return connectTime;
    }

    public String getDatabaseProduct() {
        try {
            return meta.getDatabaseProductName() + " - " + meta.getDatabaseProductVersion();
        } catch (SQLException exc) {
            return "";
        }
    }

    /**
     *  "macro" to validate that a table is somewhat valid
     */
    class NameValidator {
        private final String clazz;
        private final Pattern include;
        private final Pattern exclude;
        private final Set<String> validTypes;

        /**
         * @param clazz table or view
         * @param include
         * @param exclude
         * @param verbose
         * @param validTypes
         */
        NameValidator(String clazz, Pattern include, Pattern exclude, String[] validTypes) {
            this.clazz = clazz;
            this.include = include;
            this.exclude = exclude;
            this.validTypes = new HashSet<String>();
            for (String type : validTypes)
            {
                this.validTypes.add(type.toUpperCase());
            }
        }

        /**
         * Returns <code>true</code> if the table/view name is deemed "valid"
         *
         * @param name name of the table or view
         * @param type type as returned by metadata.getTables():TABLE_TYPE
         * @return
         */
        boolean isValid(String name, String type) {
            // some databases (MySQL) return more than we wanted
            if (!validTypes.contains(type.toUpperCase()))
                return false;

            // Oracle 10g introduced problematic flashback tables
            // with bizarre illegal names
            // Naming Convention "BIN$"${globalUID}${version}
            // http://docs.oracle.com/cd/B19306_01/backup.102/b14192/flashptr004.htm#i1016977
            if (name.indexOf("BIN$") == 0) {
                if (fineEnabled) {
                    logger.fine("Excluding " + clazz + " " + name +
                                ": \"BIN$\" prefix implies  a (deleted) table in the Oracle Recycle Bin ");
                }
                return false;
            }

            if (exclude.matcher(name).matches()) {
                if (fineEnabled) {
                    logger.fine("Excluding " + clazz + " " + name +
                                ": matches exclusion pattern \"" + exclude + '"');
                }
                return false;
            }

            boolean valid = include.matcher(name).matches();
            if (fineEnabled) {
                if (valid) {
                    logger.fine("Including " + clazz + " " + name +
                                ": matches inclusion pattern \"" + include + '"');
                } else {
                    logger.fine("Excluding " + clazz + " " + name +
                                ": doesn't match inclusion pattern \"" + include + '"');
                }
            }
            return valid;
        }
    }

    /**
     * Create/initialize any tables in the schema.

     * @param metadata
     * @param properties
     * @param config
     * @throws SQLException
     */
    private void initTables(final DatabaseMetaData metadata, final Properties properties,
                            final Config config) throws SQLException {
        final Pattern include = config.getTableInclusions();
        final Pattern exclude = config.getTableExclusions();
        final int maxThreads = config.getMaxDbThreads();

        String[] types = getTypes("tableTypes", "TABLE", properties);
        NameValidator validator = new NameValidator("table", include, exclude, types);
        List<BasicTableMeta> entries = getBasicTableMeta(metadata, true, properties, types);

        TableCreator creator;
        if (maxThreads == 1) {
            creator = new TableCreator();
        } else {
            // creating tables takes a LONG time (based on JProbe analysis),
            // so attempt to speed it up by doing several in parallel.
            // note that it's actually DatabaseMetaData.getIndexInfo() that's expensive

            creator = new ThreadedTableCreator(maxThreads);

            // "prime the pump" so if there's a database problem we'll probably see it now
            // and not in a secondary thread
            while (!entries.isEmpty()) {
                BasicTableMeta entry = entries.remove(0);

                if (validator.isValid(entry.name, entry.type)) {
                    new TableCreator().create(entry, properties);
                    break;
                }
            }
        }

        // kick off the secondary threads to do the creation in parallel
        for (BasicTableMeta entry : entries) {
            if (validator.isValid(entry.name, entry.type)) {
                creator.create(entry, properties);
            }
        }

        // wait for everyone to finish
        creator.join();
    }

    /**
     * Create/initialize any views in the schema.
     *
     * @param metadata
     * @param properties
     * @param config
     * @throws SQLException
     */
    private void initViews(DatabaseMetaData metadata, Properties properties,
                            Config config) throws SQLException {
        Pattern includeTables = config.getTableInclusions();
        Pattern excludeTables = config.getTableExclusions();
        Pattern excludeColumns = config.getColumnExclusions();
        Pattern excludeIndirectColumns = config.getIndirectColumnExclusions();

        String[] types = getTypes("viewTypes", "VIEW", properties);
        NameValidator validator = new NameValidator("view", includeTables, excludeTables, types);

        for (BasicTableMeta entry : getBasicTableMeta(metadata, false, properties, types)) {
            if (validator.isValid(entry.name, entry.type)) {
                View view = new View(this, entry.schema, entry.name, entry.remarks,
                                    entry.viewSql, properties,
                                    excludeIndirectColumns, excludeColumns);
                views.put(view.getName(), view);
                if (logger.isLoggable(Level.FINE)) {
                    logger.fine("Found details of view " + view.getName());
                } else {
                    System.out.print('.');
                }
            }
        }
    }

    /**
     * Collection of fundamental table/view metadata
     */
    private class BasicTableMeta
    {
        @SuppressWarnings("hiding")
        final String schema;
        final String name;
        final String type;
        final String remarks;
        final String viewSql;
        final long numRows;  // -1 if not determined

        /**
         * @param schema
         * @param name
         * @param type typically "TABLE" or "VIEW"
         * @param remarks
         * @param text optional textual SQL used to create the view
         * @param numRows number of rows, or -1 if not determined
         */
        BasicTableMeta(String schema, String name, String type, String remarks, String text, long numRows)
        {
            this.schema = schema;
            this.name = name;
            this.type = type;
            this.remarks = remarks;
            viewSql = text;
            this.numRows = numRows;
        }
    }

    /**
     * Return a list of basic details of the tables in the schema.
     *
     * @param metadata
     * @param forTables true if we're getting table data, false if getting view data
     * @param properties
     * @return
     * @throws SQLException
     */
    private List<BasicTableMeta> getBasicTableMeta(DatabaseMetaData metadata,
                                                    boolean forTables,
                                                    Properties properties,
                                                    String... types) throws SQLException {
        String queryName = forTables ? "selectTablesSql" : "selectViewsSql";
        String sql = properties.getProperty(queryName);
        List<BasicTableMeta> basics = new ArrayList<BasicTableMeta>();
        ResultSet rs = null;

        if (sql != null) {
            String clazz = forTables ? "table" : "view";
            PreparedStatement stmt = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String name = rs.getString(clazz + "_name");
                    String sch = getOptionalString(rs, clazz + "_schema");
                    if (sch == null)
                        sch = schema;
                    String remarks = getOptionalString(rs, clazz + "_comment");
                    String text = forTables ? null : getOptionalString(rs, "view_definition");
                    String rows = forTables ? getOptionalString(rs, "table_rows") : null;
                    long numRows = rows == null ? -1 : Long.parseLong(rows);

                    basics.add(new BasicTableMeta(sch, name, clazz, remarks, text, numRows));
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.out.flush();
                System.err.println();
                System.err.println("Failed to retrieve " + clazz + " names with custom SQL: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }

        if (basics.isEmpty()) {
            rs = metadata.getTables(null, schema, "%", types);

            try {
                while (rs.next()) {
                    String name = rs.getString("TABLE_NAME");
                    String type = rs.getString("TABLE_TYPE");
                    String schem = rs.getString("TABLE_SCHEM");
                    String remarks = getOptionalString(rs, "REMARKS");

                    basics.add(new BasicTableMeta(schem, name, type, remarks, null, -1));
                }
            } catch (SQLException exc) {
                if (forTables)
                    throw exc;

                System.out.flush();
                System.err.println();
                System.err.println("Ignoring view " + rs.getString("TABLE_NAME") + " due to exception:");
                exc.printStackTrace();
                System.err.println("Continuing analysis.");
            } finally {
                if (rs != null)
                    rs.close();
            }
        }

        return basics;
    }

    /**
     * Return a database-specific array of types from the .properties file
     * with the specified property name.
     *
     * @param propName
     * @param defaultValue
     * @param props
     * @return
     */
    private String[] getTypes(String propName, String defaultValue, Properties props) {
        String value = props.getProperty(propName, defaultValue);
        List<String> types = new ArrayList<String>();
        for (String type : value.split(",")) {
            type = type.trim();
            if (type.length() > 0)
                types.add(type);
        }

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

    /**
     * Some databases don't play nice with their metadata.
     * E.g. Oracle doesn't have a REMARKS column at all.
     * This method ignores those types of failures, replacing them with null.
     */
    public String getOptionalString(ResultSet rs, String columnName)
    {
        try {
            return rs.getString(columnName);
        } catch (SQLException ignore) {
            return null;
        }
    }

    private void initCheckConstraints(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectCheckConstraintsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String tableName = rs.getString("table_name");
                    Table table = tables.get(tableName);
                    if (table != null)
                        table.addCheckConstraint(rs.getString("constraint_name"), rs.getString("text"));
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.err.println();
                System.err.println("Failed to retrieve check constraints: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    private void initTableIds(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectTableIdsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String tableName = rs.getString("table_name");
                    Table table = tables.get(tableName);
                    if (table != null)
                        table.setId(rs.getObject("table_id"));
                }
            } catch (SQLException sqlException) {
                System.err.println();
                System.err.println(sql);
                throw sqlException;
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    private void initIndexIds(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectIndexIdsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String tableName = rs.getString("table_name");
                    Table table = tables.get(tableName);
                    if (table != null) {
                        TableIndex index = table.getIndex(rs.getString("index_name"));
                        if (index != null)
                            index.setId(rs.getObject("index_id"));
                    }
                }
            } catch (SQLException sqlException) {
                System.err.println();
                System.err.println(sql);
                throw sqlException;
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    /**
     * Initializes table comments.
     * If the SQL also returns view comments then they're plugged into the
     * appropriate views.
     *
     * @param properties
     * @throws SQLException
     */
    private void initTableComments(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectTableCommentsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String tableName = rs.getString("table_name");
                    Table table = tables.get(tableName);
                    if (table == null)
                        table = views.get(tableName);

                    if (table != null)
                        table.setComments(rs.getString("comments"));
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.err.println();
                System.err.println("Failed to retrieve table/view comments: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    /**
     * Initializes view comments.
     *
     * @param properties
     * @throws SQLException
     */
    private void initViewComments(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectViewCommentsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String viewName = rs.getString("view_name");
                    if (viewName == null)
                        viewName = rs.getString("table_name");
                    Table view = views.get(viewName);

                    if (view != null)
                        view.setComments(rs.getString("comments"));
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.err.println();
                System.err.println("Failed to retrieve table/view comments: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    /**
     * Initializes table column comments.
     * If the SQL also returns view column comments then they're plugged into the
     * appropriate views.
     *
     * @param properties
     * @throws SQLException
     */
    private void initTableColumnComments(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectColumnCommentsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String tableName = rs.getString("table_name");
                    Table table = tables.get(tableName);
                    if (table == null)
                        table = views.get(tableName);

                    if (table != null) {
                        TableColumn column = table.getColumn(rs.getString("column_name"));
                        if (column != null)
                            column.setComments(rs.getString("comments"));
                    }
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.err.println();
                System.err.println("Failed to retrieve column comments: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    /**
     * Initializes view column comments.
     *
     * @param properties
     * @throws SQLException
     */
    private void initViewColumnComments(Properties properties) throws SQLException {
        String sql = properties.getProperty("selectViewColumnCommentsSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = prepareStatement(sql, null);
                rs = stmt.executeQuery();

                while (rs.next()) {
                    String viewName = rs.getString("view_name");
                    if (viewName == null)
                        viewName = rs.getString("table_name");
                    Table view = views.get(viewName);

                    if (view != null) {
                        TableColumn column = view.getColumn(rs.getString("column_name"));
                        if (column != null)
                            column.setComments(rs.getString("comments"));
                    }
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
                System.err.println();
                System.err.println("Failed to retrieve view column comments: " + sqlException);
                System.err.println(sql);
            } finally {
                if (rs != null)
                    rs.close();
                if (stmt != null)
                    stmt.close();
            }
        }
    }

    /**
     * Create a <code>PreparedStatement</code> from the specified SQL.
     * The SQL can contain these named parameters (but <b>not</b> question marks).
     * <ol>
     * <li>:schema - replaced with the name of the schema
     * <li>:owner - alias for :schema
     * <li>:table - replaced with the name of the table
     * </ol>
     * @param sql String - SQL without question marks
     * @param tableName String - <code>null</code> if the statement doesn't deal with <code>Table</code>-level details.
     * @throws SQLException
     * @return PreparedStatement
     */
    public PreparedStatement prepareStatement(String sql, String tableName) throws SQLException {
        StringBuilder sqlBuf = new StringBuilder(sql);
        List<String> sqlParams = getSqlParams(sqlBuf, tableName); // modifies sqlBuf
        PreparedStatement stmt = getConnection().prepareStatement(sqlBuf.toString());

        try {
            for (int i = 0; i < sqlParams.size(); ++i) {
                stmt.setString(i + 1, sqlParams.get(i).toString());
            }
        } catch (SQLException exc) {
            stmt.close();
            throw exc;
        }

        return stmt;
    }

    public Table addRemoteTable(String remoteSchema, String remoteTableName, String baseSchema, Properties properties, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        String fullName = remoteSchema + "." + remoteTableName;
        Table remoteTable = remoteTables.get(fullName);
        if (remoteTable == null) {
            if (properties != null)
                remoteTable = new RemoteTable(this, remoteSchema, remoteTableName, baseSchema, properties, excludeIndirectColumns, excludeColumns);
            else
                remoteTable = new ExplicitRemoteTable(this, remoteSchema, remoteTableName, baseSchema);

            logger.fine("Adding remote table " + fullName);
            remoteTable.connectForeignKeys(tables, excludeIndirectColumns, excludeColumns);
            remoteTables.put(fullName, remoteTable);
        }

        return remoteTable;
    }

    /**
     * Return an uppercased <code>Set</code> of all SQL keywords used by a database
     *
     * @return
     * @throws SQLException
     */
    public Set<String> getSqlKeywords() throws SQLException {
        if (sqlKeywords == null) {
            // from http://www.contrib.andrew.cmu.edu/~shadow/sql/sql1992.txt:
            String[] sql92Keywords =
                ("ADA" +
                "| C | CATALOG_NAME | CHARACTER_SET_CATALOG | CHARACTER_SET_NAME" +
                "| CHARACTER_SET_SCHEMA | CLASS_ORIGIN | COBOL | COLLATION_CATALOG" +
                "| COLLATION_NAME | COLLATION_SCHEMA | COLUMN_NAME | COMMAND_FUNCTION | COMMITTED" +
                "| CONDITION_NUMBER | CONNECTION_NAME | CONSTRAINT_CATALOG | CONSTRAINT_NAME" +
                "| CONSTRAINT_SCHEMA | CURSOR_NAME" +
                "| DATA | DATETIME_INTERVAL_CODE | DATETIME_INTERVAL_PRECISION | DYNAMIC_FUNCTION" +
                "| FORTRAN" +
                "| LENGTH" +
                "| MESSAGE_LENGTH | MESSAGE_OCTET_LENGTH | MESSAGE_TEXT | MORE | MUMPS" +
                "| NAME | NULLABLE | NUMBER" +
                "| PASCAL | PLI" +
                "| REPEATABLE | RETURNED_LENGTH | RETURNED_OCTET_LENGTH | RETURNED_SQLSTATE" +
                "| ROW_COUNT" +
                "| SCALE | SCHEMA_NAME | SERIALIZABLE | SERVER_NAME | SUBCLASS_ORIGIN" +
                "| TABLE_NAME | TYPE" +
                "| UNCOMMITTED | UNNAMED" +
                "| ABSOLUTE | ACTION | ADD | ALL | ALLOCATE | ALTER | AND" +
                "| ANY | ARE | AS | ASC" +
                "| ASSERTION | AT | AUTHORIZATION | AVG" +
                "| BEGIN | BETWEEN | BIT | BIT_LENGTH | BOTH | BY" +
                "| CASCADE | CASCADED | CASE | CAST | CATALOG | CHAR | CHARACTER | CHAR_LENGTH" +
                "| CHARACTER_LENGTH | CHECK | CLOSE | COALESCE | COLLATE | COLLATION" +
                "| COLUMN | COMMIT | CONNECT | CONNECTION | CONSTRAINT" +
                "| CONSTRAINTS | CONTINUE" +
                "| CONVERT | CORRESPONDING | COUNT | CREATE | CROSS | CURRENT" +
                "| CURRENT_DATE | CURRENT_TIME | CURRENT_TIMESTAMP | CURRENT_USER | CURSOR" +
                "| DATE | DAY | DEALLOCATE | DEC | DECIMAL | DECLARE | DEFAULT | DEFERRABLE" +
                "| DEFERRED | DELETE | DESC | DESCRIBE | DESCRIPTOR | DIAGNOSTICS" +
                "| DISCONNECT | DISTINCT | DOMAIN | DOUBLE | DROP" +
                "| ELSE | END | END-EXEC | ESCAPE | EXCEPT | EXCEPTION" +
                "| EXEC | EXECUTE | EXISTS" +
                "| EXTERNAL | EXTRACT" +
                "| FALSE | FETCH | FIRST | FLOAT | FOR | FOREIGN | FOUND | FROM | FULL" +
                "| GET | GLOBAL | GO | GOTO | GRANT | GROUP" +
                "| HAVING | HOUR" +
                "| IDENTITY | IMMEDIATE | IN | INDICATOR | INITIALLY | INNER | INPUT" +
                "| INSENSITIVE | INSERT | INT | INTEGER | INTERSECT | INTERVAL | INTO | IS" +
                "| ISOLATION" +
                "| JOIN" +
                "| KEY" +
                "| LANGUAGE | LAST | LEADING | LEFT | LEVEL | LIKE | LOCAL | LOWER" +
                "| MATCH | MAX | MIN | MINUTE | MODULE | MONTH" +
                "| NAMES | NATIONAL | NATURAL | NCHAR | NEXT | NO | NOT | NULL" +
                "| NULLIF | NUMERIC" +
                "| OCTET_LENGTH | OF | ON | ONLY | OPEN | OPTION | OR" +
                "| ORDER | OUTER" +
                "| OUTPUT | OVERLAPS" +
                "| PAD | PARTIAL | POSITION | PRECISION | PREPARE | PRESERVE | PRIMARY" +
                "| PRIOR | PRIVILEGES | PROCEDURE | PUBLIC" +
                "| READ | REAL | REFERENCES | RELATIVE | RESTRICT | REVOKE | RIGHT" +
                "| ROLLBACK | ROWS" +
                "| SCHEMA | SCROLL | SECOND | SECTION | SELECT | SESSION | SESSION_USER | SET" +
                "| SIZE | SMALLINT | SOME | SPACE | SQL | SQLCODE | SQLERROR | SQLSTATE" +
                "| SUBSTRING | SUM | SYSTEM_USER" +
                "| TABLE | TEMPORARY | THEN | TIME | TIMESTAMP | TIMEZONE_HOUR | TIMEZONE_MINUTE" +
                "| TO | TRAILING | TRANSACTION | TRANSLATE | TRANSLATION | TRIM | TRUE" +
                "| UNION | UNIQUE | UNKNOWN | UPDATE | UPPER | USAGE | USER | USING" +
                "| VALUE | VALUES | VARCHAR | VARYING | VIEW" +
                "| WHEN | WHENEVER | WHERE | WITH | WORK | WRITE" +
                "| YEAR" +
                "| ZONE").split("|,\\s*");

            String[] nonSql92Keywords = getMetaData().getSQLKeywords().toUpperCase().split(",\\s*");

            sqlKeywords = new HashSet<String>();
            sqlKeywords.addAll(Arrays.asList(sql92Keywords));
            sqlKeywords.addAll(Arrays.asList(nonSql92Keywords));
        }

        return sqlKeywords;
    }

    /**
     * Return <code>id</code> quoted if required, otherwise return <code>id</code>
     *
     * @param id
     * @return
     * @throws SQLException
     */
    public String getQuotedIdentifier(String id) throws SQLException {
        // look for any character that isn't valid (then matcher.find() returns true)
        Matcher matcher = getInvalidIdentifierPattern().matcher(id);

        boolean quotesRequired = matcher.find() || getSqlKeywords().contains(id.toUpperCase());

        if (quotesRequired) {
            // name contains something that must be quoted
            String quote = getMetaData().getIdentifierQuoteString().trim();
            return quote + id + quote;
        }

        // no quoting necessary
        return id;
    }

    /**
     * Return a <code>Pattern</code> whose matcher will return <code>true</code>
     * when run against an identifier that contains a character that is not
     * acceptable by the database without being quoted.
     */
    private Pattern getInvalidIdentifierPattern() throws SQLException {
        if (invalidIdentifierPattern == null) {
            String validChars = "a-zA-Z0-9_";
            String reservedRegexChars = "-&^";
            String extraValidChars = getMetaData().getExtraNameCharacters();
            for (int i = 0; i < extraValidChars.length(); ++i) {
                char ch = extraValidChars.charAt(i);
                if (reservedRegexChars.indexOf(ch) >= 0)
                    validChars += "\\";
                validChars += ch;
            }

            invalidIdentifierPattern = Pattern.compile("[^" + validChars + "]");
        }

        return invalidIdentifierPattern;
    }

    /**
     * Replaces named parameters in <code>sql</code> with question marks and
     * returns appropriate matching values in the returned <code>List</code> of <code>String</code>s.
     *
     * @param sql StringBuffer input SQL with named parameters, output named params are replaced with ?'s.
     * @param tableName String
     * @return List of Strings
     *
     * @see #prepareStatement(String, String)
     */
    private List<String> getSqlParams(StringBuilder sql, String tableName) {
        Map<String, String> namedParams = new HashMap<String, String>();
        @SuppressWarnings("hiding")
        String schema = getSchema();
        if (schema == null)
            schema = getName(); // some 'schema-less' db's treat the db name like a schema (unusual case)
        namedParams.put(":schema", schema);
        namedParams.put(":owner", schema); // alias for :schema
        if (tableName != null) {
            namedParams.put(":table", tableName);
            namedParams.put(":view", tableName); // alias for :table
        }

        List<String> sqlParams = new ArrayList<String>();
        int nextColon = sql.indexOf(":");
        while (nextColon != -1) {
            String paramName = new StringTokenizer(sql.substring(nextColon), " ,\"')").nextToken();
            String paramValue = namedParams.get(paramName);
            if (paramValue == null)
                throw new InvalidConfigurationException("Unexpected named parameter '" + paramName + "' found in SQL '" + sql + "'");
            sqlParams.add(paramValue);
            sql.replace(nextColon, nextColon + paramName.length(), "?"); // replace with a ?
            nextColon = sql.indexOf(":", nextColon);
        }

        return sqlParams;
    }

    /**
     * Take the supplied XML-based metadata and update our model of the schema with it
     *
     * @param schemaMeta
     * @throws SQLException
     */
    private void updateFromXmlMetadata(SchemaMeta schemaMeta) throws SQLException {
        if (schemaMeta != null) {
            final Pattern excludeNone = Pattern.compile("[^.]");
            final Properties noProps = new Properties();

            description = schemaMeta.getComments();

            // done in three passes:
            // 1: create any new tables
            // 2: add/mod columns
            // 3: connect

            // add the newly defined tables and columns first
            for (TableMeta tableMeta : schemaMeta.getTables()) {
                Table table;

                if (tableMeta.getRemoteSchema() != null) {
                    table = remoteTables.get(tableMeta.getRemoteSchema() + '.' + tableMeta.getName());
                    if (table == null) {
                        table = addRemoteTable(tableMeta.getRemoteSchema(), tableMeta.getName(), getSchema(), null, excludeNone, excludeNone);
                    }
                } else {
                    table = tables.get(tableMeta.getName());

                    if (table == null)
                        table = views.get(tableMeta.getName());

                    if (table == null) {
                        table = new Table(Database.this, getSchema(), tableMeta.getName(), null, noProps, excludeNone, excludeNone);
                        tables.put(table.getName(), table);
                    }
                }

                table.update(tableMeta);
            }

            // then tie the tables together
            for (TableMeta tableMeta : schemaMeta.getTables()) {
                Table table;

                if (tableMeta.getRemoteSchema() != null) {
                    table = remoteTables.get(tableMeta.getRemoteSchema() + '.' + tableMeta.getName());
                } else {
                    table = tables.get(tableMeta.getName());
                    if (table == null)
                        table = views.get(tableMeta.getName());
                }

                table.connect(tableMeta, tables, remoteTables);
            }
        }
    }

    private void connectTables() throws SQLException {
        Pattern excludeColumns = Config.getInstance().getColumnExclusions();
        Pattern excludeIndirectColumns = Config.getInstance().getIndirectColumnExclusions();

        for (Table table : tables.values()) {
            table.connectForeignKeys(tables, excludeIndirectColumns, excludeColumns);
        }
    }

    /**
     * Single-threaded implementation of a class that creates tables
     */
    private class TableCreator {
        private final Pattern excludeColumns = Config.getInstance().getColumnExclusions();
        private final Pattern excludeIndirectColumns = Config.getInstance().getIndirectColumnExclusions();

        /**
         * Create a table and put it into <code>tables</code>
         */
        void create(BasicTableMeta tableMeta, Properties properties) throws SQLException {
            createImpl(tableMeta, properties);
        }

        protected void createImpl(BasicTableMeta tableMeta, Properties properties) throws SQLException {
            Table table = new Table(Database.this, tableMeta.schema, tableMeta.name, tableMeta.remarks, properties, excludeIndirectColumns, excludeColumns);
            if (tableMeta.numRows != -1) {
                table.setNumRows(tableMeta.numRows);
            }

            synchronized (tables) {
                tables.put(table.getName(), table);
            }

            if (logger.isLoggable(Level.FINE)) {
                logger.fine("Found details of table " + table.getName());
            } else {
                System.out.print('.');
            }
        }

        /**
         * Wait for all of the tables to be created.
         * By default this does nothing since this implementation isn't threaded.
         */
        void join() {
        }
    }

    /**
     * Multi-threaded implementation of a class that creates tables
     */
    private class ThreadedTableCreator extends TableCreator {
        private final Set<Thread> threads = new HashSet<Thread>();
        private final int maxThreads;

        ThreadedTableCreator(int maxThreads) {
            this.maxThreads = maxThreads;
        }

        @Override
        void create(final BasicTableMeta tableMeta, final Properties properties) throws SQLException {
            Thread runner = new Thread() {
                @Override
                public void run() {
                    try {
                        createImpl(tableMeta, properties);
                    } catch (SQLException exc) {
                        exc.printStackTrace(); // nobody above us in call stack...dump it here
                    } finally {
                        synchronized (threads) {
                            threads.remove(this);
                            threads.notify();
                        }
                    }
                }
            };

            synchronized (threads) {
                // wait for enough 'room'
                while (threads.size() >= maxThreads) {
                    try {
                        threads.wait();
                    } catch (InterruptedException interrupted) {
                    }
                }

                threads.add(runner);
            }

            runner.start();
        }

        /**
         * Wait for all of the started threads to complete
         */
        @Override
        public void join() {
            while (true) {
                Thread thread;

                synchronized (threads) {
                    Iterator<Thread> iter = threads.iterator();
                    if (!iter.hasNext())
                        break;

                    thread = iter.next();
                }

                try {
                    thread.join();
                } catch (InterruptedException exc) {
                }
            }
        }
    }
}
 public class EmptySchemaException extends RuntimeException {
    private static final long serialVersionUID = 1L;

    /**
     * When a message is sufficient
     *
     * @param msg
     */
    public EmptySchemaException() {
        super();
    }
}
 public class ImpliedForeignKeyConstraint extends ForeignKeyConstraint {
    /**
     * @param parentColumn
     * @param childColumn
     */
    public ImpliedForeignKeyConstraint(TableColumn parentColumn, TableColumn childColumn) {
        super(parentColumn, childColumn);
    }

    /**
     * @return
     */
    @Override
    public String getName() {
        return "Implied Constraint";
    }

    /**
     * @return
     */
    @Override
    public boolean isImplied() {
        return true;
    }

    /**
     * @return
     */
    @Override
    public String toString() {
        StringBuilder buf = new StringBuilder();

        buf.append(getChildTable());
        buf.append(".");
        buf.append(toString(getChildColumns()));
        buf.append("'s name implies that it's a child of ");
        buf.append(getParentTable());
        buf.append(".");
        buf.append(toString(getParentColumns()));
        buf.append(", but it doesn't reference that column.");
        return buf.toString();
    }
}
 public class Table implements Comparable<Table> {
    private final String schema;
    private final String name;
    protected final CaseInsensitiveMap<TableColumn> columns = new CaseInsensitiveMap<TableColumn>();
    private final List<TableColumn> primaryKeys = new ArrayList<TableColumn>();
    private final CaseInsensitiveMap<ForeignKeyConstraint> foreignKeys = new CaseInsensitiveMap<ForeignKeyConstraint>();
    private final CaseInsensitiveMap<TableIndex> indexes = new CaseInsensitiveMap<TableIndex>();
    private       Object id;
    private final Map<String, String> checkConstraints = new TreeMap<String, String>(String.CASE_INSENSITIVE_ORDER);
    private Long numRows;
    protected final Database db;
    protected final Properties properties;
    private       String comments;
    private int maxChildren;
    private int maxParents;
    private final static Logger logger = Logger.getLogger(Table.class.getName());

    /**
     * Construct a table that knows everything about the database table's metadata
     *
     * @param db
     * @param schema
     * @param name
     * @param comments
     * @param properties
     * @param excludeIndirectColumns
     * @param excludeColumns
     * @throws SQLException
     */
    public Table(Database db, String schema, String name, String comments, Properties properties, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        this.schema = schema;
        this.name = name;
        this.db = db;
        this.properties = properties;
        logger.fine("Creating " + getClass().getSimpleName().toLowerCase() + " " +
                schema == null ? name : (schema + '.' + name));
        setComments(comments);
        initColumns(excludeIndirectColumns, excludeColumns);
        initIndexes();
        initPrimaryKeys(db.getMetaData());
    }

    /**
     * "Connect" all of this table's foreign keys to their referenced primary keys
     * (and, in some cases, do the reverse as well).
     *
     * @param tables
     * @param excludeIndirectColumns
     * @param excludeColumns
     * @throws SQLException
     */
    public void connectForeignKeys(Map<String, Table> tables, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        ResultSet rs = null;

        try {
            rs = db.getMetaData().getImportedKeys(null, getSchema(), getName());

            while (rs.next()) {
                addForeignKey(rs.getString("FK_NAME"), rs.getString("FKCOLUMN_NAME"),
                        rs.getString("PKTABLE_SCHEM"), rs.getString("PKTABLE_NAME"),
                        rs.getString("PKCOLUMN_NAME"),
                        rs.getInt("UPDATE_RULE"), rs.getInt("DELETE_RULE"),
                        tables, excludeIndirectColumns, excludeColumns);
            }
        } finally {
            if (rs != null)
                rs.close();
        }

        // also try to find all of the 'remote' tables in other schemas that
        // point to our primary keys (not necessary in the normal case
        // as we infer this from the opposite direction)
        if (getSchema() != null) {
            try {
                rs = db.getMetaData().getExportedKeys(null, getSchema(), getName());

                while (rs.next()) {
                    String otherSchema = rs.getString("FKTABLE_SCHEM");
                    if (!getSchema().equals(otherSchema))
                        db.addRemoteTable(otherSchema, rs.getString("FKTABLE_NAME"), getSchema(), properties, excludeIndirectColumns, excludeColumns);
                }
            } finally {
                if (rs != null)
                    rs.close();
            }
        }
    }

    /**
     * Get the foreign keys associated with this table
     *
     * @return
     */
    public Collection<ForeignKeyConstraint> getForeignKeys() {
        return Collections.unmodifiableCollection(foreignKeys.values());
    }

    /**
     * Add a check constraint to the table
     * (no real details, just name and textual representation)
     *
     * @param constraintName
     * @param text
     */
    public void addCheckConstraint(String constraintName, String text) {
        checkConstraints.put(constraintName, text);
    }

    /**
     * @param rs ResultSet from {@link DatabaseMetaData#getImportedKeys(String, String, String)}
     * rs.getString("FK_NAME");
     * rs.getString("FKCOLUMN_NAME");
     * rs.getString("PKTABLE_SCHEM");
     * rs.getString("PKTABLE_NAME");
     * rs.getString("PKCOLUMN_NAME");
     * @param tables Map
     * @param db
     * @throws SQLException
     */
    protected void addForeignKey(String fkName, String fkColName,
                        String pkTableSchema, String pkTableName, String pkColName,
                        int updateRule, int deleteRule,
                        Map<String, Table> tables,
                        Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        if (fkName == null)
            return;

        ForeignKeyConstraint foreignKey = foreignKeys.get(fkName);

        if (foreignKey == null) {
            foreignKey = new ForeignKeyConstraint(this, fkName, updateRule, deleteRule);

            foreignKeys.put(fkName, foreignKey);
        }

        TableColumn childColumn = getColumn(fkColName);
        if (childColumn != null) {
            foreignKey.addChildColumn(childColumn);

            Table parentTable = tables.get(pkTableName);
            String parentSchema = pkTableSchema;
            String baseSchema = Config.getInstance().getSchema();

            // if named table doesn't exist in this schema
            // or exists here but really referencing same named table in another schema
            if (parentTable == null ||
                    (baseSchema != null && parentSchema != null &&
                     !baseSchema.equals(parentSchema))) {
                parentTable = db.addRemoteTable(parentSchema, pkTableName, baseSchema,
                                        properties, excludeIndirectColumns, excludeColumns);
            }

            if (parentTable != null) {
                TableColumn parentColumn = parentTable.getColumn(pkColName);
                if (parentColumn != null) {
                    foreignKey.addParentColumn(parentColumn);

                    childColumn.addParent(parentColumn, foreignKey);
                    parentColumn.addChild(childColumn, foreignKey);
                } else {
                    logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
                                        "' - Column '" + pkColName + "' doesn't exist in table '" + parentTable + "'");
                }
            } else {
                logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
                                    "' - Unknown Referenced Table '" + pkTableName + "'");
            }
        } else {
            logger.warning("Couldn't add FK '" + foreignKey.getName() + "' to table '" + this +
                                "' - Column '" + fkColName + "' doesn't exist");
        }
    }

    /**
     * @param meta
     * @throws SQLException
     */
    private void initPrimaryKeys(DatabaseMetaData meta) throws SQLException {
        if (properties == null)
            return;

        ResultSet rs = null;

        try {
            rs = meta.getPrimaryKeys(null, getSchema(), getName());

            while (rs.next())
                setPrimaryColumn(rs);
        } finally {
            if (rs != null)
                rs.close();
        }
    }

    /**
     * @param rs
     * @throws SQLException
     */
    private void setPrimaryColumn(ResultSet rs) throws SQLException {
        String pkName = rs.getString("PK_NAME");
        if (pkName == null)
            return;

        TableIndex index = getIndex(pkName);
        if (index != null) {
            index.setIsPrimaryKey(true);
        }

        String columnName = rs.getString("COLUMN_NAME");

        setPrimaryColumn(getColumn(columnName));
    }

    /**
     * @param primaryColumn
     */
    void setPrimaryColumn(TableColumn primaryColumn) {
        primaryKeys.add(primaryColumn);
    }

    /**
     * @param excludeIndirectColumns
     * @param excludeColumns
     * @throws SQLException
     */
    private void initColumns(Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        ResultSet rs = null;

        synchronized (Table.class) {
            try {
                rs = db.getMetaData().getColumns(null, getSchema(), getName(), "%");

                while (rs.next())
                    addColumn(rs, excludeIndirectColumns, excludeColumns);
            } catch (SQLException exc) {
                class ColumnInitializationFailure extends SQLException {
                    private static final long serialVersionUID = 1L;

                    public ColumnInitializationFailure(SQLException failure) {
                        super("Failed to collect column details for " + (isView() ? "view" : "table") + " '" + getName() + "' in schema '" + getSchema() + "'");
                        initCause(failure);
                    }
                }

                throw new ColumnInitializationFailure(exc);
            } finally {
                if (rs != null)
                    rs.close();
            }
        }

        if (!isView() && !isRemote())
            initColumnAutoUpdate(false);
    }

    /**
     * @param forceQuotes
     * @throws SQLException
     */
    private void initColumnAutoUpdate(boolean forceQuotes) throws SQLException {
        ResultSet rs = null;
        PreparedStatement stmt = null;

        // we've got to get a result set with all the columns in it
        // so we can ask if the columns are auto updated
        // Ugh!!!  Should have been in DatabaseMetaData instead!!!
        StringBuilder sql = new StringBuilder("select * from ");
        if (getSchema() != null) {
            sql.append(getSchema());
            sql.append('.');
        }

        if (forceQuotes) {
            String quote = db.getMetaData().getIdentifierQuoteString().trim();
            sql.append(quote + getName() + quote);
        } else
            sql.append(db.getQuotedIdentifier(getName()));

        sql.append(" where 0 = 1");

        try {
            stmt = db.getMetaData().getConnection().prepareStatement(sql.toString());
            rs = stmt.executeQuery();

            ResultSetMetaData rsMeta = rs.getMetaData();
            for (int i = rsMeta.getColumnCount(); i > 0; --i) {
                TableColumn column = getColumn(rsMeta.getColumnName(i));
                column.setIsAutoUpdated(rsMeta.isAutoIncrement(i));
            }
        } catch (SQLException exc) {
            if (forceQuotes) {
                // don't completely choke just because we couldn't do this....
                logger.warning("Failed to determine auto increment status: " + exc);
                logger.warning("SQL: " + sql.toString());
            } else {
                initColumnAutoUpdate(true);
            }
        } finally {
            if (rs != null)
                rs.close();
            if (stmt != null)
                stmt.close();
        }
    }

    /**
     * @param rs - from {@link DatabaseMetaData#getColumns(String, String, String, String)}
     * @param excludeIndirectColumns
     * @param excludeColumns
     * @throws SQLException
     */
    protected void addColumn(ResultSet rs, Pattern excludeIndirectColumns, Pattern excludeColumns) throws SQLException {
        String columnName = rs.getString("COLUMN_NAME");

        if (columnName == null)
            return;

        if (getColumn(columnName) == null) {
            TableColumn column = new TableColumn(this, rs, excludeIndirectColumns, excludeColumns);

            columns.put(column.getName(), column);
        }
    }

    /**
     * Add a column that's defined in xml metadata.
     * Assumes that a column named colMeta.getName() doesn't already exist in <code>columns</code>.
     * @param colMeta
     * @return
     */
    protected TableColumn addColumn(TableColumnMeta colMeta) {
        TableColumn column = new TableColumn(this, colMeta);

        columns.put(column.getName(), column);

        return column;
    }

    /**
     * Initialize index information
     *
     * @throws SQLException
     */
    private void initIndexes() throws SQLException {
        if (isView() || isRemote())
            return;

        // first try to initialize using the index query spec'd in the .properties
        // do this first because some DB's (e.g. Oracle) do 'bad' things with getIndexInfo()
        // (they try to do a DDL analyze command that has some bad side-effects)
        if (initIndexes(properties.getProperty("selectIndexesSql")))
            return;

        // couldn't, so try the old fashioned approach
        ResultSet rs = null;

        try {
            rs = db.getMetaData().getIndexInfo(null, getSchema(), getName(), false, true);

            while (rs.next()) {
                if (rs.getShort("TYPE") != DatabaseMetaData.tableIndexStatistic)
                    addIndex(rs);
            }
        } catch (SQLException exc) {
            logger.warning("Unable to extract index info for table '" + getName() + "' in schema '" + getSchema() + "': " + exc);
        } finally {
            if (rs != null)
                rs.close();
        }
    }

    /**
     * Try to initialize index information based on the specified SQL
     *
     * @return boolean <code>true</code> if it worked, otherwise <code>false</code>
     */
    private boolean initIndexes(String selectIndexesSql) {
        if (selectIndexesSql == null)
            return false;

        PreparedStatement stmt = null;
        ResultSet rs = null;

        try {
            stmt = db.prepareStatement(selectIndexesSql, getName());
            rs = stmt.executeQuery();

            while (rs.next()) {
                if (rs.getShort("TYPE") != DatabaseMetaData.tableIndexStatistic)
                    addIndex(rs);
            }
        } catch (SQLException sqlException) {
            logger.warning("Failed to query index information with SQL: " + selectIndexesSql);
            logger.warning(sqlException.toString());
            return false;
        } finally {
            if (rs != null) {
                try {
                    rs.close();
                } catch (Exception exc) {
                    exc.printStackTrace();
                }
            }
            if (stmt != null)  {
                try {
                    stmt.close();
                } catch (Exception exc) {
                    exc.printStackTrace();
                }
            }
        }

        return true;
    }

    /**
     * @param indexName
     * @return
     */
    public TableIndex getIndex(String indexName) {
        return indexes.get(indexName);
    }

    /**
     * @param rs
     * @throws SQLException
     */
    private void addIndex(ResultSet rs) throws SQLException {
        String indexName = rs.getString("INDEX_NAME");

        if (indexName == null)
            return;

        TableIndex index = getIndex(indexName);

        if (index == null) {
            index = new TableIndex(rs);

            indexes.put(index.getName(), index);
        }

        index.addColumn(getColumn(rs.getString("COLUMN_NAME")), rs.getString("ASC_OR_DESC"));
    }

    /**
     * Returns the schema that the table belongs to
     *
     * @return
     */
    public String getSchema() {
        return schema;
    }

    /**
     * Returns the name of the table
     *
     * @return
     */
    public String getName() {
        return name;
    }

    /**
     * Object IDs are useful for tables such as DB/2 that many times
     * give error messages based on object ID and not name
     *
     * @param id
     */
    public void setId(Object id) {
        this.id = id;
    }

    /**
     * @see #setId(Object)
     *
     * @return
     */
    public Object getId() {
        return id;
    }

    /**
     * Returns the check constraints associated with this table
     *
     * @return
     */
    public Map<String, String> getCheckConstraints() {
        return checkConstraints;
    }

    /**
     * Returns the indexes that are applied to this table
     *
     * @return
     */
    public Set<TableIndex> getIndexes() {
        return new HashSet<TableIndex>(indexes.values());
    }

    /**
     * Returns a collection of table columns that have been identified as "primary"
     *
     * @return
     */
    public List<TableColumn> getPrimaryColumns() {
        return primaryKeys;
    }

    /**
     * @return Comments associated with this table, or <code>null</code> if none.
     */
    public String getComments() {
        return comments;
    }

    /**
     * Sets the comments that are associated with this table
     *
     * @param comments
     */
    public void setComments(String comments) {
        String cmts = (comments == null || comments.trim().length() == 0) ? null : comments.trim();

        // MySQL's InnoDB engine does some insane crap of storing erroneous details in
        // with table comments.  Here I attempt to strip the "crap" out without impacting
        // other databases.  Ideally this should happen in selectColumnCommentsSql (and
        // therefore isolate it to MySQL), but it's a bit too complex to do cleanly.
        if (cmts != null) {
            int crapIndex = cmts.indexOf("; InnoDB free: ");
            if (crapIndex == -1)
                crapIndex = cmts.startsWith("InnoDB free: ") ? 0 : -1;
            if (crapIndex != -1) {
                cmts = cmts.substring(0, crapIndex).trim();
                cmts = cmts.length() == 0 ? null : cmts;
            }
        }

        this.comments = cmts;
    }

    /**
     * Returns the {@link TableColumn} with the given name, or <code>null</code>
     * if it doesn't exist
     *
     * @param columnName
     * @return
     */
    public TableColumn getColumn(String columnName) {
        return columns.get(columnName);
    }

    /**
     * Returns <code>List</code> of <code>TableColumn</code>s in ascending column number order.
     *
     * @return
     */
    public List<TableColumn> getColumns() {
        Set<TableColumn> sorted = new TreeSet<TableColumn>(new ByColumnIdComparator());
        sorted.addAll(columns.values());
        return new ArrayList<TableColumn>(sorted);
    }

    /**
     * Returns <code>true</code> if this table references no other tables..<p/>
     * Used in dependency analysis.
     * @return
     */
    public boolean isRoot() {
        for (TableColumn column : columns.values()) {
            if (column.isForeignKey()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns <code>true</code> if this table is referenced by no other tables.<p/>
     * Used in dependency analysis.
     * @return
     */
    public boolean isLeaf() {
        for (TableColumn column : columns.values()) {
            if (!column.getChildren().isEmpty()) {
                return false;
            }
        }

        return true;
    }

    /**
     * Returns the maximum number of parents that this table has had before
     * any had been removed during dependency analysis
     *
     * @return
     */
    public int getMaxParents() {
        return maxParents;
    }

    /**
     * Notification that's called to indicate that a parent has been added to
     * this table
     */
    public void addedParent() {
        maxParents++;
    }

    /**
     * "Unlink" all of the parent tables from this table
     */
    public void unlinkParents() {
        for (TableColumn column : columns.values()) {
            column.unlinkParents();
        }
    }

    /**
     * Returns the maximum number of children that this table has had before
     * any had been removed during dependency analysis
     *
     * @return
     */
    public int getMaxChildren() {
        return maxChildren;
    }

    /**
     * Notification that's called to indicate that a child has been added to
     * this table
     */
    public void addedChild() {
        maxChildren++;
    }

    /**
     * "Unlink" all of the child tables from this table
     */
    public void unlinkChildren() {
        for (TableColumn column : columns.values()) {
            column.unlinkChildren();
        }
    }

    /**
     * Remove a single self referencing constraint if one exists.
     *
     * @return
     */
    public ForeignKeyConstraint removeSelfReferencingConstraint() {
        return remove(getSelfReferencingConstraint());
    }

    /**
     * Remove the specified {@link ForeignKeyConstraint} from this table.<p>
     *
     * This is a more drastic removal solution that was proposed by Remke Rutgers
     *
     * @param constraint
     */
    private ForeignKeyConstraint remove(ForeignKeyConstraint constraint) {
        if (constraint != null) {
            for (int i = 0; i < constraint.getChildColumns().size(); i++) {
                TableColumn childColumn = constraint.getChildColumns().get(i);
                TableColumn parentColumn = constraint.getParentColumns().get(i);
                childColumn.removeParent(parentColumn);
                parentColumn.removeChild(childColumn);
            }
        }
        return constraint;
    }

    /**
     * Return a self referencing constraint if one exists
     *
     * @return
     */
    private ForeignKeyConstraint getSelfReferencingConstraint() {
        for (TableColumn column : columns.values()) {
            for (TableColumn parentColumn : column.getParents()) {
                if (compareTo(parentColumn.getTable()) == 0) {
                    return column.getParentConstraint(parentColumn);
                }
            }
        }
        return null;
    }

    /**
     * Remove any non-real foreign keys
     *
     * @return
     */
    public List<ForeignKeyConstraint> removeNonRealForeignKeys() {
        List<ForeignKeyConstraint> nonReals = new ArrayList<ForeignKeyConstraint>();

        for (TableColumn column : columns.values()) {
            for (TableColumn parentColumn : column.getParents()) {
                ForeignKeyConstraint constraint = column.getParentConstraint(parentColumn);
                if (constraint != null && !constraint.isReal()) {
                    nonReals.add(constraint);
                }
            }
        }

        // remove constraints outside of above loop to prevent
        // concurrent modification exceptions while iterating
        for (ForeignKeyConstraint constraint : nonReals) {
            remove(constraint);
        }

        return nonReals;
    }

    /**
     * Returns the number of tables that reference this table
     *
     * @return
     */
    public int getNumChildren() {
        int numChildren = 0;

        for (TableColumn column : columns.values()) {
            numChildren += column.getChildren().size();
        }

        return numChildren;
    }

    /**
     * Returns the number of non-implied children
     * @return
     */
    public int getNumNonImpliedChildren() {
        int numChildren = 0;

        for (TableColumn column : columns.values()) {
            for (TableColumn childColumn : column.getChildren()) {
                if (!column.getChildConstraint(childColumn).isImplied())
                    ++numChildren;
            }
        }

        return numChildren;
    }

    /**
     * Returns the number of tables that are referenced by this table
     *
     * @return
     */
    public int getNumParents() {
        int numParents = 0;

        for (TableColumn column : columns.values()) {
            numParents += column.getParents().size();
        }

        return numParents;
    }

    /**
     * Returns the number of non-implied parents
     *
     * @return
     */
    public int getNumNonImpliedParents() {
        int numParents = 0;

        for (TableColumn column : columns.values()) {
            for (TableColumn parentColumn : column.getParents()) {
                if (!column.getParentConstraint(parentColumn).isImplied())
                    ++numParents;
            }
        }

        return numParents;
    }

    /**
     * Remove one foreign key constraint.
     *
     * <p/>Used during dependency analysis phase.
     *
     * @return
     */
    public ForeignKeyConstraint removeAForeignKeyConstraint() {
        @SuppressWarnings("hiding")
        final List<TableColumn> columns = getColumns();
        int numParents = 0;
        int numChildren = 0;
        // remove either a child or parent, choosing which based on which has the
        // least number of foreign key associations (when either gets to zero then
        // the table can be pruned)
        for (TableColumn column : columns) {
            numParents += column.getParents().size();
            numChildren += column.getChildren().size();
        }

        for (TableColumn column : columns) {
            ForeignKeyConstraint constraint;
            if (numParents <= numChildren)
                constraint = column.removeAParentFKConstraint();
            else
                constraint = column.removeAChildFKConstraint();
            if (constraint != null)
                return constraint;
        }

        return null;
    }

    /**
     * Returns <code>true</code> if this is a view, <code>false</code> otherwise
     *
     * @return
     */
    public boolean isView() {
        return false;
    }

    /**
     * Returns <code>true</code> if this table is remote (in another schema), <code>false</code> otherwise
     * @return
     */
    public boolean isRemote() {
        return false;
    }

    /**
     * If this is a view it returns the SQL used to create the view (if it's available).
     * <code>null</code> if it's not a view or the SQL isn't available.
     * @return
     * @see #isView()
     */
    public String getViewSql() {
        return null;
    }

    /**
     * Returns the number of rows contained in this table, or -1 if unable to determine
     * the number of rows.
     *
     * @return
     */
    public long getNumRows() {
        if (numRows == null) {
            numRows = Config.getInstance().isNumRowsEnabled() ? fetchNumRows() : -1;
        }

        return numRows;
    }

    /**
     * Explicitly set the number of rows in this table
     *
     * @param numRows
     */
    public void setNumRows(long numRows) {
        this.numRows = numRows;
    }

    /**
     * Fetch the number of rows contained in this table.
     *
     * returns -1 if unable to successfully fetch the row count
     *
     * @param db Database
     * @return long
     * @throws SQLException
     */
    protected long fetchNumRows() {
        if (properties == null) // some "meta" tables don't have associated properties
            return 0;

        SQLException originalFailure = null;

        String sql = properties.getProperty("selectRowCountSql");
        if (sql != null) {
            PreparedStatement stmt = null;
            ResultSet rs = null;

            try {
                stmt = db.prepareStatement(sql, getName());
                rs = stmt.executeQuery();

                while (rs.next()) {
                    return rs.getLong("row_count");
                }
            } catch (SQLException sqlException) {
                // don't die just because this failed
            	originalFailure = sqlException;
            } finally {
                if (rs != null) {
                    try {
                        rs.close();
                    } catch (SQLException exc) {}
                }
                if (stmt != null) {
                    try {
                        stmt.close();
                    } catch (SQLException exc) {}
                }
            }
        }

        // if we get here then we either didn't have custom SQL or it didn't work
        try {
            // '*' should work best for the majority of cases
            return fetchNumRows("count(*)", false);
        } catch (SQLException try2Exception) {
            try {
                // except nested tables...try using '1' instead
                return fetchNumRows("count(1)", false);
            } catch (SQLException try3Exception) {
                logger.warning("Unable to extract the number of rows for table " + getName() + ", using '-1'");
                if (originalFailure != null)
                    logger.warning(originalFailure.toString());
                logger.warning(try2Exception.toString());
                logger.warning(try3Exception.toString());
                return -1;
            }
        }
    }

    protected long fetchNumRows(String clause, boolean forceQuotes) throws SQLException {
        PreparedStatement stmt = null;
        ResultSet rs = null;
        StringBuilder sql = new StringBuilder("select ");
        sql.append(clause);
        sql.append(" from ");
        if (getSchema() != null) {
            sql.append(getSchema());
            sql.append('.');
        }

        if (forceQuotes) {
            String quote = db.getMetaData().getIdentifierQuoteString().trim();
            sql.append(quote + getName() + quote);
        } else
            sql.append(db.getQuotedIdentifier(getName()));

        try {
            stmt = db.getConnection().prepareStatement(sql.toString());
            rs = stmt.executeQuery();
            while (rs.next()) {
                return rs.getLong(1);
            }
            return -1;
        } catch (SQLException exc) {
            if (forceQuotes) // we tried with and w/o quotes...fail this attempt
                throw exc;

            return fetchNumRows(clause, true);
        } finally {
            if (rs != null)
                rs.close();
            if (stmt != null)
                stmt.close();
        }
    }

    /**
     * Update the table with the specified XML-derived metadata
     *
     * @param tableMeta
     */
    public void update(TableMeta tableMeta) {
        String newComments = tableMeta.getComments();
        if (newComments != null) {
            comments = newComments;
        }

        for (TableColumnMeta colMeta : tableMeta.getColumns()) {
            TableColumn col = getColumn(colMeta.getName());
            if (col == null) {
                if (tableMeta.getRemoteSchema() == null) {
                    logger.warning("Unrecognized column '" + colMeta.getName() + "' for table '" + getName() + '\'');
                    continue;
                }

                col = addColumn(colMeta);
            }

            // update the column with the changes
            col.update(colMeta);
        }
    }

    /**
     * Same as {@link #connectForeignKeys(Map, Database, Properties, Pattern, Pattern)},
     * but uses XML-based metadata
     *
     * @param tableMeta
     * @param tables
     * @param remoteTables
     */
    public void connect(TableMeta tableMeta, Map<String, Table> tables, Map<String, Table> remoteTables) {
        for (TableColumnMeta colMeta : tableMeta.getColumns()) {
            TableColumn col = getColumn(colMeta.getName());

             // go thru the new foreign key defs and associate them with our columns
            for (ForeignKeyMeta fk : colMeta.getForeignKeys()) {
                Table parent = fk.getRemoteSchema() == null ? tables.get(fk.getTableName())
                                                            : remoteTables.get(fk.getRemoteSchema() + '.' + fk.getTableName());
                if (parent != null) {
                    TableColumn parentColumn = parent.getColumn(fk.getColumnName());

                    if (parentColumn == null) {
                        logger.warning(parent.getName() + '.' + fk.getColumnName() + " doesn't exist");
                    } else {
                        /**
                         * Merely instantiating a foreign key constraint ties it
                         * into its parent and child columns (& therefore their tables)
                         */
                        new ForeignKeyConstraint(parentColumn, col) {
                            @Override
                            public String getName() {
                                return "Defined in XML";
                            }
                        };
                    }
                } else {
                    logger.warning("Undefined table '" + fk.getTableName() + "' referenced by '" + getName() + '.' + col.getName() + '\'');
                }
            }
        }
    }

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

    /**
     * Returns <code>true</code> if this table has no relationships
     *
     * @param withImpliedRelationships boolean
     * @return boolean
     */
    public boolean isOrphan(boolean withImpliedRelationships) {
        if (withImpliedRelationships)
            return getMaxParents() == 0 && getMaxChildren() == 0;

        for (TableColumn column : columns.values()) {
            for (TableColumn parentColumn : column.getParents()) {
                if (!column.getParentConstraint(parentColumn).isImplied())
                    return false;
            }
            for (TableColumn childColumn : column.getChildren()) {
                if (!column.getChildConstraint(childColumn).isImplied())
                    return false;
            }
        }
        return true;
    }

    /**
     * Compare this table to another table.
     * Results are based on 1: identity, 2: table name, 3: schema name<p/>
     *
     * This implementation was put in place to deal with analyzing multiple
     * schemas that contain identically named tables.
     *
     * @see {@link Comparable#compareTo(Object)}
     */
    public int compareTo(Table other) {
        if (other == this)  // fast way out
            return 0;

        int rc = getName().compareToIgnoreCase(other.getName());
        if (rc == 0) {
            // should only get here if we're dealing with cross-schema references (rare)
            String ours = getSchema();
            String theirs = other.getSchema();
            if (ours != null && theirs != null)
                rc = ours.compareToIgnoreCase(theirs);
            else if (ours == null)
                rc = -1;
            else
                rc = 1;
        }

        return rc;
    }

    /**
     * Implementation of {@link Comparator} that sorts {@link TableColumn}s
     * by {@link TableColumn#getId() ID} (ignored if <code>null</code>)
     * followed by {@link TableColumn#getName() Name}.
     */
    private static class ByColumnIdComparator implements Comparator<TableColumn> {
        public int compare(TableColumn column1, TableColumn column2) {
            if (column1.getId() == null || column2.getId() == null)
                return column1.getName().compareToIgnoreCase(column2.getName());
            if (column1.getId() instanceof Number)
                return ((Number)column1.getId()).intValue() - ((Number)column2.getId()).intValue();
            return column1.getId().toString().compareToIgnoreCase(column2.getId().toString());
        }
    }
}
 public class Dot {
    private static Dot instance = new Dot();
    private final Version version;
    private final Version supportedVersion = new Version("2.2.1");
    private final Version badVersion = new Version("2.4");
    private final String lineSeparator = System.getProperty("line.separator");
    private String dotExe;
    private String format = "png";
    private String renderer;
    private final Set<String> validatedRenderers = Collections.synchronizedSet(new HashSet<String>());
    private final Set<String> invalidatedRenderers = Collections.synchronizedSet(new HashSet<String>());

    private Dot() {
        String versionText = null;
        // dot -V should return something similar to:
        //  dot version 2.8 (Fri Feb  3 22:38:53 UTC 2006)
        // or sometimes something like:
        //  dot - Graphviz version 2.9.20061004.0440 (Wed Oct 4 21:01:52 GMT 2006)
        String[] dotCommand = new String[] { getExe(), "-V" };

        try {
            Process process = Runtime.getRuntime().exec(dotCommand);
            BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String versionLine = reader.readLine();

            // look for a number followed numbers or dots
            Matcher matcher = Pattern.compile("[0-9][0-9.]+").matcher(versionLine);
            if (matcher.find()) {
                versionText = matcher.group();
            } else {
                if (Config.getInstance().isHtmlGenerationEnabled()) {
                    System.err.println();
                    System.err.println("Invalid dot configuration detected.  '" +
                                        getDisplayableCommand(dotCommand) + "' returned:");
                    System.err.println("   " + versionLine);
                }
            }
        } catch (Exception validDotDoesntExist) {
            if (Config.getInstance().isHtmlGenerationEnabled()) {
                System.err.println("Failed to query Graphviz version information");
                System.err.println("  with: " + getDisplayableCommand(dotCommand));
                System.err.println("  " + validDotDoesntExist);
            }
        }

        version = new Version(versionText);
    }

    public static Dot getInstance() {
        return instance;
    }

    public boolean exists() {
        return version.toString() != null;
    }

    public Version getVersion() {
        return version;
    }

    public boolean isValid() {
        return exists() && (getVersion().equals(supportedVersion) || getVersion().compareTo(badVersion) > 0);
    }

    public String getSupportedVersions() {
        return "dot version " + supportedVersion + " or versions greater than " + badVersion;
    }

    public boolean supportsCenteredEastWestEdges() {
        return getVersion().compareTo(new Version("2.6")) >= 0;
    }

    /**
     * Set the image format to generate.  Defaults to <code>png</code>.
     * See <a href='http://www.graphviz.org/doc/info/output.html'>http://www.graphviz.org/doc/info/output.html</a>
     * for valid formats.
     *
     * @param format image format to generate
     */
    public void setFormat(String format) {
        this.format = format;
    }

    /**
     * @see #setFormat(String)
     * @return
     */
    public String getFormat() {
        return format;
    }

    /**
     * Returns true if the installed dot requires specifying :gd as a renderer.
     * This was added when Win 2.15 came out because it defaulted to Cairo, which produces
     * better quality output, but at a significant speed and size penalty.<p>
     *
     * The intent of this property is to determine if it's ok to tack ":gd" to
     * the format specifier.  Earlier versions didn't require it and didn't know
     * about the option.
     *
     * @return
     */
    public boolean requiresGdRenderer() {
        return getVersion().compareTo(new Version("2.12")) >= 0 && supportsRenderer(":gd");
    }

    /**
     * Set the renderer to use for the -Tformat[:renderer[:formatter]] dot option as specified
     * at <a href='http://www.graphviz.org/doc/info/command.html'>
     * http://www.graphviz.org/doc/info/command.html</a> where "format" is specified by
     * {@link #setFormat(String)}<p>
     * Note that the leading ":" is required while :formatter is optional.
     *
     * @param renderer
     */
    public void setRenderer(String renderer) {
        this.renderer = renderer;
    }

    public String getRenderer() {
        return renderer != null && supportsRenderer(renderer) ? renderer
            : (requiresGdRenderer() ? ":gd" : "");
    }

    /**
     * If <code>true</code> then generate output of "higher quality"
     * than the default ("lower quality").
     * Note that the default is intended to be "lower quality",
     * but various installations of Graphviz may have have different abilities.
     * That is, some might not have the "lower quality" libraries and others might
     * not have the "higher quality" libraries.
     */
    public void setHighQuality(boolean highQuality) {
        if (highQuality && supportsRenderer(":cairo")) {
            setRenderer(":cairo");
        } else if (supportsRenderer(":gd")) {
            setRenderer(":gd");
        }
    }

    /**
     * @see #setHighQuality(boolean)
     */
    public boolean isHighQuality() {
        return getRenderer().indexOf(":cairo") != -1;
    }

    /**
     * Returns <code>true</code> if the specified renderer is supported.
     * See {@link #setRenderer(String)} for renderer details.
     *
     * @param renderer
     * @return
     */
    public boolean supportsRenderer(@SuppressWarnings("hiding") String renderer) {
        if (!exists())
            return false;

        if (validatedRenderers.contains(renderer))
            return true;

        if (invalidatedRenderers.contains(renderer))
            return false;

        try {
            String[] dotCommand = new String[] {
                getExe(),
                "-T" + getFormat() + ':'
            };
            Process process = Runtime.getRuntime().exec(dotCommand);
            BufferedReader errors = new BufferedReader(new InputStreamReader(process.getErrorStream()));
            String line;
            while ((line = errors.readLine()) != null) {
                if (line.contains(getFormat() + renderer)) {
                    validatedRenderers.add(renderer);
                }
            }
            process.waitFor();
        } catch (Exception exc) {
            exc.printStackTrace();
        }

        if (!validatedRenderers.contains(renderer)) {
            //System.err.println("\nFailed to validate " + getFormat() + " renderer '" + renderer + "'.  Reverting to detault renderer for " + getFormat() + '.');
            invalidatedRenderers.add(renderer);
            return false;
        }

        return true;
    }

    /**
     * Returns the executable to use to run dot
     *
     * @return
     */
    private String getExe() {
        if (dotExe == null)
        {
            File gv = Config.getInstance().getGraphvizDir();

            if (gv == null) {
                // default to finding dot in the PATH
                dotExe = "dot";
            } else {
                // pull dot from the Graphviz bin directory specified
                dotExe = new File(new File(gv, "bin"), "dot").toString();
            }
        }

        return dotExe;
    }

    /**
     * Using the specified .dot file generates an image returning the image's image map.
     */
    public String generateDiagram(File dotFile, File diagramFile) throws DotFailure {
        StringBuilder mapBuffer = new StringBuilder(1024);

        BufferedReader mapReader = null;
        // this one is for executing.  it can (hopefully) deal with funky things in filenames.
        String[] dotCommand = new String[] {
            getExe(),
            "-T" + getFormat() + getRenderer(),
            dotFile.toString(),
            "-o" + diagramFile,
            "-Tcmapx"
        };
        // this one is for display purposes ONLY.
        String commandLine = getDisplayableCommand(dotCommand);

        try {
            Process process = Runtime.getRuntime().exec(dotCommand);
            new ProcessOutputReader(commandLine, process.getErrorStream()).start();
            mapReader = new BufferedReader(new InputStreamReader(process.getInputStream()));
            String line;
            while ((line = mapReader.readLine()) != null) {
                mapBuffer.append(line);
                mapBuffer.append(lineSeparator);
            }
            int rc = process.waitFor();
            if (rc != 0)
                throw new DotFailure("'" + commandLine + "' failed with return code " + rc);
            if (!diagramFile.exists())
                throw new DotFailure("'" + commandLine + "' failed to create output file");

            // dot generates post-HTML 4.0.1 output...convert trailing />'s to >'s
            return mapBuffer.toString().replace("/>", ">");
        } catch (InterruptedException interrupted) {
            throw new RuntimeException(interrupted);
        } catch (DotFailure failed) {
            diagramFile.delete();
            throw failed;
        } catch (IOException failed) {
            diagramFile.delete();
            throw new DotFailure("'" + commandLine + "' failed with exception " + failed);
        } finally {
            if (mapReader != null) {
                try {
                    mapReader.close();
                } catch (IOException ignore) {}
            }
        }
    }

    public class DotFailure extends IOException {
        private static final long serialVersionUID = 3833743270181351987L;

        public DotFailure(String msg) {
            super(msg);
        }
    }

    private static String getDisplayableCommand(String[] command) {
        StringBuilder displayable = new StringBuilder();
        for (int i = 0; i < command.length; ++i) {
            displayable.append(command[i]);
            if (i + 1 < command.length)
                displayable.append(' ');
        }
        return displayable.toString();
    }

    private static class ProcessOutputReader extends Thread {
        private final BufferedReader processReader;
        private final String command;

        ProcessOutputReader(String command, InputStream processStream) {
            processReader = new BufferedReader(new InputStreamReader(processStream));
            this.command = command;
            setDaemon(true);
        }

        @Override
        public void run() {
            try {
                String line;
                while ((line = processReader.readLine()) != null) {
                    // don't report port id unrecognized or unrecognized port
                    if (line.indexOf("unrecognized") == -1 && line.indexOf("port") == -1)
                        System.err.println(command + ": " + line);
                }
            } catch (IOException ioException) {
                ioException.printStackTrace();
            } finally {
                try {
                    processReader.close();
                } catch (Exception exc) {
                    exc.printStackTrace(); // shouldn't ever get here...but...
                }
            }
        }
    }
} public class ResourceWriter {
    private static ResourceWriter instance = new ResourceWriter();

    protected ResourceWriter() {
    }

    public static ResourceWriter getInstance() {
        return instance;
    }

    /**
     * Write the specified resource to the specified filename
     *
     * @param resourceName
     * @param writeTo
     * @throws IOException
     */
    public void writeResource(String resourceName, File writeTo) throws IOException {
        writeTo.getParentFile().mkdirs();
        InputStream in = getClass().getResourceAsStream(resourceName);
        if (in == null)
            throw new IOException("Resource \"" + resourceName + "\" not found");

        byte[] buf = new byte[4096];

        OutputStream out = new FileOutputStream(writeTo);
        int numBytes = 0;
        while ((numBytes = in.read(buf)) != -1) {
            out.write(buf, 0, numBytes);
        }
        in.close();
        out.close();
    }
} public class DotFormatter {
    private static DotFormatter instance = new DotFormatter();
    private final int fontSize = Config.getInstance().getFontSize();

    /**
     * Singleton - prevent creation
     */
    private DotFormatter() {
    }

    public static DotFormatter getInstance() {
        return instance;
    }

    /**
     * Write real relationships (excluding implied) associated with the given table.<p>
     * Returns a set of the implied constraints that could have been included but weren't.
     */
    public Set<ForeignKeyConstraint> writeRealRelationships(Table table, boolean twoDegreesOfSeparation, WriteStats stats, LineWriter dot) throws IOException {
        return writeRelationships(table, twoDegreesOfSeparation, stats, false, dot);
    }

    /**
     * Write implied relationships associated with the given table
     */
    public void writeAllRelationships(Table table, boolean twoDegreesOfSeparation, WriteStats stats, LineWriter dot) throws IOException {
        writeRelationships(table, twoDegreesOfSeparation, stats, true, dot);
    }

    /**
     * Write relationships associated with the given table.<p>
     * Returns a set of the implied constraints that could have been included but weren't.
     */
    private Set<ForeignKeyConstraint> writeRelationships(Table table, boolean twoDegreesOfSeparation, WriteStats stats, boolean includeImplied, LineWriter dot) throws IOException {
        Set<Table> tablesWritten = new HashSet<Table>();
        Set<ForeignKeyConstraint> skippedImpliedConstraints = new HashSet<ForeignKeyConstraint>();

        DotConnectorFinder finder = DotConnectorFinder.getInstance();

        String diagramName = includeImplied ? "impliedTwoDegreesRelationshipsDiagram" : (twoDegreesOfSeparation ? "twoDegreesRelationshipsDiagram" : "oneDegreeRelationshipsDiagram");
        writeHeader(diagramName, true, dot);

        Set<Table> relatedTables = getImmediateRelatives(table, true, includeImplied, skippedImpliedConstraints);

        Set<DotConnector> connectors = new TreeSet<DotConnector>(finder.getRelatedConnectors(table, includeImplied));
        tablesWritten.add(table);

        Map<Table, DotNode> nodes = new TreeMap<Table, DotNode>();

        // write immediate relatives first
        for (Table relatedTable : relatedTables) {
            if (!tablesWritten.add(relatedTable))
                continue; // already written

            nodes.put(relatedTable, new DotNode(relatedTable, true, ""));
            connectors.addAll(finder.getRelatedConnectors(relatedTable, table, true, includeImplied));
        }

        // connect the edges that go directly to the target table
        // so they go to the target table's type column instead
        for (DotConnector connector : connectors) {
            if (connector.pointsTo(table))
                connector.connectToParentDetails();
        }

        Set<Table> allCousins = new HashSet<Table>();
        Set<DotConnector> allCousinConnectors = new TreeSet<DotConnector>();

        // next write 'cousins' (2nd degree of separation)
        if (twoDegreesOfSeparation) {
            for (Table relatedTable : relatedTables) {
                Set<Table> cousins = getImmediateRelatives(relatedTable, false, includeImplied, skippedImpliedConstraints);

                for (Table cousin : cousins) {
                    if (!tablesWritten.add(cousin))
                        continue; // already written

                    allCousinConnectors.addAll(finder.getRelatedConnectors(cousin, relatedTable, false, includeImplied));
                    nodes.put(cousin, new DotNode(cousin, false, ""));
                }

                allCousins.addAll(cousins);
            }
        }

        // glue together any 'participants' that aren't yet connected
        // note that this is the epitome of nested loops from hell
        List<Table> participants = new ArrayList<Table>(nodes.keySet());
        Iterator<Table> iter = participants.iterator();
        while (iter.hasNext()) {
            Table participantA = iter.next();
            iter.remove(); // cut down the combos as quickly as possible

            for (Table participantB : participants) {
                for (DotConnector connector : finder.getRelatedConnectors(participantA, participantB, false, includeImplied)) {
                    if (twoDegreesOfSeparation && (allCousins.contains(participantA) || allCousins.contains(participantB))) {
                        allCousinConnectors.add(connector);
                    } else {
                        connectors.add(connector);
                    }
                }
            }
        }

        markExcludedColumns(nodes, stats.getExcludedColumns());

        // now directly connect the loose ends to the title of the
        // 2nd degree of separation tables
        for (DotConnector connector : allCousinConnectors) {
            if (allCousins.contains(connector.getParentTable()) && !relatedTables.contains(connector.getParentTable()))
                connector.connectToParentTitle();
            if (allCousins.contains(connector.getChildTable()) && !relatedTables.contains(connector.getChildTable()))
                connector.connectToChildTitle();
        }

        // include the table itself
        nodes.put(table, new DotNode(table, ""));

        connectors.addAll(allCousinConnectors);
        for (DotConnector connector : connectors) {
            if (connector.isImplied()) {
                DotNode node = nodes.get(connector.getParentTable());
                if (node != null)
                    node.setShowImplied(true);
                node = nodes.get(connector.getChildTable());
                if (node != null)
                    node.setShowImplied(true);
            }
            dot.writeln(connector.toString());
        }

        for (DotNode node : nodes.values()) {
            dot.writeln(node.toString());
            stats.wroteTable(node.getTable());
        }

        dot.writeln("}");

        return skippedImpliedConstraints;
    }

    private Set<Table> getImmediateRelatives(Table table, boolean includeExcluded, boolean includeImplied, Set<ForeignKeyConstraint> skippedImpliedConstraints) {
        Set<TableColumn> relatedColumns = new HashSet<TableColumn>();

        for (TableColumn column : table.getColumns()) {
            if (column.isAllExcluded() || (!includeExcluded && column.isExcluded())) {
                continue;
            }

            for (TableColumn childColumn : column.getChildren()) {
                if (childColumn.isAllExcluded() || (!includeExcluded && childColumn.isExcluded())) {
                    continue;
                }

                ForeignKeyConstraint constraint = column.getChildConstraint(childColumn);
                if (includeImplied || !constraint.isImplied())
                    relatedColumns.add(childColumn);
                else
                    skippedImpliedConstraints.add(constraint);
            }

            for (TableColumn parentColumn : column.getParents()) {
                if (parentColumn.isAllExcluded() || (!includeExcluded && parentColumn.isExcluded())) {
                    continue;
                }

                ForeignKeyConstraint constraint = column.getParentConstraint(parentColumn);
                if (includeImplied || !constraint.isImplied())
                    relatedColumns.add(parentColumn);
                else
                    skippedImpliedConstraints.add(constraint);
            }
        }

        Set<Table> relatedTables = new HashSet<Table>();
        for (TableColumn column : relatedColumns)
            relatedTables.add(column.getTable());

        relatedTables.remove(table);

        return relatedTables;
    }

    private void writeHeader(String diagramName, boolean showLabel, LineWriter dot) throws IOException {
        dot.writeln("// dot " + Dot.getInstance().getVersion() + " on " + System.getProperty("os.name") + " " + System.getProperty("os.version"));
        dot.writeln("// SchemaSpy rev " + new Revision());
        dot.writeln("digraph \"" + diagramName + "\" {");
        dot.writeln("  graph [");
        boolean rankdirbug = Config.getInstance().isRankDirBugEnabled();
        if (!rankdirbug)
            dot.writeln("    rankdir=\"RL\"");
        dot.writeln("    bgcolor=\"" + StyleSheet.getInstance().getBodyBackground() + "\"");
        if (showLabel) {
            if (rankdirbug)
                dot.writeln("    label=\"\\nLayout is significantly better without '-rankdirbug' option\"");
            else
                dot.writeln("    label=\"\\nGenerated by SchemaSpy\"");
            dot.writeln("    labeljust=\"l\"");
        }
        dot.writeln("    nodesep=\"0.18\"");
        dot.writeln("    ranksep=\"0.46\"");
        dot.writeln("    fontname=\"" + Config.getInstance().getFont() + "\"");
        dot.writeln("    fontsize=\"" + fontSize + "\"");
        dot.writeln("  ];");
        dot.writeln("  node [");
        dot.writeln("    fontname=\"" + Config.getInstance().getFont() + "\"");
        dot.writeln("    fontsize=\"" + fontSize + "\"");
        dot.writeln("    shape=\"plaintext\"");
        dot.writeln("  ];");
        dot.writeln("  edge [");
        dot.writeln("    arrowsize=\"0.8\"");
        dot.writeln("  ];");
}

    public void writeRealRelationships(Database db, Collection<Table> tables, boolean compact, boolean showColumns, WriteStats stats, LineWriter dot) throws IOException {
        writeRelationships(db, tables, compact, showColumns, false, stats, dot);
    }

    /**
     * Returns <code>true</code> if it wrote any implied relationships
     */
    public boolean writeAllRelationships(Database db, Collection<Table> tables, boolean compact, boolean showColumns, WriteStats stats, LineWriter dot) throws IOException {
        return writeRelationships(db, tables, compact, showColumns, true, stats, dot);
    }

    private boolean writeRelationships(Database db, Collection<Table> tables, boolean compact, boolean showColumns, boolean includeImplied, WriteStats stats, LineWriter dot) throws IOException {
        DotConnectorFinder finder = DotConnectorFinder.getInstance();
        DotNodeConfig nodeConfig = showColumns ? new DotNodeConfig(!compact, false) : new DotNodeConfig();
        boolean wroteImplied = false;

        String diagramName;
        if (includeImplied) {
            if (compact)
                diagramName = "compactImpliedRelationshipsDiagram";
            else
                diagramName = "largeImpliedRelationshipsDiagram";
        } else {
            if (compact)
                diagramName = "compactRelationshipsDiagram";
            else
                diagramName = "largeRelationshipsDiagram";
        }
        writeHeader(diagramName, true, dot);

        Map<Table, DotNode> nodes = new TreeMap<Table, DotNode>();

        for (Table table : tables) {
            if (!table.isOrphan(includeImplied)) {
                nodes.put(table, new DotNode(table, "tables/", nodeConfig));
            }
        }

        for (Table table : db.getRemoteTables()) {
            nodes.put(table, new DotNode(table, "tables/", nodeConfig));
        }

        Set<DotConnector> connectors = new TreeSet<DotConnector>();

        for (DotNode node : nodes.values()) {
            connectors.addAll(finder.getRelatedConnectors(node.getTable(), includeImplied));
        }

        markExcludedColumns(nodes, stats.getExcludedColumns());

        for (DotNode node : nodes.values()) {
            Table table = node.getTable();

            dot.writeln(node.toString());
            stats.wroteTable(table);
            wroteImplied = wroteImplied || (includeImplied && table.isOrphan(false));
        }

        for (DotConnector connector : connectors) {
            dot.writeln(connector.toString());
        }

        dot.writeln("}");

        return wroteImplied;
    }

    private void markExcludedColumns(Map<Table, DotNode> nodes, Set<TableColumn> excludedColumns) {
        for (TableColumn column : excludedColumns) {
            DotNode node = nodes.get(column.getTable());
            if (node != null) {
                node.excludeColumn(column);
            }
        }
    }

    public void writeOrphan(Table table, LineWriter dot) throws IOException {
        writeHeader(table.getName(), false, dot);
        dot.writeln(new DotNode(table, true, "tables/").toString());
        dot.writeln("}");
    }
}
 public class HtmlAnomaliesPage extends HtmlFormatter {
    private static HtmlAnomaliesPage instance = new HtmlAnomaliesPage();

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlAnomaliesPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlAnomaliesPage getInstance() {
        return instance;
    }

    public void write(Database database, Collection<Table> tables, List<? extends ForeignKeyConstraint> impliedConstraints, boolean hasOrphans, LineWriter out) throws IOException {
        writeHeader(database, hasOrphans, out);
        writeImpliedConstraints(impliedConstraints, out);
        writeTablesWithoutIndexes(DbAnalyzer.getTablesWithoutIndexes(new HashSet<Table>(tables)), out);
        writeUniqueNullables(DbAnalyzer.getMustBeUniqueNullableColumns(new HashSet<Table>(tables)), out);
        writeTablesWithOneColumn(DbAnalyzer.getTablesWithOneColumn(tables), out);
        writeTablesWithIncrementingColumnNames(DbAnalyzer.getTablesWithIncrementingColumnNames(tables), out);
        writeDefaultNullStrings(DbAnalyzer.getDefaultNullStringColumns(new HashSet<Table>(tables)), out);
        writeFooter(out);
    }

    private void writeHeader(Database database, boolean hasOrphans, LineWriter html) throws IOException {
        writeHeader(database, null, "Anomalies", hasOrphans, html);
        html.writeln("<table width='100%'>");
        if (sourceForgeLogoEnabled())
            html.writeln("  <tr><td class='container' align='right' valign='top' colspan='2'><a href='http://sourceforge.net' target='_blank'><img src='http://sourceforge.net/sflogo.php?group_id=137197&amp;type=1' alt='SourceForge.net' border='0' height='31' width='88'></a></td></tr>");
        html.writeln("<tr>");
        html.writeln("<td class='container'><b>Things that might not be 'quite right' about your schema:</b></td>");
        html.writeln("<td class='container' align='right'>");
        writeFeedMe(html);
        html.writeln("</td></tr></table>");
        html.writeln("<ul>");
    }

    private void writeImpliedConstraints(List<? extends ForeignKeyConstraint> impliedConstraints, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.writeln("<b>Columns whose name and type imply a relationship to another table's primary key:</b>");
        int numDetected = 0;

        for (ForeignKeyConstraint impliedConstraint : impliedConstraints) {
            Table childTable = impliedConstraint.getChildTable();
            if (!childTable.isView()) {
                ++numDetected;
            }
        }

        if (numDetected > 0) {
            out.writeln("<table class='dataTable' border='1' rules='groups'>");
            out.writeln("<colgroup>");
            out.writeln("<colgroup>");
            out.writeln("<thead align='left'>");
            out.writeln("<tr>");
            out.writeln("  <th>Child Column</th>");
            out.writeln("  <th>Implied Parent Column</th>");
            out.writeln("</tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");

            for (ForeignKeyConstraint impliedConstraint : impliedConstraints) {
                Table childTable = impliedConstraint.getChildTable();
                if (!childTable.isView()) {
                    out.writeln(" <tr>");

                    out.write("  <td class='detail'>");
                    String tableName = childTable.getName();
                    out.write("<a href='tables/");
                    out.write(encodeHref(tableName));
                    out.write(".html'>");
                    out.write(tableName);
                    out.write("</a>.");
                    out.write(ForeignKeyConstraint.toString(impliedConstraint.getChildColumns()));
                    out.writeln("</td>");

                    out.write("  <td class='detail'>");
                    tableName = impliedConstraint.getParentTable().getName();
                    out.write("<a href='tables/");
                    out.write(encodeHref(tableName));
                    out.write(".html'>");
                    out.write(tableName);
                    out.write("</a>.");
                    out.write(ForeignKeyConstraint.toString(impliedConstraint.getParentColumns()));
                    out.writeln("</td>");

                    out.writeln(" </tr>");
                }
            }

            out.writeln("</tbody>");
            out.writeln("</table>");
        }
        writeSummary(numDetected, out);
        out.writeln("<p></li>");
    }

    private void writeUniqueNullables(List<TableColumn> uniqueNullables, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.writeln("<b>Columns that are flagged as both 'nullable' and 'must be unique':</b>");
        writeColumnBasedAnomaly(uniqueNullables, out);
        out.writeln("<p></li>");
    }

    private void writeTablesWithoutIndexes(List<Table> unindexedTables, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.writeln("<b>Tables without indexes:</b>");
        if (!unindexedTables.isEmpty()) {
            out.writeln("<table class='dataTable' border='1' rules='groups'>");
            out.writeln("<colgroup>");
            if (displayNumRows)
                out.writeln("<colgroup>");
            out.writeln("<thead align='left'>");
            out.writeln("<tr>");
            out.write("  <th>Table</th>");
            if (displayNumRows)
                out.write("<th>Rows</th>");
            out.writeln();
            out.writeln("</tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");

            for (Table table : unindexedTables) {
                out.writeln(" <tr>");
                out.write("  <td class='detail'>");
                out.write("<a href='tables/");
                out.write(encodeHref(table.getName()));
                out.write(".html'>");
                out.write(table.getName());
                out.write("</a>");
                out.writeln("</td>");
                if (displayNumRows) {
                    out.write("  <td class='detail' align='right'>");
                    if (!table.isView())
                        out.write(String.valueOf(NumberFormat.getIntegerInstance().format(table.getNumRows())));
                    out.writeln("</td>");
                }
                out.writeln(" </tr>");
            }

            out.writeln("</tbody>");
            out.writeln("</table>");
        }
        writeSummary(unindexedTables.size(), out);
        out.writeln("<p></li>");
    }

    private void writeTablesWithIncrementingColumnNames(List<Table> tables, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.writeln("<b>Tables with incrementing column names, potentially indicating denormalization:</b>");
        if (!tables.isEmpty()) {
            out.writeln("<table class='dataTable' border='1' rules='groups'>");
            out.writeln("<thead align='left'>");
            out.writeln("<tr>");
            out.writeln("  <th>Table</th>");
            out.writeln("</tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");

            for (Table table : tables) {
                out.writeln(" <tr>");
                out.write("  <td class='detail'>");
                out.write("<a href='tables/");
                out.write(encodeHref(table.getName()));
                out.write(".html'>");
                out.write(table.getName());
                out.write("</a>");
                out.writeln("</td>");
                out.writeln(" </tr>");
            }

            out.writeln("</tbody>");
            out.writeln("</table>");
        }
        writeSummary(tables.size(), out);
        out.writeln("<p></li>");
    }

    private void writeTablesWithOneColumn(List<Table> tables, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.write("<b>Tables that contain a single column:</b>");
        if (!tables.isEmpty()) {
            out.writeln("<table class='dataTable' border='1' rules='groups'>");
            out.writeln("<colgroup>");
            out.writeln("<colgroup>");
            out.writeln("<thead align='left'>");
            out.writeln("<tr>");
            out.writeln("  <th>Table</th>");
            out.writeln("  <th>Column</th>");
            out.writeln("</tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");

            for (Table table : tables) {
                out.writeln(" <tr>");
                out.write("  <td class='detail'>");
                out.write("<a href='tables/");
                out.write(encodeHref(table.getName()));
                out.write(".html'>");
                out.write(table.getName());
                out.write("</a></td><td class='detail'>");
                out.write(table.getColumns().get(0).toString());
                out.writeln("</td>");
                out.writeln(" </tr>");
            }

            out.writeln("</tbody>");
            out.writeln("</table>");
        }
        writeSummary(tables.size(), out);
        out.writeln("<p></li>");
    }

    private void writeDefaultNullStrings(List<TableColumn> uniqueNullables, LineWriter out) throws IOException {
        out.writeln("<li>");
        out.writeln("<b>Columns whose default value is the word 'NULL' or 'null', but the SQL NULL value may have been intended:</b>");
        writeColumnBasedAnomaly(uniqueNullables, out);
        out.writeln("<p></li>");
    }

    private void writeColumnBasedAnomaly(List<TableColumn> columns, LineWriter out) throws IOException {
        if (!columns.isEmpty()) {
            out.writeln("<table class='dataTable' border='1' rules='groups'>");
            out.writeln("<thead align='left'>");
            out.writeln("<tr>");
            out.writeln("  <th>Column</th>");
            out.writeln("</tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");
            for (TableColumn column : columns) {
                out.writeln(" <tr>");
                out.write("  <td class='detail'>");
                String tableName = column.getTable().getName();
                out.write("<a href='tables/");
                out.write(encodeHref(tableName));
                out.write(".html'>");
                out.write(tableName);
                out.write("</a>.");
                out.write(column.getName());
                out.writeln("</td>");
                out.writeln(" </tr>");
            }

            out.writeln("</tbody>");
            out.writeln("</table>");
        }
        writeSummary(columns.size(), out);
    }

    private void writeSummary(int numAnomalies, LineWriter out) throws IOException {
        switch (numAnomalies) {
            case 0:
                out.write("<br>Anomaly not detected");
                break;
            case 1:
                out.write("1 instance of anomaly detected");
                break;
            default:
                out.write(numAnomalies + " instances of anomaly detected");
        }
    }

    @Override
    protected void writeFooter(LineWriter out) throws IOException {
        out.writeln("</ul>");
        super.writeFooter(out);
    }

    @Override
    protected boolean isAnomaliesPage() {
        return true;
    }
}
 public class HtmlColumnsPage extends HtmlFormatter {
    private static HtmlColumnsPage instance = new HtmlColumnsPage();

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlColumnsPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlColumnsPage getInstance() {
        return instance;
    }

    /**
     * Returns details about the columns that are displayed on this page.
     *
     * @return
     */
    public List<ColumnInfo> getColumnInfos()
    {
        List<ColumnInfo> columns = new ArrayList<ColumnInfo>();

        columns.add(new ColumnInfo("Table", new ByTableComparator()));
        columns.add(new ColumnInfo("Column", new ByColumnComparator()));
        columns.add(new ColumnInfo("Type", new ByTypeComparator()));
        columns.add(new ColumnInfo("Size", new BySizeComparator()));
        columns.add(new ColumnInfo("Nulls", new ByNullableComparator()));
        columns.add(new ColumnInfo("Auto", new ByAutoUpdateComparator()));
        columns.add(new ColumnInfo("Default", new ByDefaultValueComparator()));

        return columns;
    }

    public class ColumnInfo
    {
        private final String columnName;
        private final Comparator<TableColumn> comparator;

        private ColumnInfo(String columnName, Comparator<TableColumn> comparator)
        {
            this.columnName = columnName;
            this.comparator = comparator;
        }

        public String getColumnName() {
            return columnName;
        }

        public String getLocation() {
            return getLocation(columnName);
        }

        public String getLocation(String colName) {
            return "columns.by" + colName + ".html";
        }

        private Comparator<TableColumn> getComparator() {
            return comparator;
        }

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

    public void write(Database database, Collection<Table> tables, ColumnInfo columnInfo, boolean showOrphansDiagram, LineWriter html) throws IOException {
        Set<TableColumn> columns = new TreeSet<TableColumn>(columnInfo.getComparator());
        Set<TableColumn> primaryColumns = new HashSet<TableColumn>();
        Set<TableColumn> indexedColumns = new HashSet<TableColumn>();

        for (Table table : tables) {
            columns.addAll(table.getColumns());

            primaryColumns.addAll(table.getPrimaryColumns());
            for (TableIndex index : table.getIndexes()) {
                indexedColumns.addAll(index.getColumns());
            }
        }

        writeHeader(database, columns.size(), showOrphansDiagram, columnInfo, html);

        HtmlTablePage formatter = HtmlTablePage.getInstance();

        for (TableColumn column : columns) {
            formatter.writeColumn(column, column.getTable().getName(), primaryColumns, indexedColumns, true, false, html);
        }

        writeFooter(html);
    }

    private void writeHeader(Database db, int numberOfColumns, boolean hasOrphans, ColumnInfo selectedColumn, LineWriter html) throws IOException {
        writeHeader(db, null, "Columns", hasOrphans, html);

        html.writeln("<table width='100%' border='0'>");
        html.writeln("<tr><td class='container'>");
        writeGeneratedBy(db.getConnectTime(), html);
        html.writeln("</td><td class='container' rowspan='2' align='right' valign='top'>");
        writeLegend(false, false, html);
        html.writeln("</td></tr>");
        html.writeln("<tr valign='top'><td class='container' align='left' valign='top'>");
        html.writeln("<p>");
        html.writeln("<form name='options' action=''>");
        html.writeln(" <label for='showComments'><input type=checkbox id='showComments'>Comments</label>");
        html.writeln(" <label for='showLegend'><input type=checkbox checked id='showLegend'>Legend</label>");
        html.writeln("</form>");
        html.writeln("</table>");

        html.writeln("<div class='indent'>");
        html.write("<b>");
        html.write(db.getName());
        if (db.getSchema() != null) {
            html.write('.');
            html.write(db.getSchema());
        }
        html.write(" contains ");
        html.write(String.valueOf(numberOfColumns));
        html.write(" columns</b> - click on heading to sort:");
        Collection<Table> tables = db.getTables();
        boolean hasTableIds = tables.size() > 0 && tables.iterator().next().getId() != null;
        writeMainTableHeader(hasTableIds, selectedColumn, html);
        html.writeln("<tbody valign='top'>");
    }

    public void writeMainTableHeader(boolean hasTableIds, ColumnInfo selectedColumn, LineWriter out) throws IOException {
        boolean onColumnsPage = selectedColumn != null;
        out.writeln("<a name='columns'></a>");
        out.writeln("<table id='columns' class='dataTable' border='1' rules='groups'>");
        int numCols = 6;    // base number of columns
        if (hasTableIds && !onColumnsPage)
            ++numCols;      // for table id
        if (onColumnsPage)
            ++numCols;      // for table name
        else
            numCols += 2;   // for children and parents
        for (int i = 0; i < numCols; ++i)
            out.writeln("<colgroup>");
        out.writeln("<colgroup class='comment'>");

        out.writeln("<thead align='left'>");
        out.writeln("<tr>");
        if (hasTableIds && !onColumnsPage)
            out.writeln(getTH(selectedColumn, "ID", null, "right"));
        if (onColumnsPage)
            out.writeln(getTH(selectedColumn, "Table", null, null));
        out.writeln(getTH(selectedColumn, "Column", null, null));
        out.writeln(getTH(selectedColumn, "Type", null, null));
        out.writeln(getTH(selectedColumn, "Size", null, null));
        out.writeln(getTH(selectedColumn, "Nulls", "Are nulls allowed?", null));
        out.writeln(getTH(selectedColumn, "Auto", "Is column automatically updated?", null));
        out.writeln(getTH(selectedColumn, "Default", "Default value", null));
        if (!onColumnsPage) {
            out.write("  <th title='Columns in tables that reference this column'>");
            out.writeln("<span class='notSortedByColumn'>Children</span></th>");
            out.write("  <th title='Columns in tables that are referenced by this column'>");
            out.writeln("<span class='notSortedByColumn'>Parents</span></th>");
        }
        out.writeln("  <th title='Comments' class='comment'><span class='notSortedByColumn'>Comments</span></th>");
        out.writeln("</tr>");
        out.writeln("</thead>");
    }

    private String getTH(ColumnInfo selectedColumn, String columnName, String title, String align) {
        StringBuilder buf = new StringBuilder("  <th");

        if (align != null) {
            buf.append(" align='");
            buf.append(align);
            buf.append("'");
        }

        if (title != null) {
            buf.append(" title='");
            buf.append(title);
            buf.append("'");
        }

        if (selectedColumn != null) {
            if (selectedColumn.getColumnName().equals(columnName)) {
                buf.append(" class='sortedByColumn'>");
                buf.append(columnName);
            } else {
                buf.append(" class='notSortedByColumn'>");
                buf.append("<a href='");
                buf.append(encodeHref(selectedColumn.getLocation(columnName)));
                buf.append("#columns'><span class='notSortedByColumn'>");
                buf.append(columnName);
                buf.append("</span></a>");
            }
        } else {
            buf.append('>');
            buf.append(columnName);
        }
        buf.append("</th>");

        return buf.toString();
    }

    @Override
    protected void writeFooter(LineWriter html) throws IOException {
        html.writeln("</table>");
        html.writeln("</div>");
        super.writeFooter(html);
    }

    @Override
    protected boolean isColumnsPage() {
        return true;
    }

    private class ByColumnComparator implements Comparator<TableColumn> {
        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.getName().compareToIgnoreCase(column2.getName());
            if (rc == 0)
                rc = column1.getTable().compareTo(column2.getTable());
            return rc;
        }
    }

    private class ByTableComparator implements Comparator<TableColumn> {
        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.getTable().compareTo(column2.getTable());
            if (rc == 0)
                rc = column1.getName().compareToIgnoreCase(column2.getName());
            return rc;
        }
    }

    private class ByTypeComparator implements Comparator<TableColumn> {
        private final Comparator<TableColumn> bySize = new BySizeComparator();

        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.getType().compareToIgnoreCase(column2.getType());
            if (rc == 0) {
                rc = bySize.compare(column1, column2);
            }
            return rc;
        }
    }

    private class BySizeComparator implements Comparator<TableColumn> {
        private final Comparator<TableColumn> byColumn = new ByColumnComparator();

        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.getLength() - column2.getLength();
            if (rc == 0) {
                rc = column1.getDecimalDigits() - column2.getDecimalDigits();
                if (rc == 0)
                    rc = byColumn.compare(column1, column2);
            }
            return rc;
        }
    }

    private class ByNullableComparator implements Comparator<TableColumn> {
        private final Comparator<TableColumn> byColumn = new ByColumnComparator();

        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.isNullable() == column2.isNullable() ? 0 : column1.isNullable() ? -1 : 1;
            if (rc == 0)
                rc = byColumn.compare(column1, column2);
            return rc;
        }
    }

    private class ByAutoUpdateComparator implements Comparator<TableColumn> {
        private final Comparator<TableColumn> byColumn = new ByColumnComparator();

        public int compare(TableColumn column1, TableColumn column2) {
            int rc = column1.isAutoUpdated() == column2.isAutoUpdated() ? 0 : column1.isAutoUpdated() ? -1 : 1;
            if (rc == 0)
                rc = byColumn.compare(column1, column2);
            return rc;
        }
    }

    private class ByDefaultValueComparator implements Comparator<TableColumn> {
        private final Comparator<TableColumn> byColumn = new ByColumnComparator();

        public int compare(TableColumn column1, TableColumn column2) {
            int rc = String.valueOf(column1.getDefaultValue()).compareToIgnoreCase(String.valueOf(column2.getDefaultValue()));
            if (rc == 0)
                rc = byColumn.compare(column1, column2);
            return rc;
        }
    }
}
 public class HtmlConstraintsPage extends HtmlFormatter {
    private static HtmlConstraintsPage instance = new HtmlConstraintsPage();
    private int columnCounter;

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlConstraintsPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlConstraintsPage getInstance() {
        return instance;
    }

    public void write(Database database, List<ForeignKeyConstraint> constraints, Collection<Table> tables, boolean hasOrphans, LineWriter html) throws IOException {
        writeHeader(database, hasOrphans, html);
        writeForeignKeyConstraints(constraints, html);
        writeCheckConstraints(tables, html);
        writeFooter(html);
    }

    private void writeHeader(Database database, boolean hasOrphans, LineWriter html) throws IOException {
        writeHeader(database, null, "Constraints", hasOrphans, html);
        html.writeln("<div class='indent'>");
    }

    @Override
    protected void writeFooter(LineWriter html) throws IOException {
        html.writeln("</div>");
        super.writeFooter(html);
    }

    /**
     * Write specified foreign key constraints
     *
     * @param constraints List
     * @param html LineWriter
     * @throws IOException
     */
    private void writeForeignKeyConstraints(List<ForeignKeyConstraint> constraints, LineWriter html) throws IOException {
        Set<ForeignKeyConstraint> constraintsByName = new TreeSet<ForeignKeyConstraint>();
        constraintsByName.addAll(constraints);

        html.writeln("<table width='100%'>");
        html.writeln("<tr><td class='container' valign='bottom'><b>");
        html.write(String.valueOf(constraintsByName.size()));
        html.writeln(" Foreign Key Constraints:</b>");
        html.writeln("</td><td class='container' align='right'>");
        html.writeln("<table>");
        if (sourceForgeLogoEnabled())
            html.writeln("  <tr><td class='container' align='right' valign='top'><a href='http://sourceforge.net' target='_blank'><img src='http://sourceforge.net/sflogo.php?group_id=137197&amp;type=1' alt='SourceForge.net' border='0' height='31' width='88'></a></td></tr>");
        html.writeln("<tr><td class='container'>");
        writeFeedMe(html);
        html.writeln("</td></tr></table>");
        html.writeln("</td></tr>");
        html.writeln("</table><br>");
        html.writeln("<table class='dataTable' border='1' rules='groups'>");
        html.writeln("<colgroup>");
        html.writeln("<colgroup>");
        html.writeln("<colgroup>");
        html.writeln("<colgroup>");
        html.writeln("<thead align='left'>");
        html.writeln("<tr>");
        html.writeln("  <th>Constraint Name</th>");
        html.writeln("  <th>Child Column</th>");
        html.writeln("  <th>Parent Column</th>");
        html.writeln("  <th>Delete Rule</th>");
        html.writeln("</tr>");
        html.writeln("</thead>");
        html.writeln("<tbody>");
        for (ForeignKeyConstraint constraint : constraintsByName) {
            writeForeignKeyConstraint(constraint, html);
        }
        if (constraints.size() == 0) {
            html.writeln(" <tr>");
            html.writeln("  <td class='detail' valign='top' colspan='4'>None detected</td>");
            html.writeln(" </tr>");
        }
        html.writeln("</tbody>");
        html.writeln("</table>");
    }

    /**
     * Write specified foreign key constraint
     *
     * @param constraint ForeignKeyConstraint
     * @param html LineWriter
     * @throws IOException
     */
    private void writeForeignKeyConstraint(ForeignKeyConstraint constraint, LineWriter html) throws IOException {
        boolean even = columnCounter++ % 2 == 0;
        if (even)
            html.writeln("  <tr class='even'>");
        else
            html.writeln("  <tr class='odd'>");
        html.write("  <td class='detail'>");
        html.write(constraint.getName());
        html.writeln("</td>");
        html.write("  <td class='detail'>");
        for (Iterator<TableColumn> iter = constraint.getChildColumns().iterator(); iter.hasNext(); ) {
            TableColumn column = iter.next();
            html.write("<a href='tables/");
            html.write(encodeHref(column.getTable().getName()));
            html.write(".html'>");
            html.write(column.getTable().getName());
            html.write("</a>");
            html.write(".");
            html.write(column.getName());
            if (iter.hasNext())
                html.write("<br>");
        }
        html.writeln("</td>");
        html.write("  <td class='detail'>");
        for (Iterator<TableColumn> iter = constraint.getParentColumns().iterator(); iter.hasNext(); ) {
            TableColumn column = iter.next();
            html.write("<a href='tables/");
            html.write(encodeHref(column.getTable().getName()));
            html.write(".html'>");
            html.write(column.getTable().getName());
            html.write("</a>");
            html.write(".");
            html.write(column.getName());
            if (iter.hasNext())
                html.write("<br>");
        }
        html.writeln("</td>");
        html.write("  <td class='detail'>");
        String ruleText = constraint.getDeleteRuleDescription();
        String ruleName = constraint.getDeleteRuleName();
        html.write("<span title='" + ruleText + "'>" + ruleName + "&nbsp;</span>");
        html.writeln("</td>");
        html.writeln(" </tr>");
    }

    /**
     * Write check constraints associated with the specified tables
     *
     * @param tables Collection
     * @param html LineWriter
     * @throws IOException
     */
    public void writeCheckConstraints(Collection<Table> tables, LineWriter html) throws IOException {
        html.writeln("<a name='checkConstraints'></a><p>");
        html.writeln("<b>Check Constraints:</b>");
        html.writeln("<TABLE class='dataTable' border='1' rules='groups'>");
        html.writeln("<colgroup>");
        html.writeln("<colgroup>");
        html.writeln("<colgroup>");
        html.writeln("<thead align='left'>");
        html.writeln("<tr>");
        html.writeln("  <th>Table</th>");
        html.writeln("  <th>Constraint Name</th>");
        html.writeln("  <th>Constraint</th>");
        html.writeln("</tr>");
        html.writeln("</thead>");
        html.writeln("<tbody>");

        List<Table> tablesByName = DbAnalyzer.sortTablesByName(new ArrayList<Table>(tables));

        int constraintsWritten = 0;

        // iter over all tables...only ones with check constraints will write anything
        for (Table table : tablesByName) {
            constraintsWritten += writeCheckConstraints(table, html);
        }

        if (constraintsWritten == 0) {
            html.writeln(" <tr>");
            html.writeln("  <td class='detail' valign='top' colspan='3'>None detected</td>");
            html.writeln(" </tr>");
        }

        html.writeln("</tbody>");
        html.writeln("</table>");
    }

    /**
     * Write check constraints associated with the specified table (if any)
     *
     * @param table Table
     * @param html LineWriter
     * @throws IOException
     * @return int
     */
    private int writeCheckConstraints(Table table, LineWriter html) throws IOException {
        Map<String, String> constraints = table.getCheckConstraints();  // constraint name -> text pairs
        int constraintsWritten = 0;
        for (String name : constraints.keySet()) {
            html.writeln(" <tr>");
            html.write("  <td class='detail' valign='top'><a href='tables/");
            html.write(encodeHref(table.getName()));
            html.write(".html'>");
            html.write(table.getName());
            html.write("</a></td>");
            html.write("  <td class='detail' valign='top'>");
            html.write(name);
            html.writeln("</td>");
            html.write("  <td class='detail'>");
            html.write(HtmlEncoder.encodeString(constraints.get(name).toString()));
            html.writeln("</td>");
            html.writeln(" </tr>");
            ++constraintsWritten;
        }

        return constraintsWritten;
    }

    @Override
    protected boolean isConstraintsPage() {
        return true;
    }
}
 public class HtmlMainIndexPage extends HtmlFormatter {
    private static HtmlMainIndexPage instance = new HtmlMainIndexPage();
    private final NumberFormat integerFormatter = NumberFormat.getIntegerInstance();

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlMainIndexPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlMainIndexPage getInstance() {
        return instance;
    }

    public void write(Database database, Collection<Table> tables, boolean showOrphansDiagram, LineWriter html) throws IOException {
        Set<Table> byName = new TreeSet<Table>(new Comparator<Table>() {
            public int compare(Table table1, Table table2) {
                return table1.compareTo(table2);
            }
        });
        byName.addAll(tables);

        boolean showIds = false;
        int numViews = 0;
        boolean comments = false;

        for (Table table : byName) {
            if (table.isView())
                ++numViews;
            showIds |= table.getId() != null;
            if (table.getComments() != null)
                comments = true;
        }

        writeHeader(database, byName.size() - numViews, numViews, showIds, showOrphansDiagram, comments, html);

        int numTableCols = 0;
        int numViewCols = 0;
        long numRows = 0;
        for (Table table : byName) {
            writeLineItem(table, showIds, html);

            if (!table.isView())
                numTableCols += table.getColumns().size();
            else
                numViewCols += table.getColumns().size();
            numRows += table.getNumRows();
        }

        writeFooter(byName.size() - numViews, numTableCols, numViews, numViewCols, numRows, html);
    }

    private void writeHeader(Database db, int numberOfTables, int numberOfViews, boolean showIds, boolean hasOrphans, boolean hasComments, LineWriter html) throws IOException {
        List<String> javascript = new ArrayList<String>();

        // we can't use the hard-coded even odd technique that we use
        // everywhere else because we're dynamically changing the visibility
        // of tables/views within the list
        javascript.add("$(function(){");
        javascript.add("  associate($('#showTables'), $('.tbl'));");
        javascript.add("  associate($('#showViews'),  $('.view'));");
        javascript.add("  jQuery.fn.alternateRowColors = function() {");
        javascript.add("    $('tbody tr:visible').each(function(i) {");
        javascript.add("      if (i % 2 == 0) {");
        javascript.add("        $(this).removeClass('even').addClass('odd');");
        javascript.add("      } else {");
        javascript.add("        $(this).removeClass('odd').addClass('even');");
        javascript.add("      }");
        javascript.add("    });");
        javascript.add("    return this;");
        javascript.add("  };");
        javascript.add("  $('#showTables, #showViews').click(function() {");
        javascript.add("    $('table.dataTable').alternateRowColors();");
        javascript.add("  });");
        javascript.add("  $('table.dataTable').alternateRowColors();");
        javascript.add("})");

        writeHeader(db, null, null, hasOrphans, javascript, html);
        html.writeln("<table width='100%'>");
        html.writeln(" <tr><td class='container'>");
        writeGeneratedBy(db.getConnectTime(), html);
        html.writeln(" </td></tr>");
        html.writeln(" <tr>");
        html.write("  <td class='container'>Database Type: ");
        html.write(db.getDatabaseProduct());
        html.writeln("  </td>");
        html.writeln("  <td class='container' align='right' valign='top' rowspan='3'>");
        if (sourceForgeLogoEnabled())
            html.writeln("    <a href='http://sourceforge.net' target='_blank'><img src='http://sourceforge.net/sflogo.php?group_id=137197&amp;type=1' alt='SourceForge.net' border='0' height='31' width='88'></a><br>");
        html.write("    <br>");
        writeFeedMe(html);
        html.writeln("  </td>");
        html.writeln(" </tr>");
        html.writeln(" <tr>");
        html.write("  <td class='container'>");
        String xmlName = db.getName();
        if (db.getSchema() != null)
            xmlName += '.' + db.getSchema();
        html.write("<br><a href='" + xmlName + ".xml' title='XML Representation'>XML Representation</a>");
        html.write("<br><a href='insertionOrder.txt' title='Useful for loading data into a database'>Insertion Order</a>&nbsp;");
        html.write("<a href='deletionOrder.txt' title='Useful for purging data from a database'>Deletion Order</a>");
        html.write("&nbsp;(for database loading/purging scripts)");
        html.writeln("</td>");
        html.writeln(" </tr>");
        html.writeln("</table>");

        html.writeln("<div class='indent'>");
        html.write("<p>");
        html.write("<b>");
        if (numberOfViews == 0) {
            html.writeln("<label for='showTables' style='display:none;'><input type='checkbox' id='showTables' checked></label>");
        } else if (numberOfTables == 0) {
            html.writeln("<label for='showViews' style='display:none;'><input type='checkbox' id='showViews' checked></label>");
        } else {
            html.write("<label for='showTables'><input type='checkbox' id='showTables' checked>Tables</label>");
            html.write(" <label for='showViews'><input type='checkbox' id='showViews' checked>Views</label>");
        }

        html.writeln(" <label for='showComments'><input type=checkbox " + (hasComments  ? "checked " : "") + "id='showComments'>Comments</label>");
        html.writeln("</b>");

        html.writeln("<table class='dataTable' border='1' rules='groups'>");
        int numGroups = 4 + (showIds ? 1 : 0) + (displayNumRows ? 1 : 0);
        for (int i = 0; i < numGroups; ++i)
            html.writeln("<colgroup>");
        html.writeln("<colgroup class='comment'>");
        html.writeln("<thead align='left'>");
        html.writeln("<tr>");
        String tableHeading;
        if (numberOfViews == 0)
            tableHeading = "Table";
        else if (numberOfTables == 0)
            tableHeading = "View";
        else
            tableHeading = "Table / View";
        html.writeln("  <th valign='bottom'>" + tableHeading + "</th>");
        if (showIds)
            html.writeln("  <th align='center' valign='bottom'>ID</th>");
        html.writeln("  <th align='right' valign='bottom'>Children</th>");
        html.writeln("  <th align='right' valign='bottom'>Parents</th>");
        html.writeln("  <th align='right' valign='bottom'>Columns</th>");
        if (displayNumRows)
            html.writeln("  <th align='right' valign='bottom'>Rows</th>");
        html.writeln("  <th class='comment' align='left' valign='bottom'>Comments</th>");
        html.writeln("</tr>");
        html.writeln("</thead>");
        html.writeln("<tbody>");
    }

    private void writeLineItem(Table table, boolean showIds, LineWriter html) throws IOException {
        html.write(" <tr class='" + (table.isView() ? "view" : "tbl") + "' valign='top'>");
        html.write("  <td class='detail'><a href='tables/");
        html.write(encodeHref(table.getName()));
        html.write(".html'>");
        html.write(table.getName());
        html.writeln("</a></td>");

        if (showIds) {
            html.write("  <td class='detail' align='right'>");
            Object id = table.getId();
            if (id != null)
                html.write(String.valueOf(id));
            else
                html.writeln("&nbsp;");
            html.writeln("</td>");
        }

        html.write("  <td class='detail' align='right'>");
        int numRelatives = table.getNumNonImpliedChildren();
        if (numRelatives != 0)
            html.write(String.valueOf(integerFormatter.format(numRelatives)));
        html.writeln("</td>");
        html.write("  <td class='detail' align='right'>");
        numRelatives = table.getNumNonImpliedParents();
        if (numRelatives != 0)
            html.write(String.valueOf(integerFormatter.format(numRelatives)));
        html.writeln("</td>");

        html.write("  <td class='detail' align='right'>");
        html.write(String.valueOf(integerFormatter.format(table.getColumns().size())));
        html.writeln("</td>");

        if (displayNumRows) {
            html.write("  <td class='detail' align='right'>");
            if (!table.isView())
                html.write(String.valueOf(integerFormatter.format(table.getNumRows())));
            else
                html.write("<span title='Views contain no real rows'>view</span>");
            html.writeln("</td>");
        }
        html.write("  <td class='comment detail'>");
        String comments = table.getComments();
        if (comments != null) {
            if (encodeComments)
                for (int i = 0; i < comments.length(); ++i)
                    html.write(HtmlEncoder.encodeToken(comments.charAt(i)));
            else
                html.write(comments);
        }
        html.writeln("</td>");
        html.writeln("  </tr>");
    }

    protected void writeFooter(int numTables, int numTableCols, int numViews, int numViewCols, long numRows, LineWriter html) throws IOException {
        html.writeln("  <tr>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        if (displayNumRows)
            html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='comment detail'>&nbsp;</td>");
        html.writeln("  </tr>");
        String name = numTables == 1 ? " Table" : " Tables";
        html.writeln("  <tr class='tbl'>");
        html.writeln("    <td class='detail'><b>" + integerFormatter.format(numTables) + name + "</b></td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail' align='right'><b>" + integerFormatter.format(numTableCols) + "</b></td>");
        if (displayNumRows)
            html.writeln("    <td class='detail' align='right'><b>" + integerFormatter.format(numRows) + "</b></td>");
        html.writeln("    <td class='comment detail'>&nbsp;</td>");
        html.writeln("  </tr>");
        name = numViews == 1 ? " View" : " Views";
        html.writeln("  <tr class='view'>");
        html.writeln("    <td class='detail'><b>" + integerFormatter.format(numViews) + name + "</b></td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='detail' align='right'><b>" + integerFormatter.format(numViewCols) + "</b></td>");
        if (displayNumRows)
            html.writeln("    <td class='detail'>&nbsp;</td>");
        html.writeln("    <td class='comment detail'>&nbsp;</td>");
        html.writeln("  </tr>");
        html.writeln("</table>");

        super.writeFooter(html);
    }

    @Override
    protected boolean isMainIndex() {
        return true;
    }
}
 public class HtmlOrphansPage extends HtmlDiagramFormatter {
    private static HtmlOrphansPage instance = new HtmlOrphansPage();

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlOrphansPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlOrphansPage getInstance() {
        return instance;
    }

    public boolean write(Database db, List<Table> orphanTables, File diagramDir, LineWriter html) throws IOException {
        Dot dot = getDot();
        if (dot == null)
            return false;

        Set<Table> orphansWithImpliedRelationships = new HashSet<Table>();

        for (Table table : orphanTables) {
            if (!table.isOrphan(true)){
                orphansWithImpliedRelationships.add(table);
            }
        }

        writeHeader(db, "Utility Tables", !orphansWithImpliedRelationships.isEmpty(), html);

        html.writeln("<a name='diagram'>");
        try {
            StringBuilder maps = new StringBuilder(64 * 1024);

            for (Table table : orphanTables) {
                String dotBaseFilespec = table.getName();

                File dotFile = new File(diagramDir, dotBaseFilespec + ".1degree.dot");
                File imgFile = new File(diagramDir, dotBaseFilespec + ".1degree.png");

                LineWriter dotOut = new LineWriter(dotFile, Config.DOT_CHARSET);
                DotFormatter.getInstance().writeOrphan(table, dotOut);
                dotOut.close();
                try {
                    maps.append(dot.generateDiagram(dotFile, imgFile));
                } catch (Dot.DotFailure dotFailure) {
                    System.err.println(dotFailure);
                    return false;
                }

                html.write("  <img src='diagrams/summary/" + encodeHref( imgFile.getName() ) + "' usemap='#" + table + "' border='0' alt='' align='top'");
                if (orphansWithImpliedRelationships.contains(table))
                    html.write(" class='impliedNotOrphan'");
                html.writeln(">");
            }

            html.write(maps.toString());

            return true;
        } finally {
            html.writeln("</a>");
            writeFooter(html);
        }
    }

    private void writeHeader(Database db, String title, boolean hasImpliedRelationships, LineWriter html) throws IOException {
        writeHeader(db, null, title, true, html);
        html.writeln("<table class='container' width='100%'>");
        html.writeln("<tr><td class='container'>");
        writeGeneratedBy(db.getConnectTime(), html);
        html.writeln("</td>");
        html.writeln("<td class='container' align='right' valign='top' rowspan='2'>");
        writeLegend(false, html);
        html.writeln("</td></tr>");
        html.writeln("<tr><td class='container' align='left' valign='top'>");
        if (hasImpliedRelationships) {
            html.writeln("<form action=''>");
            html.writeln(" <label for='removeImpliedOrphans'><input type=checkbox id='removeImpliedOrphans'>");
            html.writeln("  Hide tables with implied relationships</label>");
            html.writeln("</form>");
        }
        html.writeln("</td></tr></table>");
    }

    @Override
    protected boolean isOrphansPage() {
        return true;
    }
}
 public class HtmlRelationshipsPage extends HtmlDiagramFormatter {
    private static final HtmlRelationshipsPage instance = new HtmlRelationshipsPage();
    private static final boolean fineEnabled = Logger.getLogger(HtmlRelationshipsPage.class.getName()).isLoggable(Level.FINE);

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlRelationshipsPage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlRelationshipsPage getInstance() {
        return instance;
    }

    public boolean write(Database db, File diagramDir, String dotBaseFilespec, boolean hasOrphans, boolean hasRealRelationships, boolean hasImpliedRelationships, Set<TableColumn> excludedColumns, LineWriter html) {
        File compactRelationshipsDotFile = new File(diagramDir, dotBaseFilespec + ".real.compact.dot");
        File compactRelationshipsDiagramFile = new File(diagramDir, dotBaseFilespec + ".real.compact.png");
        File largeRelationshipsDotFile = new File(diagramDir, dotBaseFilespec + ".real.large.dot");
        File largeRelationshipsDiagramFile = new File(diagramDir, dotBaseFilespec + ".real.large.png");
        File compactImpliedDotFile = new File(diagramDir, dotBaseFilespec + ".implied.compact.dot");
        File compactImpliedDiagramFile = new File(diagramDir, dotBaseFilespec + ".implied.compact.png");
        File largeImpliedDotFile = new File(diagramDir, dotBaseFilespec + ".implied.large.dot");
        File largeImpliedDiagramFile = new File(diagramDir, dotBaseFilespec + ".implied.large.png");

        try {
            Dot dot = getDot();
            if (dot == null) {
                writeHeader(db, null, "All Relationships", hasOrphans, html);
                html.writeln("<div class='content'>");
                writeInvalidGraphvizInstallation(html);
                html.writeln("</div>");
                writeFooter(html);
                return false;
            }

            writeHeader(db, "All Relationships", hasOrphans, hasRealRelationships, hasImpliedRelationships, html);
            html.writeln("<table width=\"100%\"><tr><td class=\"container\">");

            if (hasRealRelationships) {
                if (!fineEnabled)
                    System.out.print(".");

                html.writeln(dot.generateDiagram(compactRelationshipsDotFile, compactRelationshipsDiagramFile));
                html.writeln("  <a name='diagram'><img id='realCompactImg' src='diagrams/summary/" + encodeHref( compactRelationshipsDiagramFile.getName() ) + "' usemap='#compactRelationshipsDiagram' class='diagram' border='0' alt=''></a>");

                // we've run into instances where the first diagrams get generated, but then
                // dot fails on the second one...try to recover from that scenario 'somewhat'
                // gracefully
                try {
                    if (!fineEnabled)
                        System.out.print(".");

                    html.writeln(dot.generateDiagram(largeRelationshipsDotFile, largeRelationshipsDiagramFile));
                    html.writeln("  <a name='diagram'><img id='realLargeImg' src='diagrams/summary/" + encodeHref( largeRelationshipsDiagramFile.getName() ) + "' usemap='#largeRelationshipsDiagram' class='diagram' border='0' alt=''></a>");
                } catch (Dot.DotFailure dotFailure) {
                    System.err.println("dot failed to generate all of the relationships diagrams:");
                    System.err.println(dotFailure);
                    System.err.println("...but the relationships page may still be usable.");
                }
            }

            try {
                if (hasImpliedRelationships) {
                    if (!fineEnabled)
                        System.out.print(".");

                    html.writeln(dot.generateDiagram(compactImpliedDotFile, compactImpliedDiagramFile));
                    html.writeln("  <a name='diagram'><img id='impliedCompactImg' src='diagrams/summary/" + encodeHref(compactImpliedDiagramFile.getName()) + "' usemap='#compactImpliedRelationshipsDiagram' class='diagram' border='0' alt=''></a>");

                    if (!fineEnabled)
                        System.out.print(".");

                    html.writeln(dot.generateDiagram(largeImpliedDotFile, largeImpliedDiagramFile));
                    html.writeln("  <a name='diagram'><img id='impliedLargeImg' src='diagrams/summary/" + encodeHref(largeImpliedDiagramFile.getName() ) + "' usemap='#largeImpliedRelationshipsDiagram' class='diagram' border='0' alt=''></a>");
                }
            } catch (Dot.DotFailure dotFailure) {
                System.err.println("dot failed to generate all of the relationships diagrams:");
                System.err.println(dotFailure);
                System.err.println("...but the relationships page may still be usable.");
            }

            if (!fineEnabled)
                System.out.print(".");
            html.writeln("</td></tr></table>");
            writeExcludedColumns(excludedColumns, null, html);

            writeFooter(html);
            return true;
        } catch (Dot.DotFailure dotFailure) {
            System.err.println(dotFailure);
            return false;
        } catch (IOException ioExc) {
            ioExc.printStackTrace();
            return false;
        }
    }

    private void writeHeader(Database db, String title, boolean hasOrphans, boolean hasRealRelationships, boolean hasImpliedRelationships, LineWriter html) throws IOException {
        writeHeader(db, null, title, hasOrphans, html);
        html.writeln("<table class='container' width='100%'>");
        html.writeln("<tr><td class='container'>");
        writeGeneratedBy(db.getConnectTime(), html);
        html.writeln("</td>");
        html.writeln("<td class='container' align='right' valign='top' rowspan='2'>");
        writeLegend(false, html);
        html.writeln("</td></tr>");
        if (!hasRealRelationships) {
            html.writeln("<tr><td class='container' align='left' valign='top'>");
            if (hasImpliedRelationships) {
                html.writeln("No 'real' Foreign Key relationships were detected in the schema.<br>");
                html.writeln("Displayed relationships are implied by a column's name/type/size matching another table's primary key.<p>");
            }
            else
                html.writeln("No relationships were detected in the schema.");
            html.writeln("</td></tr>");
        }
        html.writeln("<tr><td class='container' align='left' valign='top'>");

        html.writeln("<form name='options' action=''>");
        if (hasImpliedRelationships) {
            html.write("  <span ");
            // if no real relationships then hide the 'implied' checkbox and make it 'checked'
            if (!hasRealRelationships)
                html.write("style=\"display:none\" ");
            html.writeln("title=\"Show relationships implied by column name/type/size matching another table's primary key\">");
            html.write("    <label for='implied'><input type='checkbox' id='implied'" + (hasRealRelationships ? "" : " checked" ) + '>');
            html.writeln("Implied relationships</label>");
            html.writeln("  </span>");
        }
        if (hasRealRelationships || hasImpliedRelationships) {
            html.writeln("  <span title=\"By default only columns that are primary keys, foreign keys or indexes are shown\">");
            html.write("    <label for='showNonKeys'><input type='checkbox' id='showNonKeys'>");
            html.writeln("All columns</label>");
            html.writeln("  </span>");
        }
        html.writeln("</form>");

        html.writeln("</td></tr></table>");
    }

    @Override
    protected boolean isRelationshipsPage() {
        return true;
    }
} public class HtmlTablePage extends HtmlFormatter {
    private static final HtmlTablePage instance = new HtmlTablePage();
    private int columnCounter = 0;

    private final Map<String, String> defaultValueAliases = new HashMap<String, String>();
    {
        defaultValueAliases.put("CURRENT TIMESTAMP", "now"); // DB2
        defaultValueAliases.put("CURRENT TIME", "now");      // DB2
        defaultValueAliases.put("CURRENT DATE", "now");      // DB2
        defaultValueAliases.put("SYSDATE", "now");           // Oracle
        defaultValueAliases.put("CURRENT_DATE", "now");      // Oracle
    }

    /**
     * Singleton: Don't allow instantiation
     */
    private HtmlTablePage() {
    }

    /**
     * Singleton accessor
     *
     * @return the singleton instance
     */
    public static HtmlTablePage getInstance() {
        return instance;
    }

    public WriteStats write(Database db, Table table, boolean hasOrphans, File outputDir, WriteStats stats, LineWriter out) throws IOException {
        File diagramsDir = new File(outputDir, "diagrams");
        boolean hasImplied = generateDots(table, diagramsDir, stats);

        writeHeader(db, table, null, hasOrphans, out);
        out.writeln("<table width='100%' border='0'>");
        out.writeln("<tr valign='top'><td class='container' align='left' valign='top'>");
        writeHeader(table, hasImplied, out);
        out.writeln("</td><td class='container' rowspan='2' align='right' valign='top'>");
        writeLegend(true, out);
        out.writeln("</td><tr valign='top'><td class='container' align='left' valign='top'>");
        writeMainTable(table, out);
        writeNumRows(db, table, out);
        out.writeln("</td></tr></table>");
        writeCheckConstraints(table, out);
        writeIndexes(table, out);
        writeView(table, db, out);
        writeDiagram(table, stats, diagramsDir, out);
        writeFooter(out);

        return stats;
    }

    private void writeHeader(Table table, boolean hasImplied, LineWriter html) throws IOException {
        html.writeln("<form name='options' action=''>");
        if (hasImplied) {
            html.write(" <label for='implied'><input type=checkbox id='implied'");
            if (table.isOrphan(false))
                html.write(" checked");
            html.writeln(">Implied relationships</label>");
        }

        // initially show comments if any of the columns contain comments
        boolean showCommentsInitially = false;
        for (TableColumn column : table.getColumns()) {
            if (column.getComments() != null) {
                showCommentsInitially = true;
                break;
            }
        }

        html.writeln(" <label for='showRelatedCols'><input type=checkbox id='showRelatedCols'>Related columns</label>");
        html.writeln(" <label for='showConstNames'><input type=checkbox id='showConstNames'>Constraints</label>");
        html.writeln(" <label for='showComments'><input type=checkbox " + (showCommentsInitially  ? "checked " : "") + "id='showComments'>Comments</label>");
        html.writeln(" <label for='showLegend'><input type=checkbox checked id='showLegend'>Legend</label>");
        html.writeln("</form>");
    }

    public void writeMainTable(Table table, LineWriter out) throws IOException {
        HtmlColumnsPage.getInstance().writeMainTableHeader(table.getId() != null, null, out);

        out.writeln("<tbody valign='top'>");
        Set<TableColumn> primaries = new HashSet<TableColumn>(table.getPrimaryColumns());
        Set<TableColumn> indexedColumns = new HashSet<TableColumn>();
        for (TableIndex index : table.getIndexes()) {
            indexedColumns.addAll(index.getColumns());
        }

        boolean showIds = table.getId() != null;
        for (TableColumn column : table.getColumns()) {
            writeColumn(column, null, primaries, indexedColumns, false, showIds, out);
        }
        out.writeln("</table>");
    }

    public void writeColumn(TableColumn column, String tableName, Set<TableColumn> primaries, Set<TableColumn> indexedColumns, boolean slim, boolean showIds, LineWriter out) throws IOException {
        boolean even = columnCounter++ % 2 == 0;
        if (even)
            out.writeln("<tr class='even'>");
        else
            out.writeln("<tr class='odd'>");

        if (showIds) {
            out.write(" <td class='detail' align='right'>");
            out.write(String.valueOf(column.getId()));
            out.writeln("</td>");
        }
        if (tableName != null) {
            out.write(" <td class='detail'><a href='tables/");
            out.write(encodeHref(tableName));
            out.write(".html'>");
            out.write(tableName);
            out.writeln("</a></td>");
        }
        if (primaries.contains(column))
            out.write(" <td class='primaryKey' title='Primary Key'>");
        else if (indexedColumns.contains(column))
            out.write(" <td class='indexedColumn' title='Indexed'>");
        else
            out.write(" <td class='detail'>");
        out.write(column.getName());
        out.writeln("</td>");
        out.write(" <td class='detail'>");
        out.write(column.getType().toLowerCase());
        out.writeln("</td>");
        out.write(" <td class='detail' align='right'>");
        out.write(column.getDetailedSize());
        out.writeln("</td>");
        out.write(" <td class='detail' align='center'");
        if (column.isNullable())
            out.write(" title='nullable'>&nbsp;&radic;&nbsp;");
        else
            out.write(">");
        out.writeln("</td>");
        out.write(" <td class='detail' align='center'");
        if (column.isAutoUpdated()) {
            out.write(" title='Automatically updated by the database'>&nbsp;&radic;&nbsp;");
        } else {
            out.write(">");
        }
        out.writeln("</td>");

        Object defaultValue = column.getDefaultValue();
        if (defaultValue != null || column.isNullable()) {
            Object alias = defaultValueAliases.get(String.valueOf(defaultValue).trim());
            if (alias != null) {
                out.write(" <td class='detail' align='right' title='");
                out.write(String.valueOf(defaultValue));
                out.write("'><i>");
                out.write(alias.toString());
                out.writeln("</i></td>");
            } else {
                out.write(" <td class='detail' align='right'>");
                out.write(String.valueOf(defaultValue));
                out.writeln("</td>");
            }
        } else {
            out.writeln(" <td class='detail'></td>");
        }
        if (!slim) {
            out.write(" <td class='detail'>");
            String path = tableName == null ? "" : "tables/";
            writeRelatives(column, false, path, even, out);
            out.writeln("</td>");
            out.write(" <td class='detail'>");
            writeRelatives(column, true, path, even, out);
            out.writeln(" </td>");
        }
        out.write(" <td class='comment detail'>");
        String comments = column.getComments();
        if (comments != null) {
            if (encodeComments)
                for (int i = 0; i < comments.length(); ++i)
                    out.write(HtmlEncoder.encodeToken(comments.charAt(i)));
            else
                out.write(comments);
        }
        out.writeln("</td>");
        out.writeln("</tr>");
    }

    /**
     * Write our relatives
     * @param tableName String
     * @param baseRelative TableColumn
     * @param dumpParents boolean
     * @param out LineWriter
     * @throws IOException
     */
    private void writeRelatives(TableColumn baseRelative, boolean dumpParents, String path, boolean even, LineWriter out) throws IOException {
        Set<TableColumn> columns = dumpParents ? baseRelative.getParents() : baseRelative.getChildren();
        final int numColumns = columns.size();
        final String evenOdd = (even ? "even" : "odd");

        if (numColumns > 0) {
            out.newLine();
            out.writeln("  <table border='0' width='100%' cellspacing='0' cellpadding='0'>");
        }

        for (TableColumn column : columns) {
            String columnTableName = column.getTable().getName();
            ForeignKeyConstraint constraint = dumpParents ? column.getChildConstraint(baseRelative) : column.getParentConstraint(baseRelative);
            if (constraint.isImplied())
                out.writeln("   <tr class='impliedRelationship relative " + evenOdd + "' valign='top'>");
            else
                out.writeln("   <tr class='relative " + evenOdd + "' valign='top'>");
            out.write("    <td class='relatedTable detail' title=\"");
            out.write(constraint.toString());
            out.write("\">");
            out.write("<a href='");
            if (!column.getTable().isRemote() || Config.getInstance().isOneOfMultipleSchemas()) {
                out.write(path);
                if (column.getTable().isRemote()) {
                    out.write("../../" + column.getTable().getSchema() + "/tables/");
                }
                out.write(encodeHref(columnTableName));
                out.write(".html");
            }
            out.write("'>");
            out.write(columnTableName);
            out.write("</a>");
            out.write("<span class='relatedKey'>.");
            out.write(column.getName());
            out.writeln("</span>");
            out.writeln("    </td>");

            out.write("    <td class='constraint detail'>");
            out.write(constraint.getName());
            String ruleText = constraint.getDeleteRuleDescription();
            if (ruleText.length() > 0)
            {
                String ruleAlias = constraint.getDeleteRuleAlias();
                out.write("<span title='" + ruleText + "'>&nbsp;" + ruleAlias + "</span>");
            }
            out.writeln("</td>");

            out.writeln("   </tr>");
        }
        if (numColumns > 0) {
            out.writeln("  </table>");
        }
    }

    private void writeNumRows(Database db, Table table, LineWriter out) throws IOException {
        out.write("<p title='" + table.getColumns().size() + " columns'>");
        if (displayNumRows && !table.isView()) {
            out.write("Table contained " + NumberFormat.getIntegerInstance().format(table.getNumRows()) + " rows at ");
        } else {
            out.write("Analyzed at ");
        }
        out.write(db.getConnectTime());
        out.writeln("<p/>");
    }

    private void writeCheckConstraints(Table table, LineWriter out) throws IOException {
        Map<String, String> constraints = table.getCheckConstraints();
        if (constraints != null && !constraints.isEmpty()) {
            out.writeln("<div class='indent'>");
            out.writeln("<b>Requirements (check constraints):</b>");
            out.writeln("<table class='dataTable' border='1' rules='groups'><colgroup><colgroup>");
            out.writeln("<thead>");
            out.writeln(" <tr>");
            out.writeln("  <th>Constraint</th>");
            out.writeln("  <th class='constraint' style='text-align:left;'>Constraint Name</th>");
            out.writeln(" </tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");
            for (String name : constraints.keySet()) {
                out.writeln(" <tr>");
                out.write("  <td class='detail'>");
                out.write(HtmlEncoder.encodeString(constraints.get(name).toString()));
                out.writeln("</td>");
                out.write("  <td class='constraint' style='text-align:left;'>");
                out.write(name);
                out.writeln("</td>");
                out.writeln(" </tr>");
            }
            out.writeln("</table></div><p>");
        }
    }

    private void writeIndexes(Table table, LineWriter out) throws IOException {
        boolean showId = table.getId() != null;
        Set<TableIndex> indexes = table.getIndexes();
        if (indexes != null && !indexes.isEmpty()) {
            // see if we've got any strangeness so we can have the correct number of colgroups
            boolean containsAnomalies = false;
            for (TableIndex index : indexes) {
                containsAnomalies = index.isUniqueNullable();
                if (containsAnomalies)
                    break;
            }

            out.writeln("<div class='indent'>");
            out.writeln("<b>Indexes:</b>");
            out.writeln("<table class='dataTable' border='1' rules='groups'><colgroup><colgroup><colgroup><colgroup>" + (showId ? "<colgroup>" : "") + (containsAnomalies ? "<colgroup>" : ""));
            out.writeln("<thead>");
            out.writeln(" <tr>");
            if (showId)
                out.writeln("  <th>ID</th>");
            out.writeln("  <th>Column(s)</th>");
            out.writeln("  <th>Type</th>");
            out.writeln("  <th>Sort</th>");
            out.writeln("  <th class='constraint' style='text-align:left;'>Constraint Name</th>");
            if (containsAnomalies)
                out.writeln("  <th>Anomalies</th>");
            out.writeln(" </tr>");
            out.writeln("</thead>");
            out.writeln("<tbody>");

            indexes = new TreeSet<TableIndex>(indexes); // sort primary keys first

            for (TableIndex index : indexes) {
                out.writeln(" <tr>");

                if (showId) {
                    out.write("  <td class='detail' align='right'>");
                    out.write(String.valueOf(index.getId()));
                    out.writeln("</td>");
                }

                if (index.isPrimaryKey())
                    out.write("  <td class='primaryKey'>");
                else
                    out.write("  <td class='indexedColumn'>");
                String columns = index.getColumnsAsString();
                if (columns.startsWith("+"))
                    columns = columns.substring(1);
                out.write(columns);
                out.writeln("</td>");

                out.write("  <td class='detail'>");
                out.write(index.getType());
                out.writeln("</td>");

                out.write("  <td class='detail' style='text-align:left;'>");
                Iterator<TableColumn> columnsIter = index.getColumns().iterator();
                while (columnsIter.hasNext()) {
                    TableColumn column = columnsIter.next();
                    if (index.isAscending(column))
                        out.write("<span title='Ascending'>Asc</span>");
                    else
                        out.write("<span title='Descending'>Desc</span>");
                    if (columnsIter.hasNext())
                        out.write("/");
                }
                out.writeln("</td>");

                out.write("  <td class='constraint' style='text-align:left;'>");
                out.write(index.getName());
                out.writeln("</td>");

                if (index.isUniqueNullable()) {
                    if (index.getColumns().size() == 1)
                        out.writeln("  <td class='detail'>This unique column is also nullable</td>");
                    else
                        out.writeln("  <td class='detail'>These unique columns are also nullable</td>");
                } else if (containsAnomalies) {
                    out.writeln("  <td>&nbsp;</td>");
                }
                out.writeln(" </tr>");
            }
            out.writeln("</table>");
            out.writeln("</div>");
        }
    }

    private void writeView(Table table, Database db, LineWriter out) throws IOException {
        String sql;
        if (table.isView() && (sql = table.getViewSql()) != null) {
            Map<String, Table> tables = new CaseInsensitiveMap<Table>();

            for (Table t : db.getTables())
                tables.put(t.getName(), t);
            for (View v : db.getViews())
                tables.put(v.getName(), v);

            Set<Table> references = new TreeSet<Table>();
            String formatted = Config.getInstance().getSqlFormatter().format(sql, db, references);

            out.writeln("<div class='indent spacer'>");
            out.writeln("  View Definition:");
            out.writeln(formatted);
            out.writeln("</div>");
            out.writeln("<div class='spacer'>&nbsp;</div>");

            if (!references.isEmpty()) {
                out.writeln("<div class='indent'>");
                out.writeln("  Possibly Referenced Tables/Views:");
                out.writeln("  <div class='viewReferences'>");
                out.write("  ");
                for (Table t : references) {
                    out.write("<a href='");
                    out.write(encodeHref(t.getName()));
                    out.write(".html'>");
                    out.write(t.getName());
                    out.write("</a>&nbsp;");
                }

                out.writeln("  </div>");
                out.writeln("</div><p/>");
            }

        }
    }

    /**
     * Generate the .dot file(s) to represent the specified table's relationships.
     *
     * Generates a <TABLENAME>.dot if the table has real relatives.
     *
     * Also generates a <TABLENAME>..implied2degrees.dot if the table has implied relatives within
     * two degrees of separation.
     *
     * @param table Table
     * @param diagramsDir File
     * @throws IOException
     * @return boolean <code>true</code> if the table has implied relatives within two
     *                 degrees of separation.
     */
    private boolean generateDots(Table table, File diagramDir, WriteStats stats) throws IOException {
        File oneDegreeDotFile = new File(diagramDir, table.getName() + ".1degree.dot");
        File oneDegreeDiagramFile = new File(diagramDir, table.getName() + ".1degree.png");
        File twoDegreesDotFile = new File(diagramDir, table.getName() + ".2degrees.dot");
        File twoDegreesDiagramFile = new File(diagramDir, table.getName() + ".2degrees.png");
        File impliedDotFile = new File(diagramDir, table.getName() + ".implied2degrees.dot");
        File impliedDiagramFile = new File(diagramDir, table.getName() + ".implied2degrees.png");

        // delete before we start because we'll use the existence of these files to determine
        // if they should be turned into pngs & presented
        oneDegreeDotFile.delete();
        oneDegreeDiagramFile.delete();
        twoDegreesDotFile.delete();
        twoDegreesDiagramFile.delete();
        impliedDotFile.delete();
        impliedDiagramFile.delete();

        if (table.getMaxChildren() + table.getMaxParents() > 0) {
            Set<ForeignKeyConstraint> impliedConstraints;

            DotFormatter formatter = DotFormatter.getInstance();
            LineWriter dotOut = new LineWriter(oneDegreeDotFile, Config.DOT_CHARSET);
            WriteStats oneStats = new WriteStats(stats);
            formatter.writeRealRelationships(table, false, oneStats, dotOut);
            dotOut.close();

            dotOut = new LineWriter(twoDegreesDotFile, Config.DOT_CHARSET);
            WriteStats twoStats = new WriteStats(stats);
            impliedConstraints = formatter.writeRealRelationships(table, true, twoStats, dotOut);
            dotOut.close();

            if (oneStats.getNumTablesWritten() + oneStats.getNumViewsWritten() == twoStats.getNumTablesWritten() + twoStats.getNumViewsWritten()) {
                twoDegreesDotFile.delete(); // no different than before, so don't show it
            }

            if (!impliedConstraints.isEmpty()) {
                dotOut = new LineWriter(impliedDotFile, Config.DOT_CHARSET);
                formatter.writeAllRelationships(table, true, stats, dotOut);
                dotOut.close();
                return true;
            }
        }

        return false;
    }

    private void writeDiagram(Table table, WriteStats stats, File diagramsDir, LineWriter html) throws IOException {
        if (table.getMaxChildren() + table.getMaxParents() > 0) {
            html.writeln("<table width='100%' border='0'><tr><td class='container'>");
            if (HtmlTableDiagrammer.getInstance().write(table, diagramsDir, html)) {
                html.writeln("</td></tr></table>");
                writeExcludedColumns(stats.getExcludedColumns(), table, html);
            } else {
                html.writeln("</td></tr></table><p>");
                writeInvalidGraphvizInstallation(html);
            }
        }
    }

    @Override
    protected String getPathToRoot() {
        return "../";
    }
}
 public class ImageWriter extends ResourceWriter {
    private static ImageWriter instance = new ImageWriter();

    private ImageWriter() {
    }

    public static ImageWriter getInstance() {
        return instance;
    }

    public void writeImages(File outputDir) throws IOException {
        new File(outputDir, "images").mkdir();

        writeResource("/images/tabLeft.gif", new File(outputDir, "/images/tabLeft.gif"));
        writeResource("/images/tabRight.gif", new File(outputDir, "/images/tabRight.gif"));
        writeResource("/images/background.gif", new File(outputDir, "/images/background.gif"));
    }
}

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.Driver;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import net.sourceforge.schemaspy.model.ConnectionFailure;
import net.sourceforge.schemaspy.model.Database;
import net.sourceforge.schemaspy.model.EmptySchemaException;
import net.sourceforge.schemaspy.model.ForeignKeyConstraint;
import net.sourceforge.schemaspy.model.ImpliedForeignKeyConstraint;
import net.sourceforge.schemaspy.model.InvalidConfigurationException;
import net.sourceforge.schemaspy.model.Table;
import net.sourceforge.schemaspy.model.TableColumn;
import net.sourceforge.schemaspy.model.xml.SchemaMeta;
import net.sourceforge.schemaspy.util.ConnectionURLBuilder;
import net.sourceforge.schemaspy.util.DOMUtil;
import net.sourceforge.schemaspy.util.DbSpecificOption;
import net.sourceforge.schemaspy.util.Dot;
import net.sourceforge.schemaspy.util.LineWriter;
import net.sourceforge.schemaspy.util.LogFormatter;
import net.sourceforge.schemaspy.util.PasswordReader;
import net.sourceforge.schemaspy.util.ResourceWriter;
import net.sourceforge.schemaspy.view.DotFormatter;
import net.sourceforge.schemaspy.view.HtmlAnomaliesPage;
import net.sourceforge.schemaspy.view.HtmlColumnsPage;
import net.sourceforge.schemaspy.view.HtmlConstraintsPage;
import net.sourceforge.schemaspy.view.HtmlMainIndexPage;
import net.sourceforge.schemaspy.view.HtmlOrphansPage;
import net.sourceforge.schemaspy.view.HtmlRelationshipsPage;
import net.sourceforge.schemaspy.view.HtmlTablePage;
import net.sourceforge.schemaspy.view.ImageWriter;
import net.sourceforge.schemaspy.view.StyleSheet;
import net.sourceforge.schemaspy.view.TextFormatter;
import net.sourceforge.schemaspy.view.WriteStats;
import net.sourceforge.schemaspy.view.XmlTableFormatter;
import org.w3c.dom.Document;
import org.w3c.dom.Element;

            tables.addAll(db.getViews());

            if (tables.isEmpty()) {
                dumpNoTablesMessage(schema, config.getUser(), meta, config.getTableInclusions() != null);
                if (!config.isOneOfMultipleSchemas()) // don't bail if we're doing the whole enchilada
                    throw new EmptySchemaException();
            }

            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
            DocumentBuilder builder = factory.newDocumentBuilder();
            Document document = builder.newDocument();
            Element rootNode = document.createElement("database");
            document.appendChild(rootNode);
            DOMUtil.appendAttribute(rootNode, "name", dbName);
            if (schema != null)
                DOMUtil.appendAttribute(rootNode, "schema", schema);
            DOMUtil.appendAttribute(rootNode, "type", db.getDatabaseProduct());

            if (config.isHtmlGenerationEnabled()) {
                startSummarizing = System.currentTimeMillis();
                if (!fineEnabled) {
                    System.out.println("(" + (startSummarizing - start) / 1000 + "sec)");
                }

                logger.info("Gathered schema details in " + (startSummarizing - start) / 1000 + " seconds");
                logger.info("Writing/graphing summary");
                System.err.flush();
                System.out.flush();
                if (!fineEnabled) {
                    System.out.print("Writing/graphing summary");
                    System.out.print(".");
                }
                ImageWriter.getInstance().writeImages(outputDir);
                ResourceWriter.getInstance().writeResource("/jquery.js", new File(outputDir, "/jquery.js"));
                ResourceWriter.getInstance().writeResource("/schemaSpy.js", new File(outputDir, "/schemaSpy.js"));
                if (!fineEnabled)
                    System.out.print(".");

                boolean showDetailedTables = tables.size() <= config.getMaxDetailedTables();
                final boolean includeImpliedConstraints = config.isImpliedConstraintsEnabled();

                // if evaluating a 'ruby on rails-based' database then connect the columns
                // based on RoR conventions
                // note that this is done before 'hasRealRelationships' gets evaluated so
                // we get a relationships ER diagram
                if (config.isRailsEnabled())
                    DbAnalyzer.getRailsConstraints(db.getTablesByName());

                File diagramsDir = new File(outputDir, "diagrams/summary");

                // generate the compact form of the relationships .dot file
                String dotBaseFilespec = "relationships";
                out = new LineWriter(new File(diagramsDir, dotBaseFilespec + ".real.compact.dot"), Config.DOT_CHARSET);
                WriteStats stats = new WriteStats(tables);
                DotFormatter.getInstance().writeRealRelationships(db, tables, true, showDetailedTables, stats, out);
                boolean hasRealRelationships = stats.getNumTablesWritten() > 0 || stats.getNumViewsWritten() > 0;
                out.close();

                if (hasRealRelationships) {
                    // real relationships exist so generate the 'big' form of the relationships .dot file
                    if (!fineEnabled)
                        System.out.print(".");
                    out = new LineWriter(new File(diagramsDir, dotBaseFilespec + ".real.large.dot"), Config.DOT_CHARSET);
                    DotFormatter.getInstance().writeRealRelationships(db, tables, false, showDetailedTables, stats, out);
                    out.close();
                }

                // getting implied constraints has a side-effect of associating the parent/child tables, so don't do it
                // here unless they want that behavior
                List<ImpliedForeignKeyConstraint> impliedConstraints = null;
                if (includeImpliedConstraints)
                    impliedConstraints = DbAnalyzer.getImpliedConstraints(tables);
                else
                    impliedConstraints = new ArrayList<ImpliedForeignKeyConstraint>();

                List<Table> orphans = DbAnalyzer.getOrphans(tables);
                boolean hasOrphans = !orphans.isEmpty() && Dot.getInstance().isValid();

                if (!fineEnabled)
                    System.out.print(".");

                File impliedDotFile = new File(diagramsDir, dotBaseFilespec + ".implied.compact.dot");
                out = new LineWriter(impliedDotFile, Config.DOT_CHARSET);
                boolean hasImplied = DotFormatter.getInstance().writeAllRelationships(db, tables, true, showDetailedTables, stats, out);

                Set<TableColumn> excludedColumns = stats.getExcludedColumns();
                out.close();
                if (hasImplied) {
                    impliedDotFile = new File(diagramsDir, dotBaseFilespec + ".implied.large.dot");
                    out = new LineWriter(impliedDotFile, Config.DOT_CHARSET);
                    DotFormatter.getInstance().writeAllRelationships(db, tables, false, showDetailedTables, stats, out);
                    out.close();
                } else {
                    impliedDotFile.delete();
                }

                out = new LineWriter(new File(outputDir, dotBaseFilespec + ".html"), config.getCharset());
                HtmlRelationshipsPage.getInstance().write(db, diagramsDir, dotBaseFilespec, hasOrphans, hasRealRelationships, hasImplied, excludedColumns, out);
                out.close();

                if (!fineEnabled)
                    System.out.print(".");

                dotBaseFilespec = "utilities";
                out = new LineWriter(new File(outputDir, dotBaseFilespec + ".html"), config.getCharset());
                HtmlOrphansPage.getInstance().write(db, orphans, diagramsDir, out);
                out.close();

                if (!fineEnabled)
                    System.out.print(".");

                out = new LineWriter(new File(outputDir, "index.html"), 64 * 1024, config.getCharset());
                HtmlMainIndexPage.getInstance().write(db, tables, hasOrphans, out);
                out.close();

                if (!fineEnabled)
                    System.out.print(".");

                List<ForeignKeyConstraint> constraints = DbAnalyzer.getForeignKeyConstraints(tables);
                out = new LineWriter(new File(outputDir, "constraints.html"), 256 * 1024, config.getCharset());

                HtmlConstraintsPage constraintIndexFormatter = HtmlConstraintsPage.getInstance();
