ca.sqlpower.sqlobject.SQLDatabase.java Source code

Java tutorial

Introduction

Here is the source code for ca.sqlpower.sqlobject.SQLDatabase.java

Source

/*
 * Copyright (c) 2008, SQL Power Group Inc.
 *
 * This file is part of SQL Power Library.
 *
 * SQL Power Library is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * SQL Power Library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>. 
 */
package ca.sqlpower.sqlobject;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.sql.Connection;
import java.sql.DatabaseMetaData;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;

import org.apache.commons.dbcp.ConnectionFactory;
import org.apache.commons.dbcp.PoolableConnectionFactory;
import org.apache.commons.pool.BaseObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool;
import org.apache.commons.pool.impl.GenericObjectPool.Config;
import org.apache.log4j.Logger;

import ca.sqlpower.object.ObjectDependentException;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.object.annotation.Accessor;
import ca.sqlpower.object.annotation.Constructor;
import ca.sqlpower.object.annotation.ConstructorParameter;
import ca.sqlpower.object.annotation.Mutator;
import ca.sqlpower.object.annotation.NonProperty;
import ca.sqlpower.object.annotation.Transient;
import ca.sqlpower.object.annotation.ConstructorParameter.ParameterType;
import ca.sqlpower.sql.JDBCDSConnectionFactory;
import ca.sqlpower.sql.JDBCDataSource;
import ca.sqlpower.sql.SPDataSource;
import ca.sqlpower.sql.jdbcwrapper.DatabaseMetaDataDecorator;

public class SQLDatabase extends SQLObject implements java.io.Serializable, PropertyChangeListener {
    private static Logger logger = Logger.getLogger(SQLDatabase.class);

    /**
     * Defines an absolute ordering of the child types of this class.
     */
    @SuppressWarnings("unchecked")
    public static final List<Class<? extends SPObject>> allowedChildTypes = Collections
            .unmodifiableList(new ArrayList<Class<? extends SPObject>>(
                    Arrays.asList(SQLCatalog.class, SQLSchema.class, SQLTable.class)));

    /**
     * This SPDataSource describes how to connect to the 
     * physical database that backs this SQLDatabase object.
     */
    private JDBCDataSource dataSource;

    /**
     * A pool of JDBC connections backed by a Jakarta Commons DBCP pool.
     * You should access it only via the getConnectionPool() method.
     */
    private transient BaseObjectPool connectionPool;

    /**
     * Tells this database that it is being used to back the PlayPen.  Also 
     * stops removal of children and the closure of connection when properties
     * change.
     */
    private boolean playPenDatabase = false;

    /**
     * Indicates the maximum number of connections held active ever.
     */
    private int maxActiveConnections = 0;

    /**
     * The catalog term for the underlying database, according to the JDBC driver's
     * database meta data at the time this database object was populated. Null means
     * the database does not have catalogs.
     */
    private String catalogTerm;

    /**
     * The schema term for the underlying database, according to the JDBC driver's
     * database meta data at the time this database object was populated. Null means
     * the database does not have schemas.
     */
    private String schemaTerm;

    /**
     * The {@link SQLCatalog} children of this {@link SQLDatabase}.
     */
    private final List<SQLCatalog> catalogs = new ArrayList<SQLCatalog>();

    private final List<SQLSchema> schemas = new ArrayList<SQLSchema>();

    private final List<SQLTable> tables = new ArrayList<SQLTable>();

    /**
     * The internal name of the database if the data source is null or if
     * it is the play pen database.
     */
    private String name;

    @Constructor
    public SQLDatabase(
            @ConstructorParameter(parameterType = ParameterType.PROPERTY, propertyName = "dataSource") JDBCDataSource dataSource) {
        setDataSource(dataSource);
    }

    /**
     * Constructor for non-JDBC connected instances.
     */
    public SQLDatabase() {
        populated = true;
    }

    @NonProperty
    public synchronized boolean isConnected() {
        return connectionPool != null;
    }

