/* * 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)) catalogTerm = null; //$NON-NLS-1$ schemaTerm = dbmd.getSchemaTerm(); if ("".equals(schemaTerm)) schemaTerm = null; //$NON-NLS-1$ 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+"): no such catalog!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ 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+"): no such schema!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ 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+"): catalog and schema ok; no such table!"); //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$ 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); } }