    protected synchronized void populateImpl() throws SQLObjectException {
        logger.debug("SQLDatabase: is populated " + populated); //$NON-NLS-1$
        if (populated)
            return;

        logger.debug("SQLDatabase: populate starting"); //$NON-NLS-1$

        Connection con = null;
        ResultSet rs = null;
        final List<SQLCatalog> fetchedCatalogs;
        final List<SQLSchema> fetchedSchemas;
        final List<SQLTable> fetchedTables;
        try {
            con = getConnection();
            DatabaseMetaData dbmd = con.getMetaData();

            catalogTerm = dbmd.getCatalogTerm();
            if ("".equals(catalogTerm)) //$NON-NLS-1$
                catalogTerm = null;

            schemaTerm = dbmd.getSchemaTerm();
            if ("".equals(schemaTerm)) //$NON-NLS-1$
                schemaTerm = null;

            fetchedCatalogs = SQLCatalog.fetchCatalogs(dbmd);

            // If there were no catalogs, we should look for schemas
            // instead (i.e. this database has no catalogs, and schemas
            // may be attached directly to the database)
            if (fetchedCatalogs.isEmpty()) {
                fetchedSchemas = SQLSchema.fetchSchemas(dbmd, null);
            } else {
                fetchedSchemas = null;
            }

            // Finally, look for tables directly under the database (this
            // could be a platform without catalogs or schemas at all)
            if (fetchedCatalogs.isEmpty() && fetchedSchemas.isEmpty()) {
                fetchedTables = SQLTable.fetchTablesForTableContainer(dbmd, "", ""); //$NON-NLS-1$ //$NON-NLS-2$
            } else {
                fetchedTables = null;
            }

        } catch (SQLException e) {
            throw new SQLObjectException(Messages.getString("SQLDatabase.populateFailed"), e); //$NON-NLS-1$
        } finally {
            try {
                if (rs != null)
                    rs.close();
            } catch (SQLException e2) {
                throw new SQLObjectException(Messages.getString("SQLDatabase.closeRSFailed"), e2); //$NON-NLS-1$
            }
            try {
                if (con != null)
                    con.close();
            } catch (SQLException e2) {
                throw new SQLObjectException(Messages.getString("SQLDatabase.closeConFailed"), e2); //$NON-NLS-1$
            }
        }
        runInForeground(new Runnable() {

            public void run() {
                synchronized (SQLDatabase.this) {
                    if (populated)
                        return;
                    if (!fetchedCatalogs.isEmpty()) {
                        populateDatabaseWithList(SQLDatabase.this, fetchedCatalogs);
                    } else if (!fetchedSchemas.isEmpty()) {
                        populateDatabaseWithList(SQLDatabase.this, fetchedSchemas);
                    } else if (!fetchedTables.isEmpty()) {
                        populateDatabaseWithList(SQLDatabase.this, fetchedTables);
                    }
                }
            }
        });

        logger.debug("SQLDatabase: populate finished"); //$NON-NLS-1$
    }

    /**
     * Populates the SQLDatabase with a given list of children. This must be
     * done on the foreground thread. The list of children must be of one type
     * only, {@link SQLCatalog}, {@link SQLSchema} or {@link SQLTable}.
     * <p>
     * Package private for use in the {@link SQLObjectUtils}.
     * 
     * @param db
     *            The database to populate
     * @param children
     *            The list of children to add as children. All objects in this
     *            list must be of the same type.
     */
    static void populateDatabaseWithList(SQLDatabase db, List<? extends SQLObject> children) {
        try {

            if (children.isEmpty())
                return;

            Class<? extends SPObject> childType;
            int index;
            if (children.get(0) instanceof SQLCatalog) {
                index = db.catalogs.size();
                childType = SQLCatalog.class;
                for (SQLObject cat : children) {
                    db.catalogs.add((SQLCatalog) cat);
                    cat.setParent(db);
                }
            } else if (children.get(0) instanceof SQLSchema) {
                index = db.schemas.size();
                childType = SQLSchema.class;
                for (SQLObject schema : children) {
                    db.schemas.add((SQLSchema) schema);
                    schema.setParent(db);
                }
            } else if (children.get(0) instanceof SQLTable) {
                index = db.tables.size();
                childType = SQLTable.class;
                for (SQLObject table : children) {
                    db.tables.add((SQLTable) table);
                    table.setParent(db);
                }
            } else {
                throw new IllegalArgumentException(
                        "Database " + db + " cannot have children " + "of type " + children.get(0).getClass());
            }

            db.populated = true;
            db.begin("Populating Database " + db);
            for (SQLObject o : children) {
                db.fireChildAdded(childType, o, index);
                index++;
            }
            db.firePropertyChange("populated", false, true);
            db.commit();
        } catch (Exception e) {
            db.rollback(e.getMessage());
            if (children.get(0) instanceof SQLCatalog) {
                for (SQLObject cat : children) {
                    db.catalogs.remove((SQLCatalog) cat);
                }
            } else if (children.get(0) instanceof SQLSchema) {
                for (SQLObject schema : children) {
                    db.schemas.remove((SQLSchema) schema);
                }
            } else if (children.get(0) instanceof SQLTable) {
                for (SQLObject table : children) {
                    db.tables.remove((SQLTable) table);
                }
            }
            db.populated = false;
            throw new RuntimeException(e);
        }
    }

    @NonProperty
    public SQLCatalog getCatalogByName(String catalogName) throws SQLObjectException {
        populate();
        if (getChildrenWithoutPopulating().isEmpty()) {
            return null;
        }
        if (catalogs.isEmpty()) {
            // this database doesn't contain catalogs!
            return null;
        }
        for (SQLCatalog child : catalogs) {
            if (child.getName().equalsIgnoreCase(catalogName)) {
                return child;
            }
        }
        return null;
    }

    /**
     * Searches for the named schema as a direct child of this
     * database, or as a child of any catalog of this database.
     *
     * <p>Note: there may be more than one schema with the given name,
     * if your RDBMS supports catalogs.  In that case, use {@link
     * SQLCatalog#getSchemaByName} or write another version of this
     * method that return an array of SQLSchema.
     *
     * @return the first SQLSchema whose name matches the given schema
     * name.
     */
    @NonProperty
    public SQLSchema getSchemaByName(String schemaName) throws SQLObjectException {
        populate();
        if (getChildrenWithoutPopulating().isEmpty()) {
            return null;
        }
        if (schemas.isEmpty() && catalogs.isEmpty()) {
            // this database doesn't contain schemas or catalogs!
            return null;
        }
        for (SQLObject child : getChildren()) {
            if (child instanceof SQLCatalog) {
                // children are tables or schemas
                SQLSchema schema = ((SQLCatalog) child).findSchemaByName(schemaName);
                if (schema != null) {
                    return schema;
                }
            } else if (child instanceof SQLSchema) {
                boolean match = (child.getName() == null ? schemaName == null
                        : child.getName().equalsIgnoreCase(schemaName));
                if (match) {
                    return (SQLSchema) child;
                }
            } else {
                throw new IllegalStateException(
                        "Database contains a mix of schemas or catalogs with other objects"); //$NON-NLS-1$
            }
        }
        return null;
    }

    @NonProperty
    public SQLTable getTableByName(String tableName) throws SQLObjectException {
        return getTableByName(null, null, tableName);
    }

    /**
     * Searches this database's list of tables for one with the given
     * name, ignoring case because SQL isn't (usually) case sensitive.
     *
     * @param catalogName The name of the catalog to search, or null
     * if you want to search all catalogs.
     * @param schemaName The name of the schema to search (in this
     * database or in the given catalog) or null to search all
     * schemas.
     * @param tableName The name of the table to look for (null is not
     * allowed).
     * @return the first SQLTable with the given name, or null if no
     * such table exists.
     */
    @NonProperty
    public SQLTable getTableByName(String catalogName, String schemaName, String tableName)
            throws SQLObjectException {

        this.populate();

        if (tableName == null || tableName.length() == 0) {
            throw new NullPointerException("Table Name must be specified"); //$NON-NLS-1$
        }

        // we will recursively search a target (database, catalog, or schema)
        SQLObject target = this;

        if (catalogName != null && catalogName.length() > 0) {
            target = getCatalogByName(catalogName);
        }

        // no such catalog?
        if (target == null) {
            if (logger.isDebugEnabled())
                logger.debug("getTableByName(" + catalogName + "," + schemaName + "," + tableName //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
                        + "): no such catalog!"); //$NON-NLS-1$
            return null;
        }

        if (schemaName != null && schemaName.length() > 0) {
            if (target instanceof SQLDatabase) {
                target = ((SQLDatabase) target).getSchemaByName(schemaName);
            } else if (target instanceof SQLCatalog) {
                target = ((SQLCatalog) target).findSchemaByName(schemaName);
            } else {
                throw new IllegalStateException("Oops, somebody forgot to update this!"); //$NON-NLS-1$
            }
        }

        // no such schema or catalog.schema?
        if (target == null) {
            if (logger.isDebugEnabled())
                logger.debug("getTableByName(" + catalogName + "," + schemaName + "," + tableName //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
                        + "): no such schema!"); //$NON-NLS-1$
            return null;
        }
        target.populate();
        for (SQLObject child : target.getChildren()) {
            if (child instanceof SQLTable) {
                SQLTable table = (SQLTable) child;
                if (table.getName().equalsIgnoreCase(tableName)) {
                    return table;
                }
            } else if (child instanceof SQLCatalog) {
                SQLTable table = ((SQLCatalog) child).getTableByName(tableName);
                if (table != null) {
                    return table;
                }
            } else if (child instanceof SQLSchema) {
                SQLTable table = ((SQLSchema) child).findTableByName(tableName);
                if (table != null) {
                    return table;
                }
            }
        }

        if (logger.isDebugEnabled())
            logger.debug("getTableByName(" + catalogName + "," + schemaName + "," + tableName //$NON-NLS-1$//$NON-NLS-2$//$NON-NLS-3$
                    + "): catalog and schema ok; no such table!"); //$NON-NLS-1$
        return null;
    }

    // ---------------------- SQLObject support ------------------------

    @Override
    @Accessor
    public String getName() {
        if (dataSource != null && !playPenDatabase) {
            return dataSource.getDisplayName();
        } else {
            return name;
        }
    }

    /**
     * Sets the data source name if the data source is not null
     */
    @Override
    @Mutator
    public void setName(String argName) {
        String oldName = getName();
        if (dataSource != null) {
            dataSource.setName(argName);
        }
        name = argName;
        firePropertyChange("name", oldName, argName);
    }

    @Transient
    @Accessor
    public String getShortDisplayName() {
        return getName();
    }

    /**
     * Determines whether this SQL object is a container for catalog
     *
     * @return true (the default) if there are no children; false if
     * the first child is not of type SQLCatlog.
     */
    @NonProperty
    public boolean isCatalogContainer() throws SQLObjectException {
        if (getParent() != null) {
            populate();
        }

        return (getChildrenWithoutPopulating().isEmpty() || !catalogs.isEmpty());
    }

    /**
     * Determines whether this SQL object is a container for schemas
     *
     * @return true (the default) if there are no children; false if
     * the first child is not of type SQLSchema.
     */
    @NonProperty
    public boolean isSchemaContainer() throws SQLObjectException {
        if (getParent() != null) {
            populate();
        }

        // catalog has been populated
        return (getChildrenWithoutPopulating().isEmpty() || !schemas.isEmpty());
    }

    // ----------------- accessors and mutators -------------------

    /**
     * Recursively searches this database for SQLTable descendants, compiles a
     * list of those that were found, and returns that list.
     * 
     * @return a list of all the tables in this database (which may exist under
     *         catalogs and schemas). You are free to modify the returned list,
     *         but doing so will not affect the contents of this database.
     */
    @NonProperty
    public List<SQLTable> getTables() throws SQLObjectException {
        return getTableDescendants(this);
    }

    /**
     * This is the recursive subroutine used by {@link #getTables}. It is
     * preferable to use this algorithm to discover all the tables rather than
     * the generic {@link SQLObjectUtils#findDescendentsByClass(SQLObject, Class, List)}
     * because this one does not cause all the tables in the database to populate.
     */
    private static List<SQLTable> getTableDescendants(SQLObject o) throws SQLObjectException {

        // this seemingly redundant short-circuit is required because
        // we don't want o.getChildren() to be null
        if (!o.allowsChildren())
            return Collections.emptyList();

        List<SQLTable> tables = new LinkedList<SQLTable>();
        for (SQLObject c : o.getChildren()) {
            if (c instanceof SQLTable) {
                tables.add((SQLTable) c);
            } else {
                tables.addAll(getTableDescendants(c));
            }
        }
        return tables;
    }

    /**
     * Gets the value of dataSource
     *
     * @return the value of dataSource
     */
    @Accessor
    public JDBCDataSource getDataSource() {
        return this.dataSource;
    }

    /**
     * Sets the value of dataSource
     *
     * @param argDataSource Value to assign to this.dataSource
     */
    @Mutator
    public void setDataSource(JDBCDataSource argDataSource) {
        SPDataSource oldDataSource = this.dataSource;
        begin("Resetting Database");
        if (dataSource != null) {
            dataSource.removePropertyChangeListener(this);
            if (isMagicEnabled()) {
                reset();
            }
        }
        dataSource = argDataSource;
        if (dataSource != null) {
            dataSource.addPropertyChangeListener(this);
        }
        if (playPenDatabase && isMagicEnabled()) {
            setName(Messages.getString("SQLDatabase.playPenDB")); //$NON-NLS-1$
        } else if (dataSource == null && !playPenDatabase && isMagicEnabled()) {
            setName(Messages.getString("SQLDatabase.disconnected")); //$NON-NLS-1$
        }
        firePropertyChange("dataSource", oldDataSource, argDataSource); //$NON-NLS-1$
        commit();
    }

    @Mutator
    public void setPlayPenDatabase(boolean v) {
        try {
            fireTransactionStarted("Setting database to be the play pen database");
            boolean oldValue = playPenDatabase;
            playPenDatabase = v;
            if (oldValue != v) {
                firePropertyChange("playPenDatabase", oldValue, v); //$NON-NLS-1$
            }
            if (playPenDatabase && isMagicEnabled()) {
                setName(Messages.getString("SQLDatabase.playPenDB")); //$NON-NLS-1$
            } else if (!playPenDatabase && dataSource == null && isMagicEnabled()) {
                setName(Messages.getString("SQLDatabase.disconnected")); //$NON-NLS-1$
            }
            fireTransactionEnded();
        } catch (Throwable t) {
            fireTransactionRollback("Failed due to " + t.getMessage());
            throw new RuntimeException(t);
        }
    }

    @Accessor
    public boolean isPlayPenDatabase() {
        return playPenDatabase;
    }

    /**
     * Removes all children, closes and discards the JDBC connection.  
     * Unless {@link #playPenDatabase} is true
     */
    protected synchronized void reset() {

        if (playPenDatabase) {
            // preserve the objects that are in the Target system when
            // the connection spec changes
            logger.debug("Ignoring Reset request for: " + getDataSource()); //$NON-NLS-1$
            populated = true;
        } else {
            // discard everything and reload (this is generally for source systems)
            logger.debug("Resetting: " + getDataSource()); //$NON-NLS-1$
            // tear down old connection stuff
            try {
                begin("Resetting Database " + this);
                for (int i = getChildrenWithoutPopulating().size() - 1; i >= 0; i--) {
                    removeChild(getChildrenWithoutPopulating().get(i));
                }
                populated = false;
                commit();
            } catch (IllegalArgumentException e) {
                rollback(e.getMessage());
                throw new RuntimeException(e);
            } catch (ObjectDependentException e) {
                rollback(e.getMessage());
                throw new RuntimeException(e);
            }
        }

        // destroy connection pool in either case (it still points to the old data source)
        if (connectionPool != null) {
            try {
                connectionPool.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        connectionPool = null;
    }

    /**
     * Listens for changes in DBCS properties, and resets this
     * SQLDatabase if a critical property (url, driver, username)
     * changes.
     */
    public void propertyChange(PropertyChangeEvent e) {
        String pn = e.getPropertyName();
        if ((e.getOldValue() == null && e.getNewValue() != null)
                || (e.getOldValue() != null && e.getNewValue() == null) || (e.getOldValue() != null
                        && e.getNewValue() != null && !e.getOldValue().equals(e.getNewValue()))) {
            if ("url".equals(pn) || "driverClass".equals(pn) || "user".equals(pn)) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
                reset();
            } else if ("name".equals(pn)) { //$NON-NLS-1$
                firePropertyChange("shortDisplayName", e.getOldValue(), e.getNewValue()); //$NON-NLS-1$
            }
        }
    }

    /**
     * Returns a JDBC connection to the backing database, if there
     * is one.  The connection that you get will be yours and only yours
     * until you call close() on it.  To maximize efficiency of the pool,
     * try to call close() as soon as you are done with the connection.
     *
     * @return an open connection if this database has a valid
     * dataSource; null if this is a dummy database (such as the
     * playpen instance).
     */
    @NonProperty
    public Connection getConnection() throws SQLObjectException {
        if (dataSource == null) {
            return null;
        } else {
            try {
                int newActiveCount = getConnectionPool().getNumActive() + 1;
                maxActiveConnections = Math.max(maxActiveConnections, newActiveCount);
                if (logger.isDebugEnabled()) {
                    logger.debug("getConnection(): giving out active connection " + newActiveCount); //$NON-NLS-1$
                    for (StackTraceElement ste : Thread.currentThread().getStackTrace()) {
                        logger.debug(ste.toString());
                    }
                }
                return (Connection) getConnectionPool().borrowObject();
            } catch (Exception e) {
                final SQLObjectException ex = new SQLObjectException(
                        "Couldn't connect to database: " + e.getMessage(), e); //$NON-NLS-1$
                runInForeground(new Runnable() {
                    public void run() {
                        try {
                            setChildrenInaccessibleReason(ex, SQLObject.class, false);
                        } catch (SQLObjectException e) {
                            throw new SQLObjectRuntimeException(e);
                        }
                    }
                });
                throw ex;
            }
        }
    }

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

    /**
     * Closes all connections and other resources that were allocated
     * by the connect() method.  Logs, but does not propogate, SQL exceptions.
     */
    public void disconnect() {
        if (dataSource != null) {
            dataSource.removePropertyChangeListener(this);
        }
        try {
            if (connectionPool != null) {
                connectionPool.close();
            }
        } catch (Exception ex) {
            logger.error("Error closing connection pool", ex); //$NON-NLS-1$
        } finally {
            connectionPool = null;
        }
        maxActiveConnections = 0;
    }

    synchronized BaseObjectPool getConnectionPool() {
        if (connectionPool == null) {
            Config poolConfig = new GenericObjectPool.Config();
            poolConfig.maxActive = 5;
            poolConfig.whenExhaustedAction = GenericObjectPool.WHEN_EXHAUSTED_FAIL;
            connectionPool = new GenericObjectPool(null, poolConfig);
            ConnectionFactory cf = new JDBCDSConnectionFactory(dataSource);
            new PoolableConnectionFactory(cf, connectionPool, null, null, false, true);
        }
        return connectionPool;
    }

    /**
     * Returns the maximum number of active connections that
     * this database has ever opened. 
     * @return Maximum number of active connections ever opened.
     */
    @Transient
    @Accessor
    public int getMaxActiveConnections() {
        return maxActiveConnections;
    }

    /**
     * Re-reads the definition of all populated objects for this entire
     * database. This involves a lot of recursion and re-populating. When
     * SQLObjects are added, removed, or modified as a result of this process,
     * they will fire the appropriate SQLObject events. The entire operation
     * happens in the context of a single compound event.
     * <p>
     * The refresh will not cause any additional SQLObjects to become populated,
     * although it may end up removing some objects that were already populated
     * (because they were dropped in the physical database).
     */
    public void refresh() throws SQLObjectException {
        if (!populated) {
            logger.info("Not refreshing unpopulated database " + getName()); //$NON-NLS-1$
            return;
        }

        DatabaseMetaDataDecorator.putHint(DatabaseMetaDataDecorator.CACHE_STALE_DATE, new Date());

        // We're going to just leave caching on all the time and see how it pans out
        DatabaseMetaDataDecorator.putHint(DatabaseMetaDataDecorator.CACHE_TYPE,
                DatabaseMetaDataDecorator.CacheType.EAGER_CACHE);

        Connection con = null;
        try {
            runInForeground(new Runnable() {
                public void run() {
                    begin(Messages.getString("SQLDatabase.refreshDatabase", getName())); //$NON-NLS-1$
                }
            });

            con = getConnection();
            DatabaseMetaData dbmd = con.getMetaData();

            if (catalogTerm != null) {
                logger.debug("refresh: catalogTerm is '" + catalogTerm + "'. refreshing catalogs!"); //$NON-NLS-1$ //$NON-NLS-2$
                final List<SQLCatalog> newCatalogs = SQLCatalog.fetchCatalogs(dbmd);
                runInForeground(new Runnable() {
                    public void run() {
                        try {
                            SQLObjectUtils.refreshChildren(SQLDatabase.this, newCatalogs, SQLCatalog.class);
                        } catch (SQLObjectException e) {
                            rollback(e.getMessage());
                            throw new SQLObjectRuntimeException(e);
                        } catch (RuntimeException e) {
                            rollback(e.getMessage());
                            throw e;
                        }
                    }
                });
            } else if (schemaTerm != null) {
                logger.debug("refresh: schemaTerm is '" + schemaTerm + "'. refreshing schemas!"); //$NON-NLS-1$ //$NON-NLS-2$
                final List<SQLSchema> newSchemas = SQLSchema.fetchSchemas(dbmd, null);
                runInForeground(new Runnable() {
                    public void run() {
                        try {
                            SQLObjectUtils.refreshChildren(SQLDatabase.this, newSchemas, SQLSchema.class);
                        } catch (SQLObjectException e) {
                            rollback(e.getMessage());
                            throw new SQLObjectRuntimeException(e);
                        } catch (RuntimeException e) {
                            rollback(e.getMessage());
                            throw e;
                        }
                    }
                });
            }

            // close connection before invoking the super refresh,
            // which will probably want to open another one
            con.close();
            con = null;

            // bootstrap: if this database is a catalog or schema container, 
            super.refresh();

            runInForeground(new Runnable() {
                public void run() {
                    commit();
                }
            });
        } catch (final SQLException e) {
            runInForeground(new Runnable() {
                public void run() {
                    rollback(e.getMessage());
                }
            });
            throw new SQLObjectException(Messages.getString("SQLDatabase.refreshFailed"), e); //$NON-NLS-1$
        } catch (final RuntimeException e) {
            runInForeground(new Runnable() {
                public void run() {
                    rollback(e.getMessage());
                }
            });
            throw e;
        } finally {
            try {
                if (con != null)
                    con.close();
            } catch (SQLException ex) {
                logger.warn("Failed to close connection. Squishing this exception:", ex); //$NON-NLS-1$
            }
        }
    }

    /**
     * Returns all the relationships under this database. Beware of calling this
     * method if this SQLDatabase instance is lazy-loading from a physical
     * database, because it will cause all objects underneath to fully populate!
     * 
     * @return a collection of all the SQLRelationship objects that exist within
     *         this database.
     * @throws SQLObjectException
     *             if this is a lazy-loading database and populating any of its
     *             objects fails for any reason.
     */
    @NonProperty
    public Collection<SQLRelationship> getRelationships() throws SQLObjectException {
        List<SQLRelationship> allRelationships = SQLObjectUtils.findDescendentsByClass(this, SQLRelationship.class,
                new ArrayList<SQLRelationship>());

        // relationships appear in two places within the SQLObject tree, so
        // we have to uniquify the list before returning it
        Set<SQLRelationship> uniqueRelationships = new HashSet<SQLRelationship>(allRelationships);

        return uniqueRelationships;
    }

    @NonProperty
    public List<? extends SQLObject> getChildrenWithoutPopulating() {
        List<SQLObject> children = new ArrayList<SQLObject>();
        children.addAll(catalogs);
        children.addAll(schemas);
        children.addAll(tables);
        return Collections.unmodifiableList(children);
    }

    @Override
    protected boolean removeChildImpl(SPObject child) {
        if (child instanceof SQLCatalog) {
            return removeCatalog((SQLCatalog) child);
        } else if (child instanceof SQLSchema) {
            return removeSchema((SQLSchema) child);
        } else if (child instanceof SQLTable) {
            return removeTable((SQLTable) child);
        } else {
            throw new IllegalArgumentException(
                    "Cannot remove children of type " + child.getClass() + " from " + getName());
        }
    }

    public boolean removeCatalog(SQLCatalog child) {
        if (child.getParent() != this) {
            throw new IllegalStateException("Cannot remove child " + child.getName() + " of type "
                    + child.getClass() + " as its parent is not " + getName());
        }
        int index = catalogs.indexOf(child);
        if (index != -1) {
            catalogs.remove(index);
            fireChildRemoved(SQLCatalog.class, child, index);
            child.setParent(null);
            return true;
        }
        return false;
    }

    public boolean removeSchema(SQLSchema child) {
        if (isMagicEnabled() && child.getParent() != this) {
            throw new IllegalStateException("Cannot remove child " + child.getName() + " of type "
                    + child.getClass() + " as its parent is not " + getName());
        }
        int index = schemas.indexOf(child);
        if (index != -1) {
            schemas.remove(index);
            fireChildRemoved(SQLSchema.class, child, index);
            child.setParent(null);
            return true;
        }
        return false;
    }

    public boolean removeTable(SQLTable child) {
        if (isMagicEnabled() && child.getParent() != this) {
            throw new IllegalStateException("Cannot remove child " + child.getName() + " of type "
                    + child.getClass() + " as its parent is not " + getName());
        }
        child.removeNotify();
        int index = tables.indexOf(child);
        if (index != -1) {
            try {
                begin("Removing a table and setting its parent to null.");
                tables.remove(index);
                fireChildRemoved(SQLTable.class, child, index);
                child.setParent(null);
                commit();
                return true;
            } catch (RuntimeException e) {
                rollback(e.getMessage());
                throw e;
            }
        }
        return false;
    }

    @NonProperty
    public List<? extends SPObject> getDependencies() {
        return Collections.emptyList();
    }

    public void removeDependency(SPObject dependency) {
        for (SQLObject child : getChildren()) {
            child.removeDependency(dependency);
        }
    }

    @Override
    protected void addChildImpl(SPObject child, int index) {
        if (child instanceof SQLCatalog) {
            addCatalog((SQLCatalog) child, index);
        } else if (child instanceof SQLSchema) {
            addSchema((SQLSchema) child, index);
        } else if (child instanceof SQLTable) {
            addTable((SQLTable) child, index);
        } else {
            throw new IllegalArgumentException("The child " + child.getName() + " of type " + child.getClass()
                    + " is not a valid child type of " + getClass() + ".");
        }
    }

    public void addCatalog(SQLCatalog catalog) {
        addCatalog(catalog, catalogs.size());
    }

    public void addCatalog(SQLCatalog catalog, int index) {
        catalogs.add(index, catalog);
        catalog.setParent(this);
        fireChildAdded(SQLCatalog.class, catalog, index);
    }

    public void addSchema(SQLSchema schema) {
        addSchema(schema, schemas.size());
    }

    public void addSchema(SQLSchema schema, int index) {
        schemas.add(index, schema);
        schema.setParent(this);
        fireChildAdded(SQLSchema.class, schema, index);
    }

    public void addTable(SQLTable table) {
        addTable(table, tables.size());
    }

    public void addTable(SQLTable table, int index) {
        tables.add(index, table);
        table.setParent(this);
        fireChildAdded(SQLTable.class, table, index);
    }

    /**
     * The child types for a database change as children are added. This is
     * different from most other cases but the order of the allowed children
     * will remain the same as the order specified by {@link #allowedChildTypes}.
     */
    @NonProperty
    public List<Class<? extends SPObject>> getAllowedChildTypes() {
        List<Class<? extends SPObject>> types = new ArrayList<Class<? extends SPObject>>();
        if (schemas.isEmpty() && tables.isEmpty()) {
            types.add(SQLCatalog.class);
        }
        if (catalogs.isEmpty() && tables.isEmpty()) {
            types.add(SQLSchema.class);
        }
        if (catalogs.isEmpty() && schemas.isEmpty()) {
            types.add(SQLTable.class);
        }
        return Collections.unmodifiableList(types);
    }
}