/*
* 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.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.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
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.ConstructorParameter.ParameterType;
import ca.sqlpower.object.annotation.Mutator;
import ca.sqlpower.object.annotation.NonBound;
import ca.sqlpower.object.annotation.NonProperty;
import ca.sqlpower.object.annotation.Transient;
import ca.sqlpower.sql.CachedRowSet;
import ca.sqlpower.sqlobject.SQLIndex.Column;
import ca.sqlpower.sqlobject.SQLRelationship.SQLImportedKey;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.SessionNotFoundException;
import com.google.common.collect.ListMultimap;
public class SQLTable extends SQLObject {
/**
* 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(SQLColumn.class, SQLRelationship.class,
SQLImportedKey.class, SQLIndex.class)));
private static Logger logger = Logger.getLogger(SQLTable.class);
/**
* These are the different ways of transferring an object
* to/in the play pen. An object could be copied from one
* place to another or reverse engineered. These two
* operations differ slightly in what they create and
* inherit.
*/
public enum TransferStyles {
REVERSE_ENGINEER,
COPY;
}
protected String remarks="";
private String objectType;
/**
* A List of SQLColumn objects which make up all the columns of
* this table.
*/
protected List<SQLColumn> columns = new ArrayList<SQLColumn>();
/**
* A List of SQLRelationship objects describing keys that this
* table exports. This SQLTable is the "pkTable" in its exported
* keys.
*/
protected List<SQLRelationship> exportedKeys = new ArrayList<SQLRelationship>();
/**
* A List for SQLRelationship objects describing keys that this
* table imports. This SQLTable is the "fkTable" in its imported
* keys.
*/
protected List<SQLImportedKey> importedKeys = new ArrayList<SQLImportedKey>();
/**
* This index represents the primary key of this table. This will never be
* null or changed to prevent confusion with the primary key becoming null.
* If there are no columns in the primary key this index will not have
* children representing columns.
*/
private final SQLIndex primaryKeyIndex;
/**
* A List for SQLIndex objects that describe the various database indices
* that exist on this table.
*/
private List<SQLIndex> indices = new ArrayList<SQLIndex>();
/**
* Determinant of whether this table's {@link SQLColumn}s have been populated.
*/
private boolean columnsPopulated = false;
/**
* Determinant of whether this table's {@link SQLIndex}s have been populated.
*/
private boolean indicesPopulated = false;
/**
* Determinant of whether this table's exported keys have been populated.
*/
private boolean exportedKeysPopulated = false;
/**
* Determinant of whether this table's imported keys have been populated.
*/
private boolean importedKeysPopulated = false;
public SQLTable(SQLObject parent, String name, String remarks, String objectType,
boolean startPopulated) throws SQLObjectException {
this(parent, name, remarks, objectType, startPopulated, new SQLIndex());
}
public SQLTable(SQLObject parent, String name, String remarks, String objectType,
boolean startPopulated, SQLIndex primaryKey) throws SQLObjectException {
this(parent, name, remarks, objectType, startPopulated,
primaryKey, startPopulated, startPopulated, startPopulated, startPopulated);
}
@Constructor
public SQLTable(@ConstructorParameter(propertyName = "parent") SQLObject parent,
@ConstructorParameter(propertyName = "name") String name,
@ConstructorParameter(propertyName = "remarks") String remarks,
@ConstructorParameter(propertyName = "objectType") String objectType,
@ConstructorParameter(propertyName = "populated") boolean startPopulated,
@ConstructorParameter(parameterType = ParameterType.CHILD, propertyName = "primaryKeyIndex") SQLIndex primaryKeyIndex,
@ConstructorParameter(propertyName = "columnsPopulated") boolean columnsPopulated,
@ConstructorParameter(propertyName = "indicesPopulated") boolean indicesPopulated,
@ConstructorParameter(propertyName = "exportedKeysPopulated") boolean exportedKeysPopulated,
@ConstructorParameter(propertyName = "importedKeysPopulated") boolean importedKeysPopulated)
throws SQLObjectException {
super();
logger.debug("NEW TABLE "+name+"@"+hashCode());
for (Column wrapper : primaryKeyIndex.getChildrenWithoutPopulating()) {
if (wrapper.getColumn() == null) {
throw new SQLObjectException("The primary key of the table " + name +
" is not allowed to have calculated columns");
} else if (!columns.contains(wrapper.getColumn())) {
throw new SQLObjectException("The primary key of the table " + name +
" contains a column " + wrapper.getColumn() + " that is not " +
"part of the table.");
}
}
this.primaryKeyIndex = primaryKeyIndex;
primaryKeyIndex.setParent(this);
initFolders(startPopulated);
setColumnsPopulated(columnsPopulated);
setIndicesPopulated(indicesPopulated);
setExportedKeysPopulated(exportedKeysPopulated);
setImportedKeysPopulated(importedKeysPopulated);
setup(parent, name, remarks, objectType);
}
/**
* Sets up the values for the new Table
*/
private void setup(SQLObject parent, String name, String remarks, String objectType) {
super.setParent(parent);
super.setName(name); // this.setName will try to be far to fancy at this point, and break stuff
this.remarks = remarks;
this.objectType = objectType;
super.setPhysicalName(name);
updatePKIndexNameToMatch(null, name);
if (this.objectType == null) throw new NullPointerException();
}
/**
* Creates a new SQLTable with parent as its parent and a null
* schema and catalog. The table will contain the four default
* folders: "Columns" "Exported Keys" "Imported Keys" and "Indices".
*
* @param startPopulated The initial setting of this table's folders' <tt>populated</tt> flags.
* If this is set to false, the table will attempt to lazy-load the child folders. Otherwise,
* this table will not try to load its children from a database connection.
*/
public SQLTable(SQLDatabase parent, boolean startPopulated) throws SQLObjectException {
this(parent, "", "", "TABLE", startPopulated);
}
/**
* Creates a new SQLTable with parent as its parent and a null schema and
* catalog. The table will contain the four default folders: "Columns"
* "Exported Keys" "Imported Keys" and "Indices".
*
* @param startPopulated
* The initial setting of this table's folders'
* <tt>populated</tt> flags. If this is set to false, the table
* will attempt to lazy-load the child folders. Otherwise, this
* table will not try to load its children from a database
* connection.
* @param primaryKeyIndex
* the primary key of the table. This allows for a setup to
* create a primary key specific for this table.
* @throws SQLObjectException
*/
public SQLTable(SQLDatabase parent, boolean startPopulated, SQLIndex primaryKeyIndex) throws SQLObjectException {
this(parent, "", "", "TABLE", startPopulated, primaryKeyIndex);
}
/**
* Creates a new SQLTable with no children, no parent, and all
* properties set to their defaults. Note this should never
* Initialize the folders.
*
* <p>This is mainly for code that needs to reconstruct a SQLTable
* from outside configuration info, such as the SwingUIProject.load() method.
* If you want to make SQLTable objects from scratch, consider using one
* of the other constructors, which initialise the state more thoroughly.
*
*/
public SQLTable() {
super();
primaryKeyIndex = new SQLIndex();
primaryKeyIndex.setParent(this);
setup(null,null,null,"TABLE");
}
/**
* If you create a table from scratch using the no-args
* constructor, you should call this to create the standard set of
* Folder objects under this table. The regular constructor does
* it automatically.
*
* @param populated The initial value to give the folders'
* populated status. When loading from a file, this should be true;
* if lazy loading from a database, it should be false.
*/
public void initFolders(boolean populated) {
this.populated = populated;
columnsPopulated = populated;
exportedKeysPopulated = populated;
importedKeysPopulated = populated;
indicesPopulated = populated;
}
/**
* Updates all the simple properties of this SQLTable to match those of the given
* source table. Does not descend into children (columns, indexes, etc). Does not
* update the parent pointer of this SQLObject.
*/
@Override
public void updateToMatch(SQLObject source) throws SQLObjectException {
SQLTable sourceTable = (SQLTable) source;
setName(sourceTable.getName());
setRemarks(sourceTable.getRemarks());
setPhysicalName(sourceTable.getPhysicalName());
setObjectType(sourceTable.getObjectType());
}
/**
* Creates a new SQLTable under the given parent database. The new table will have
* all the same properties as the given source table.
*
* @param parent The database to insert the new table into
* @return The new table
* @throws SQLObjectException if there are populate problems on source or parent
* Or if the parent has children of type other than SQLTable.
*/
public SQLTable createInheritingInstance(SQLDatabase parent) throws SQLObjectException {
return createTableFromSource(parent, TransferStyles.REVERSE_ENGINEER, false);
}
/**
* This method creates a new table based on this table. This method
* is used for reverse engineering and copying as they are similar.
* @throws SQLObjectException if there are populate problems on source or parent
* Or if the parent has children of type other than SQLTable.
*/
private SQLTable createTableFromSource(SQLDatabase parent, TransferStyles transferStyle, boolean preserveColumnSource)
throws SQLObjectException {
populateColumns();
populateIndices();
populateRelationships();
populateImportedKeys();
SQLIndex newPKIndex = new SQLIndex();
SQLTable t = new SQLTable(parent, getName(), remarks, "TABLE", true, newPKIndex);
for (Map.Entry<Class<? extends SQLObject>, Throwable> inaccessibleReason : getChildrenInaccessibleReasons().entrySet()) {
t.setChildrenInaccessibleReason(inaccessibleReason.getValue(), inaccessibleReason.getKey(), false);
}
t.inherit(this, transferStyle, preserveColumnSource);
inheritIndices(this, t);
parent.addChild(t);
return t;
}
/**
* Creates a new SQLTable under the given parent database. The new table will have
* all the same properties as the given source table.
*
* @param parent The database to insert the new table into
* @return The new table
* @throws SQLObjectException if there are populate problems on source or parent
* Or if the parent has children of type other than SQLTable.
*/
public SQLTable createCopy(SQLDatabase parent, boolean preserveColumnSource)
throws SQLObjectException {
return createTableFromSource(parent, TransferStyles.COPY, preserveColumnSource);
}
/**
* inherit indices from the source table. This will update the target's
* primary key index to match the source table.
*
* @param source
* @param target
* @throws SQLObjectException
*/
private static void inheritIndices(SQLTable source, SQLTable target) throws SQLObjectException {
for ( SQLIndex index : source.getIndices()) {
if (index.isPrimaryKeyIndex()) {
target.getPrimaryKeyIndex().updateToMatch(index);
} else {
SQLIndex index2 = SQLIndex.getDerivedInstance(index,target);
target.addIndex(index2);
}
}
}
/**
* Populates the columns of all tables from the database that are not
* already populated. If successful, then the indices for just this table
* will also be populated.
*
* @throws SQLObjectException
*/
protected void populateColumns() throws SQLObjectException {
if (columnsPopulated) return;
synchronized(getClass()) {
synchronized(this) {
if (columns.size() > 0) {
throw new IllegalStateException("Can't populate table because it already contains columns");
}
logger.debug("column folder populate starting for table " + getName());
populateAllColumns(getCatalogName(), getSchemaName(), getName(), getParentDatabase(), getParent());
logger.debug("column folder populate finished for table " + getName());
populateIndices();
}
}
}
/**
* This method will populate all of the columns in all of the tables with
* one database call. This is done for optimization as making one database
* call for each table for just the columns in that table can become very
* slow with network traffic. At the end of this method all of the tables
* passed in will be populated with all of the columns found for the tables
* in the database. No additional tables will be created even if columns
* exist for missing tables.
* <p>
* Note that this class will iterate over all columns, obtaining locks on
* all them. Any methods calling this must be sure to synchronize on the
* class <b>before</b> the table instance, or else risk causing deadlock.
* <p>
* This is a helper method for {@link #populateColumns()}.
*
* @param catalogName
* The catalog name the tables are contained in, may be null.
* @param schemaName
* The schema name the tables are contained in, may be null.
* @param parentDB
* The parent database object. Will be used to connect to the
* database with.
* @param tableContainer
* The SQLObject that contains all of the tables in the system.
* @throws SQLObjectException
*/
private synchronized static void populateAllColumns(final String catalogName, final String schemaName,
final String tableName,
final SQLDatabase parentDB, final SQLObject tableContainer) throws SQLObjectException {
Connection con = null;
try {
con = parentDB.getConnection();
DatabaseMetaData dbmd = con.getMetaData();
final ListMultimap<String, SQLColumn> cols = SQLColumn.fetchColumnsForTable(
catalogName, schemaName, tableName, dbmd);
Runnable runner = new Runnable() {
public void run() {
try {
parentDB.begin("Populating all columns");
if (cols.isEmpty()) {
SQLTable t = parentDB.getTableByName(catalogName, schemaName, tableName);
if (t != null) {
t.setColumnsPopulated(true);
}
}
for (String tableName : cols.keySet()) {
SQLTable table = tableContainer.getChildByName(tableName, SQLTable.class);
//The multimap will contain table names of system tables
//that are not contained in the table container. Skipping
//these tables. If a table is missed due to an error it will
//at least not be marked as populated.
if (table == null) continue;
if (table.isColumnsPopulated()) continue;
populateColumnsWithList(table, cols.get(tableName));
}
parentDB.commit();
} catch (Throwable t) {
parentDB.rollback(t.getMessage());
throw new RuntimeException(t);
}
}
};
try {
parentDB.getRunnableDispatcher().runInForeground(runner);
} catch (SessionNotFoundException e) {
runner.run();
}
} catch (SQLException e) {
throw new SQLObjectException("Failed to populate columns of tables", e);
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException ex) {
logger.error("Couldn't close connection. Squishing this exception: ", ex);
}
}
}
}
/**
* Used to populate a table based on a list containing all of the column
* children of the table. This method must be called on the foreground
* thread. If the table is not populated when calling this method the table
* will be considered populated in terms of columns at the end. If the table
* is populated then this is will add the children to the table as a group.
* (Happens when loading from a repository with the server.)
* <p>
* Package private so they can be called from {@link SQLObjectUtils}.
*
* @param table
* The table to populate with the child list.
* @param allChildren
* A list that contains all of the child columns that belong to
* the table.
*/
static void populateColumnsWithList(SQLTable table, List<SQLColumn> allChildren) {
if (!table.isForegroundThread())
throw new IllegalStateException("This method must be called on the foreground thread.");
boolean populateStart = table.columnsPopulated;
try {
//The children are added without firing events because we need all of the
//children to be added to the table and have the table defined as populated
//before outside code is notified.
int index = table.columns.size();
for (SQLColumn col : allChildren) {
table.columns.add(col);
col.setParent(table);
}
table.columnsPopulated = true;
table.begin("Populating all columns");
for (SQLColumn col : allChildren) {
table.fireChildAdded(SQLColumn.class, col, index);
index++;
}
table.firePropertyChange("columnsPopulated", populateStart, true);
table.commit();
} catch (Throwable t) {
table.rollback(t.getMessage());
for (SQLColumn col : allChildren) {
table.columns.remove(col);
}
table.columnsPopulated = populateStart;
throw new RuntimeException(t);
}
}
/**
* Retrieves all index information about this table from the source database
* it is associated with. If the index folder has already been populated, this
* method returns immediately with no side effects.
*
* <p>Note: It is essential that the columns folder of this table has been populated before calling
* this method.
*
* @throws IllegalStateException if the columns folder is not yet populated, or if the
* index folder is both non-empty and non-populated
*/
protected synchronized void populateIndices() throws SQLObjectException {
if (indicesPopulated) return;
if (indices.size() > 0) {
throw new IllegalStateException("Can't populate indices because it already contains children!");
}
// If the SQLTable is a view, simply indicated folder is populated and then leave
// Since Views don't have indices (and Oracle throws an error)
if (objectType.equals("VIEW")) {
runInForeground(new Runnable() {
public void run() {
setIndicesPopulated(true);
}
});
return;
}
logger.debug("index folder populate starting");
Connection con = null;
try {
con = getParentDatabase().getConnection();
DatabaseMetaData dbmd = con.getMetaData();
logger.debug("before addIndicesToTable");
final List<SQLIndex> indexes = SQLIndex.fetchIndicesForTableAndUpdatePK(dbmd, this);
runInForeground(new Runnable() {
public void run() {
//someone beat us to this already
if (indicesPopulated) return;
populateIndicesWithList(SQLTable.this, indexes);
}
});
logger.debug("found "+indices.size()+" indices.");
} catch (SQLException e) {
throw new SQLObjectException("Failed to populate indices of table "+getName(), e);
} finally {
try {
if (con != null) con.close();
} catch (SQLException e) {
logger.error("Closing connection failed. Squishing this exception: ", e);
}
}
logger.debug("index folder populate finished");
}
/**
* Used to populate a table based on a list containing all of the index
* children of the table. This method must be called on the foreground
* thread. The columns of the table must be populated before the indices for
* the indices to have proper objects to reference. If the table is not
* populated when calling this method the table will be considered populated
* in terms of indices at the end. If the table is populated then this is
* will add the children to the table as a group. (Happens when loading from
* a repository with the server.)
* <p>
* Package private so they can be called from {@link SQLObjectUtils}.
*
* @param table
* The table to populate with the child list.
* @param indices
* A list that contains all of the child indices that belong to
* the table.
*/
static void populateIndicesWithList(SQLTable table, List<SQLIndex> indices) {
if (!table.isForegroundThread())
throw new IllegalStateException("This method must be called on the foreground thread.");
if (!table.columnsPopulated)
throw new IllegalStateException("Columns must be populated");
boolean startPopulated = table.indicesPopulated;
try {
//Must set the indices and then fire events so the populate flag
//is set before events fire to keep the state of the system consistent.
for (SQLIndex i : indices) {
table.indices.add(i);
i.setParent(table);
}
table.indicesPopulated = true;
table.begin("Populating Indices for Table " + table);
for (SQLIndex i : indices) {
table.fireChildAdded(SQLIndex.class, i, table.indices.indexOf(i) + 1);
}
table.firePropertyChange("indicesPopulated", startPopulated, true);
table.commit();
} catch (Throwable t) {
table.rollback(t.getMessage());
for (SQLIndex i : indices) {
table.indices.remove(i);
}
table.indicesPopulated = startPopulated;
throw new RuntimeException(t);
}
}
protected void populateImportedKeys() throws SQLObjectException {
// Must synchronize on class before instance. See populateAllColumns
if (importedKeysPopulated) return;
synchronized(getClass()) {
synchronized(this) {
CachedRowSet crs = null;
Connection con = null;
try {
con = getParentDatabase().getConnection();
DatabaseMetaData dbmd = con.getMetaData();
crs = new CachedRowSet();
ResultSet exportedKeysRS = dbmd.getImportedKeys(getCatalogName(), getSchemaName(), getName());
crs.populate(exportedKeysRS);
exportedKeysRS.close();
} catch (SQLException ex) {
throw new SQLObjectException("Couldn't locate related tables", ex);
} finally {
// close the connection before it makes the recursive call
// that could lead to opening more connections
try {
if (con != null) con.close();
} catch (SQLException ex) {
logger.warn("Couldn't close connection", ex);
}
}
try {
while (crs.next()) {
if (crs.getInt(9) != 1) {
// just another column mapping in a relationship we've already handled
logger.debug("Got exported key with sequence " + crs.getInt(9) + " on " + crs.getString(5) + "." + crs.getString(6) + "." + crs.getString(7) + ", continuing.");
continue;
}
logger.debug("Got exported key with sequence " + crs.getInt(9) + " on " + crs.getString(5) + "." + crs.getString(6) + "." + crs.getString(7) + ", populating.");
String cat = crs.getString(1);
String sch = crs.getString(2);
String tab = crs.getString(3);
SQLTable pkTable = getParentDatabase().getTableByName(cat, sch, tab);
if (pkTable == null) {
throw new IllegalStateException("While populating table " +
SQLObjectUtils.toQualifiedName(getParent()) +
", I failed to find child table " +
"\""+cat+"\".\""+sch+"\".\""+tab+"\"");
}
pkTable.populateColumns();
pkTable.populateIndices();
pkTable.populateRelationships(this);
}
setImportedKeysPopulated(true);
} catch (SQLException ex) {
throw new SQLObjectException("Couldn't locate related tables", ex);
} finally {
try {
if (crs != null) crs.close();
} catch (SQLException ex) {
logger.warn("Couldn't close resultset", ex);
}
}
}
}
}
/**
* Populates all the imported key relationships, where it first ensures that
* columns have been populated.
*
* @throws SQLObjectException
*/
protected void populateExportedKeys() throws SQLObjectException {
// Must synchronize on class before instance. See populateAllColumns
synchronized(getClass()) {
synchronized(this) {
populateColumns();
populateIndices();
populateRelationships();
}
}
}
/**
* Populates all the exported key relationships. This has the
* side effect of populating the imported key side of the
* relationships for the exporting tables.
* <p>
* XXX This is a temporary patch to make relationship populating not cascade.
* This can be improved upon.
*/
protected synchronized void populateRelationships(SQLTable fkTable) throws SQLObjectException {
if (exportedKeysPopulated) {
return;
}
logger.debug("SQLTable: relationship populate starting");
final List<SQLRelationship> newKeys = SQLRelationship.fetchExportedKeys(this, fkTable);
runInForeground(new Runnable() {
public void run() {
//Someone beat us to populating the relationships
if (exportedKeysPopulated) return;
populateRelationshipsWithList(SQLTable.this, newKeys);
}
});
logger.debug("SQLTable: relationship populate finished");
}
/**
* Populates all the exported key relationships. This has the
* side effect of populating the imported key side of the
* relationships for the exporting tables.
*/
protected synchronized void populateRelationships() throws SQLObjectException {
if (exportedKeysPopulated) {
return;
}
logger.debug("SQLTable: relationship populate starting");
final List<SQLRelationship> newKeys = SQLRelationship.fetchExportedKeys(this, null);
runInForeground(new Runnable() {
public void run() {
//Someone beat us to populating the relationships
if (exportedKeysPopulated) return;
populateRelationshipsWithList(SQLTable.this, newKeys);
}
});
logger.debug("SQLTable: relationship populate finished");
}
/**
* Used to populate a table based on a list containing all of the exported
* relationship children of the table. This method must be called on the
* foreground thread. The columns and indices of the table must be populated
* before the relationships so the relationships have proper columns and
* primary key to reference. If the table is not populated when calling this
* method the table will be considered populated in terms of exported keys
* at the end. If the table is populated then this is will add the children
* to the table as a group. (Happens when loading from a repository with the
* server.)
* <p>
* Package private so they can be called from {@link SQLObjectUtils}.
*
* @param table
* The table to populate with the child list.
* @param allChildren
* A list that contains all of the child relationships that will
* be added to the table. For backwards compatibility if the
* relationship exists in the table it won't be added again.
*/
static void populateRelationshipsWithList(SQLTable table, List<SQLRelationship> allChildren) {
if (!table.isForegroundThread())
throw new IllegalStateException("This method must be called on the foreground thread.");
if (!table.columnsPopulated) {
throw new IllegalStateException("Table must be populated before relationships are added");
}
if (!table.indicesPopulated) {
throw new IllegalStateException("Table indices must be populated before relationships are added");
}
boolean startPopulated = table.exportedKeysPopulated;
List<SQLRelationship> relsAdded = new ArrayList<SQLRelationship>();
try {
for (SQLRelationship addMe : allChildren) {
//This if is for backwards compatibility
addMe.getParent().exportedKeys.add(addMe);
if (addMe.getPkTable().isMagicEnabled()) {
if (!addMe.getFkTable().getImportedKeysWithoutPopulating().contains(addMe.getForeignKey())) {
addMe.getFkTable().importedKeys.add(addMe.getForeignKey());
relsAdded.add(addMe);
}
}
}
table.exportedKeysPopulated = true;
table.begin("Populating relationships for Table " + table);
for (SQLRelationship addMe : relsAdded) {
SQLTable pkTable = addMe.getParent();
if (pkTable.isMagicEnabled()) {
SQLTable fkTable = addMe.getFkTable();
SQLImportedKey foreignKey = addMe.getForeignKey();
addMe.attachRelationship(pkTable, fkTable, false, false);
fkTable.fireChildAdded(SQLImportedKey.class, foreignKey, fkTable.importedKeys.indexOf(foreignKey));
}
pkTable.fireChildAdded(SQLRelationship.class, addMe, pkTable.exportedKeys.indexOf(addMe));
}
table.firePropertyChange("exportedKeysPopulated", startPopulated, true);
table.commit();
} catch (SQLObjectException e) {
table.rollback(e.getMessage());
for (SQLRelationship rel : relsAdded) {
rel.getParent().exportedKeys.remove(rel);
if (table.isMagicEnabled()) {
rel.getFkTable().importedKeys.remove(rel.getForeignKey());
}
}
table.exportedKeysPopulated = startPopulated;
throw new SQLObjectRuntimeException(e);
} catch (Throwable t) {
table.rollback(t.getMessage());
for (SQLRelationship rel : relsAdded) {
rel.getParent().exportedKeys.remove(rel);
rel.getFkTable().importedKeys.remove(rel.getForeignKey());
}
table.exportedKeysPopulated = startPopulated;
throw new RuntimeException(t);
}
}
public void addImportedKey(SQLImportedKey r) {
addImportedKey(r, importedKeys.size());
}
public void addImportedKey(SQLImportedKey k, int index) {
importedKeys.add(index, k);
k.setParent(this);
fireChildAdded(SQLImportedKey.class, k, index);
}
public boolean removeImportedKey(SQLImportedKey k) {
if (isMagicEnabled() && k.getParent() != this) {
throw new IllegalStateException("Cannot remove child " + k.getName() +
" of type " + k.getClass() + " as its parent is not " + getName());
}
k.getRelationship().disconnectRelationship(false);
int index = importedKeys.indexOf(k);
if (index != -1) {
importedKeys.remove(index);
fireChildRemoved(SQLImportedKey.class, k, index);
return true;
}
return false;
}
public void addExportedKey(SQLRelationship r) {
addExportedKey(r, exportedKeys.size());
}
public void addExportedKey(SQLRelationship r, int index) {
exportedKeys.add(index, r);
r.setParent(this);
fireChildAdded(SQLRelationship.class, r, index);
}
public boolean removeExportedKey(SQLRelationship r) {
if (isMagicEnabled() && r.getParent() != this) {
throw new IllegalStateException("Cannot remove child " + r.getName() +
" of type " + r.getClass() + " as its parent is not " + getName());
}
r.disconnectRelationship(true);
int index = exportedKeys.indexOf(r);
if (index != -1) {
exportedKeys.remove(index);
fireChildRemoved(SQLRelationship.class, r, index);
return true;
}
return false;
}
/**
* Counts the number of columns in the primary key of this table.
* <p>
* This does not populate the columns or indicies of the table. Either
* the table should be populated already or populate may need to be called.
*/
@Transient @Accessor
public int getPkSize() {
return primaryKeyIndex.getChildrenWithoutPopulating().size();
}
/**
* Adds all the columns of the given source table to the end of
* this table's column list.
*/
public void inherit(SQLTable source, TransferStyles transferStyle, boolean preserveColumnSource) throws SQLObjectException {
inherit(columns.size(), source, transferStyle, preserveColumnSource);
}
/**
* Inserts all the columns of the given source table into this
* table at position <code>pos</code>.
*
* <p>If this table currently has no columns, then the source's
* primary key will remain intact (and this table will become an
* identical copy of source). If not, and if the insertion
* position <= this.pkSize(), then all source columns will be
* added to this table's primary key. Otherwise, no source
* columns will be added to this table's primary key.
*/
public List<SQLColumn> inherit(int pos, SQLTable source, TransferStyles transferStyle, boolean preserveColumnSource) throws SQLObjectException {
if (source == this) {
throw new SQLObjectException("Cannot inherit from self");
}
boolean addToPK;
int pkSize = getPkSize();
source.populateColumns();
source.populateIndices();
if (pos < pkSize) {
addToPK = true;
} else {
addToPK = false;
}
List<SQLColumn> addedColumns = new ArrayList<SQLColumn>();
begin("Inherting columns from source table");
for (SQLColumn child : source.getColumns()) {
addedColumns.add(inherit(pos, child, addToPK, transferStyle, preserveColumnSource));
pos++;
}
commit();
return addedColumns;
}
public SQLColumn inherit(int pos, SQLColumn sourceCol, boolean addToPK, TransferStyles transferStyle, boolean preserveColumnSource) throws SQLObjectException {
if (addToPK && pos > 0 && !getColumn(pos - 1).isPrimaryKey()) {
throw new IllegalArgumentException("Can't inherit new PK column below a non-PK column! Insert pos="+pos+"; addToPk="+addToPK);
}
SQLColumn c;
switch (transferStyle) {
case REVERSE_ENGINEER:
c = sourceCol.createInheritingInstance(this);
break;
case COPY:
c = sourceCol.createCopy(this, preserveColumnSource);
break;
default:
throw new IllegalStateException("Unknown transfer type of " + transferStyle);
}
addColumn(c, addToPK, pos);
return c;
}
@NonProperty
public SQLColumn getColumn(int index) throws SQLObjectException {
populateColumns();
return columns.get(index);
}
/**
* Populates this table then searches for the named column in a case-insensitive
* manner.
*/
@NonProperty
public SQLColumn getColumnByName(String colName) throws SQLObjectException {
return getColumnByName(colName, true, false);
}
/**
* Searches for the named column.
*
* @param populate If true, this table will retrieve its column
* list from the database; otherwise it just searches the current
* list.
*/
@NonProperty
public SQLColumn getColumnByName(String colName, boolean populate, boolean caseSensitive)
throws SQLObjectException {
if (populate) populateColumns();
/* if columnsFolder.children.iterator(); gets changed to getColumns().iterator()
* we get infinite recursion between populateColumns, getColumns,
* getColumnsByName and addColumnsToTable
*/
if (logger.isDebugEnabled()) {
// logger.debug("Looking for column "+colName+" in "+columns);
// logger.debug("Table " + getName() + " has " + columns.size() + " columns");
}
for (SQLColumn col : columns) {
//logger.debug("Current column name is '" + col.getName() + "'");
if (caseSensitive) {
if (col.getName().equals(colName)) {
logger.debug("FOUND");
return col;
}
} else {
if (col.getName().equalsIgnoreCase(colName)) {
logger.debug("FOUND");
return col;
}
}
}
logger.debug("NOT FOUND");
return null;
}
@NonProperty
public int getColumnIndex(SQLColumn col) throws SQLObjectException {
// logger.debug("Looking for column index of: " + col);
List<SQLColumn> columns = getColumns();
int index = columns.indexOf(col);
if (index == -1) {
// logger.debug("NOT FOUND");
}
return index;
}
public void addColumn(SQLColumn col) throws SQLObjectException {
addColumn(col, columns.size());
}
/**
* Adds a column to the given position in the table. If magic is enabled the
* primary key sequence of the column may be updated depending on the
* position of the column and the number of primary keys.If the column is
* being added just after the primary key the column will not be added to
* the primary key. If this decision has been already decided see
* {@link #addColumn(SQLColumn, boolean, int)}.
*
* @param col
* The column to add.
* @param pos
* The position to add the column to.
* @throws SQLObjectException
*/
public void addColumn(SQLColumn col, int pos) throws SQLObjectException {
boolean addToPk = getPkSize() > pos;
addColumnWithoutPopulating(col, addToPk, pos);
}
/**
* XXX The boolean is before the position to prevent it from having the same
* signature of a recently removed method. This is mainly for refactoring
* aid.
*
* @param col
* The column to add to the table.
* @param addToPk
* If true and the position is valid to be in the primary key the
* column will be added to the primary key. If false and the
* position is valid the column will not be added to the primary
* key. This is mainly for use in the edge case when adding a
* column just after the last column of the primary key to decide
* if the column should be in the primary key.
* @param pos
* The position to add the column to.
*/
public void addColumn(SQLColumn col, boolean addToPk, int pos) throws SQLObjectException {
populateColumns();
addColumnWithoutPopulating(col, addToPk, pos);
}
/**
* Adds a {@link SQLColumn} to the end of the child list without populating first.
*/
public void addColumnWithoutPopulating(SQLColumn col) {
addColumnWithoutPopulating(col, columns.size());
}
/**
* Adds a {@link SQLColumn} at a given index of the child list without
* populating first. If the column is being added just after the primary key
* the column will not be added to the primary key. If this decision has
* been already decided see
* {@link #addColumnWithoutPopulating(SQLColumn, boolean, int)}.
*/
public void addColumnWithoutPopulating(SQLColumn col, int pos) {
boolean addToPk = getPkSize() > pos;
addColumnWithoutPopulating(col, addToPk, pos);
}
/**
* Adds a {@link SQLColumn} at a given index of the child list without
* populating first. This will throw an {@link IllegalArgumentException} if
* the boolean to add to the pk and the position do not agree.
* <p>
* XXX The boolean is before the position to prevent it from having the same
* signature of a recently removed method. This is mainly for refactoring
* aid.
*
* @param col
* The column to add to the table.
* @param addToPk
* If true and the position is valid to be in the primary key the
* column will be added to the primary key. If false and the
* position is valid the column will not be added to the primary
* key. This is mainly for use in the edge case when adding a
* column just after the last column of the primary key to decide
* if the column should be in the primary key.
* @param pos
* The position to add the column to.
*/
public void addColumnWithoutPopulating(SQLColumn col, boolean addToPk, int pos) {
int pkSize = getPkSize();
if ((pos < pkSize && !addToPk) || (pos > pkSize && addToPk)) {
throw new IllegalArgumentException("The column " + col + " is being " + (addToPk ? "" : "not") + " added to " +
"the primary key at position " + pos + " but there are " + pkSize +
" pk column(s) so the add position is invalid.");
}
if (columns.indexOf(col) != -1) {
col.addReference();
return;
}
columns.add(pos, col);
col.setParent(this);
fireChildAdded(SQLColumn.class, col, pos);
if (isMagicEnabled()) {
if (addToPk) {
primaryKeyIndex.addIndexColumn(col);
}
}
}
/**
* Adds the given SQLIndex object to this table's index folder.
*/
public void addIndex(SQLIndex sqlIndex) {
addIndex(sqlIndex, indices.size() + 1);
}
/**
* Adds the given SQLIndex object to this table's index list.
*
* @param sqlIndex
* The index to add to this table
* @param index
* The index to place the object at. This includes the primary
* key index.
*/
public void addIndex(SQLIndex sqlIndex, int index) {
//The primary key index is not added to the index list.
if (sqlIndex == primaryKeyIndex) return;
indices.add(index - 1, sqlIndex);
sqlIndex.setParent(this);
fireChildAdded(SQLIndex.class, sqlIndex, index);
}
@Override
protected void addChildImpl(SPObject child, int index) {
if (child instanceof SQLColumn) {
addColumnWithoutPopulating((SQLColumn) child, index);
} else if (child instanceof SQLRelationship) {
addExportedKey((SQLRelationship) child);
} else if (child instanceof SQLImportedKey) {
addImportedKey((SQLImportedKey) child);
} else if (child instanceof SQLIndex) {
addIndex((SQLIndex) child, index);
} else {
throw new IllegalArgumentException("The child " + child.getName() +
" of type " + child.getClass() + " is not a valid child type of " +
getClass() + ".");
}
}
/**
* Calls {@link #removeColumn(SQLColumn)} with the appropriate argument.
*
* @throws LockedColumnException
* If the column is "owned" by a relationship, and cannot be
* safely removed.
*/
public boolean removeColumn(int index) throws SQLObjectException {
try {
return removeChild(columns.get(index));
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (ObjectDependentException e) {
throw new RuntimeException(e);
}
}
/**
* Removes the given column if it is in this table. If you want to change a
* column's, index, use the {@link #changeColumnIndex(int,int)} method
* because it does not throw LockedColumnException.
*
* <p>
* FIXME: This should be implemented by decreasing the column's reference
* count. (addColumn already does increase reference count when appropriate)
* Then, everything that manipulates reference counts directly can just use
* regular addColumn and removeColumn and magic will take care of the
* correct behaviour!
*
* @throws LockedColumnException
* If the column is "owned" by a relationship, and cannot be
* safely removed.
*/
public boolean removeColumn(SQLColumn col) {
if (isMagicEnabled() && col.getParent() != this) {
throw new IllegalStateException("Cannot remove child " + col.getName() +
" of type " + col.getClass() + " as its parent is not " + getName());
}
if (isMagicEnabled()) {
primaryKeyIndex.removeColumn(col);
}
try {
begin("Removing column " + col.getName());
int index = columns.indexOf(col);
if (index != -1) {
columns.remove(index);
fireChildRemoved(SQLColumn.class, col, index);
col.setParent(null);
return true;
}
} finally {
commit();
}
return false;
}
/**
* Moves the column at index <code>oldIdx</code> to index
* <code>newIdx</code>. This may cause the moved column to become part of
* the primary key (or to be removed from the primary key).
*
* @param oldIdx
* the present index of the column.
* @param newIdx
* the index that the column will have when this
* @param putInPK
* Used to decide if the column should go into the pk on corner
* cases. If the column is being moved to a position where it is
* above a column in the primary key it will be placed in the
* primary key. If the column is being moved to a position where
* it is below a column not in the primary key it will not be
* placed in the primary key. If the column is placed where it
* could be either in or out of the primary key the boolean will
* decide what to do. method returns.
*/
public void changeColumnIndex(int oldIdx, int newIdx, boolean putInPK) throws SQLObjectException {
SQLColumn col = columns.get(oldIdx);
int pkSize = getPkSize();
if ((newIdx < pkSize && !putInPK && !col.isPrimaryKey()) ||
(newIdx < pkSize - 1 && !putInPK && col.isPrimaryKey())) {
putInPK = true;
} else if ((newIdx > pkSize && putInPK && !col.isPrimaryKey()) ||
(newIdx > pkSize - 1 && putInPK && col.isPrimaryKey())) {
putInPK = false;
}
try {
begin("Changing column index");
// If the indices are the same, then there's no point in moving the column
if (oldIdx != newIdx) {
removeColumn(col);
addColumn(col, putInPK, newIdx);
} else if (putInPK && !col.isPrimaryKey()) {
primaryKeyIndex.addIndexColumn(col);
} else if (!putInPK && col.isPrimaryKey()) {
primaryKeyIndex.removeColumn(col);
}
commit();
} catch (SQLObjectException e) {
rollback(e.getMessage());
throw e;
} catch (Exception e) {
rollback(e.getMessage());
throw new RuntimeException(e);
}
}
public List<SQLRelationship> keysOfColumn(SQLColumn col) throws SQLObjectException {
LinkedList<SQLRelationship> keys = new LinkedList<SQLRelationship>();
for (SQLRelationship r : exportedKeys) {
if (r.containsPkColumn(col)) {
keys.add(r);
}
}
for (SQLRelationship r : exportedKeys) {
if (r.containsFkColumn(col)) {
keys.add(r);
}
}
return keys;
}
public String toString() {
return getShortDisplayName();
}
// ---------------------- SQLObject support ------------------------
/**
* The table's name.
*/
@Transient @Accessor
public String getShortDisplayName() {
SQLSchema schema = getSchema();
if (schema != null) {
return schema.getName()+"."+ getName()+" ("+objectType+")";
} else {
if (objectType != null) {
return getName()+" ("+objectType+")";
} else {
return getName();
}
}
}
/**
* Since SQLTable is just a container for Folders, there is no special populate
* step. The various populate operations (columns, keys, indices) are triggered
* by visiting the individual folders.
*/
protected void populateImpl() throws SQLObjectException {
if (populated) return;
try {
runInForeground(new Runnable() {
public void run() {
begin("Populating");
}
});
populateColumns();
populateIndices();
populateRelationships();
if (columnsPopulated && indicesPopulated && exportedKeysPopulated) {
populated = true;
}
runInForeground(new Runnable() {
public void run() {
commit();
}
});
} catch (final SQLObjectException e) {
runInForeground(new Runnable() {
public void run() {
rollback(e.getMessage());
logger.error("Sketchy transaction rollback");
}
});
throw e;
} catch (final RuntimeException e) {
runInForeground(new Runnable() {
public void run() {
rollback(e.getMessage());
}
});
throw e;
}
}
@Accessor
public boolean isPopulated() {
if (!populated && isColumnsPopulated() && isImportedKeysPopulated() && isExportedKeysPopulated() && isIndicesPopulated()) {
populated = true;
}
return populated;
}
@NonBound
public Class<? extends SQLObject> getChildType() {
return SQLObject.class;
}
// ------------------ Accessors and mutators below this line ------------------------
/**
* Walks up the SQLObject containment hierarchy and returns the
* first SQLDatabase object encountered. If this SQLTable has no
* SQLDatabase ancestors, the return value is null.
*
* @return the value of parentDatabase
*/
@Transient @Accessor
public SQLDatabase getParentDatabase() {
return SQLPowerUtils.getAncestor(this, SQLDatabase.class);
}
@Override
@Accessor
public SQLObject getParent() {
return (SQLObject) super.getParent();
}
/**
* Because we constrained the return type on getParent there needs to be a
* setter that has the same constraint otherwise the reflection in the undo
* events will not find a setter to match the getter and won't be able to
* undo parent property changes.
*/
@Mutator
public void setParent(SQLObject parent) {
super.setParent(parent);
}
/**
* @return An empty string if the catalog for this table is null;
* otherwise, getCatalog().getCatalogName().
*/
@Transient @Accessor
public String getCatalogName() {
SQLCatalog catalog = getCatalog();
if (catalog == null) {
return "";
} else {
return catalog.getName();
}
}
@Transient @Accessor
public SQLCatalog getCatalog() {
return SQLPowerUtils.getAncestor(this, SQLCatalog.class);
}
/**
* @return An empty string if the schema for this table is null;
* otherwise, schema.getSchemaName().
*/
@Transient @Accessor
public String getSchemaName() {
SQLSchema schema = getSchema();
if (schema == null) {
return "";
} else {
return schema.getName();
}
}
@Transient @Accessor
public SQLSchema getSchema() {
return SQLPowerUtils.getAncestor(this, SQLSchema.class);
}
/**
* Sets the table name, and also modifies the primary key name if
* it was previously null or set to the default of
* "oldTableName_pk". Additionally, if any of this table's columns'
* sequence names have been explicitly set, the old table name within
* those sequence names will be replaced by the new table name.
*
* @param argName The new table name. NULL is not allowed.
*/
@Mutator
public void setPhysicalName(String argName) {
logger.debug("About to change table name from \""+getPhysicalName()+"\" to \""+argName+"\"");
// this method can be called very early in a SQLTable's life,
// before its folders exist. Therefore, we have to
// be careful not to look up the primary key before one exists.
if ( (!isMagicEnabled()) || (indices == null) || (columns == null) ) {
super.setPhysicalName(argName);
} else try {
String oldName;
if (getPhysicalName() != null) {
oldName = getPhysicalName();
} else {
oldName = getName();
}
begin("Table Name Change");
super.setPhysicalName(argName);
updatePKIndexNameToMatch(oldName, argName);
if (isColumnsPopulated()) {
for (SQLColumn col : getColumns()) {
if (col.isAutoIncrementSequenceNameSet()) {
String testingName = col.discoverSequenceNameFormat(oldName, col.getPhysicalName());
if (testingName.equals(col.getAutoIncrementSequenceName())) {
col.setAutoIncrementSequenceName(
col.makeAutoIncrementSequenceName());
}
}
}
}
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
} finally {
commit();
}
}
@Mutator
@Override
public void setName(String name) {
try {
begin("Setting name and possibly physical or primary key name.");
if (isMagicEnabled()) {
updatePhysicalNameToMatch(getName(), name);
}
super.setName(name);
commit();
} catch (Throwable t) {
rollback(t.getMessage());
throw new RuntimeException(t);
}
}
/**
* Simple helper that updates the PK Index name to match this table's name
* if the user hasn't changed it.
*/
private void updatePKIndexNameToMatch(String oldName, String newName) {
if (newName != null &&
primaryKeyIndex != null &&
(primaryKeyIndex.getName() == null
|| "".equals(primaryKeyIndex.getName().trim())
|| (oldName + "_pk").equals(primaryKeyIndex.getName())) ) {
// if the physical name is still null when forward engineer,
// the DDLGenerator will generate the physical name from the
// logic name and this index will be updated.
primaryKeyIndex.setName(newName + "_pk");
}
}
/**
* Gets the value of remarks
*
* @return the value of remarks
*/
@Accessor(isInteresting=true)
public String getRemarks() {
return this.remarks;
}
/**
* Sets the value of remarks
*
* @param argRemarks Value to assign to this.remarks
*/
@Mutator
public void setRemarks(String argRemarks) {
String oldRemarks = this.remarks;
this.remarks = argRemarks;
firePropertyChange("remarks",oldRemarks,argRemarks);
}
/**
* Gets the value of columns after populating. This will allow access to the
* most up to date list of columns.
*
* @return the value of columns
*/
@NonProperty
public List<SQLColumn> getColumns() throws SQLObjectException {
// Must synchronize on class before instance. See populateAllColumns
synchronized(getClass()) {
synchronized(this) {
populateColumns();
return getColumnsWithoutPopulating();
}
}
}
/**
* Returns the list of columns without populating first. This will allow
* access to the columns at current.
*/
@NonProperty
public synchronized List<SQLColumn> getColumnsWithoutPopulating() {
return Collections.unmodifiableList(columns);
}
/**
* Gets the value of importedKeys after populating. This will allow access
* to the most up to date list of imported keys.
*
* @return the value of importedKeys
* @throws SQLObjectException
*/
@NonProperty
public List<SQLImportedKey> getImportedKeys() throws SQLObjectException {
populateImportedKeys();
return getImportedKeysWithoutPopulating();
}
/**
* Returns the list of imported keys without populating first. This will
* allow access to the imported keys at current.
*/
@NonProperty
public List<SQLImportedKey> getImportedKeysWithoutPopulating() {
return Collections.unmodifiableList(importedKeys);
}
/**
* Gets the value of exportedKeys after populating. This will allow access
* to the most updated list of exported keys.
*
* @return the value of exportedKeys
* @throws SQLObjectException
*/
@NonProperty
public List<SQLRelationship> getExportedKeys() throws SQLObjectException {
populateExportedKeys();
return getExportedKeysWithoutPopulating();
}
/**
* Returns the list of exported keys without populating first. This will
* allow access to the exported keys at current.
*/
@NonProperty
public List<SQLRelationship> getExportedKeysWithoutPopulating() {
return Collections.unmodifiableList(exportedKeys);
}
/**
* Gets the value of exportedKeys by name
*
* @return the value of exportedKeys
*/
@NonProperty
public SQLRelationship getExportedKeyByName(String name) throws SQLObjectException {
return getExportedKeyByName(name,true);
}
/**
* Gets the value of exportedKeys by name
*
* @return the value of exportedKeys
*/
@NonProperty
public SQLRelationship getExportedKeyByName(String name,
boolean populate ) throws SQLObjectException {
if (populate) populateRelationships();
logger.debug("Looking for Exported Key ["+name+"] in "+exportedKeys );
for (SQLRelationship r : exportedKeys) {
if (r.getName().equalsIgnoreCase(name)) {
logger.debug("FOUND");
return r;
}
}
logger.debug("NOT FOUND");
return null;
}
/**
* Gets a list of unique indices
*/
@NonProperty
public List<SQLIndex> getUniqueIndices() throws SQLObjectException {
// Must synchronize on class before instance. See populateAllColumns
synchronized(getClass()) {
synchronized(this) {
populateColumns();
populateIndices();
List<SQLIndex> list = new ArrayList<SQLIndex>();
list.add(primaryKeyIndex);
for (SQLIndex index : indices) {
if (index.isUnique()) {
list.add(index);
}
}
return list;
}
}
}
/**
* Gets the value of index by name
*
* @return the value of index
*/
@NonProperty
public SQLIndex getIndexByName(String name) throws SQLObjectException {
return getIndexByName(name,true);
}
/**
* Gets the value of index by name
*
* @return the value of index
*/
@NonProperty
public SQLIndex getIndexByName(String name,
boolean populate ) throws SQLObjectException {
if (populate) {
populateColumns();
populateIndices();
}
logger.debug("Looking for Index ["+name+"] in "+indices);
if (primaryKeyIndex.getName().equalsIgnoreCase(name)) {
logger.debug("Found primary key");
return primaryKeyIndex;
}
for (SQLIndex index : indices) {
if (index.getName().equalsIgnoreCase(name)) {
logger.debug("FOUND");
return index;
}
}
logger.debug("NOT FOUND");
return null;
}
/**
* Returns true if this table's columns folder says it's populated.
*/
@Accessor
public boolean isColumnsPopulated() {
return columnsPopulated;
}
/**
* Returns true if this table's imported keys and exported
* keys both say are populated.
*/
@Transient @Accessor
public boolean isRelationshipsPopulated() {
return importedKeysPopulated && exportedKeysPopulated;
}
/**
* Returns true if this table's imported keys have been populated.
*/
@Accessor
public boolean isImportedKeysPopulated() {
return importedKeysPopulated;
}
/**
* Returns true if this table's exported keys have been populated.
*/
@Accessor
public boolean isExportedKeysPopulated() {
return exportedKeysPopulated;
}
/**
* Returns true if this table's indices folder says it's populated.
*/
@Accessor
public boolean isIndicesPopulated() {
return indicesPopulated;
}
@Mutator
public void setColumnsPopulated(boolean columnsPopulated) {
boolean oldPop = this.columnsPopulated;
this.columnsPopulated = columnsPopulated;
firePropertyChange("columnsPopulated", oldPop, columnsPopulated);
if (!columnsPopulated) {
setUnpopulatedIfPartiallyPopulated();
}
}
@Mutator
public void setImportedKeysPopulated(boolean importedKeysPopulated) {
boolean oldPop = this.importedKeysPopulated;
this.importedKeysPopulated = importedKeysPopulated;
firePropertyChange("importedKeysPopulated", oldPop, importedKeysPopulated);
if (!columnsPopulated) {
setUnpopulatedIfPartiallyPopulated();
}
}
@Mutator
public void setExportedKeysPopulated(boolean exportedKeysPopulated) {
boolean oldPop = this.exportedKeysPopulated;
this.exportedKeysPopulated = exportedKeysPopulated;
firePropertyChange("exportedKeysPopulated", oldPop, exportedKeysPopulated);
if (!columnsPopulated) {
setUnpopulatedIfPartiallyPopulated();
}
}
@Mutator
public void setIndicesPopulated(boolean indicesPopulated) {
boolean oldPop = this.indicesPopulated;
this.indicesPopulated = indicesPopulated;
firePropertyChange("indicesPopulated", oldPop, indicesPopulated);
if (!columnsPopulated) {
setUnpopulatedIfPartiallyPopulated();
}
}
/**
* Helper method for setting one of the populated child types to be
* unpopulated. This will set the top level "populated" value to false if it
* was previously true.
* <p>
* XXX This may not be necessary if we can get rid of the top level
* populated flag and just keep a set of populated flags, one for each child
* type.
*/
private void setUnpopulatedIfPartiallyPopulated() {
if (populated) {
populated = false;
firePropertyChange("populated", true, false);
}
}
/**
* Gets the type of table this object represents (TABLE or VIEW).
*
* @return the value of objectType
*/
@Accessor
public String getObjectType() {
return this.objectType;
}
/**
* Sets the type of table this object represents (TABLE or VIEW).
*
* @param argObjectType Value to assign to this.objectType
*/
@Mutator
public void setObjectType(String argObjectType) {
String oldObjectType = this.objectType;
this.objectType = argObjectType;
if (this.objectType == null) throw new NullPointerException();
firePropertyChange("objectType",oldObjectType, argObjectType);
}
/**
* Returns the primary key for this table. This will always be the same
* index and never be null, though the index could have no columns
* associated with it.
*/
@Transient @Accessor
public SQLIndex getPrimaryKeyIndex() {
return primaryKeyIndex;
}
/**
* Retrieves all of the table names for the given catalog, schema
* in the container's database using DatabaseMetaData. This method
* is a subroutine of the populate() methods in SQLDatabase, SQLCatalog,
* and SQLSchema.
* <p>
* Important Note: This method adds the tables directly to the parent's
* children list. No SQLObjectEvents will be generated. Calling code
* has to do this at the appropriate time, when it's safe to do so.
*
* @param container The container that will be the direct parent of
* all tables created by this method call.
* @param dbmd The DatabaseMetaData for the parent database in question.
* The fact that you have to pass it in is just an optimization: all
* the places from which this method gets called already have an instance
* of DatabaseMetaData ready to go.
*/
static List<SQLTable> fetchTablesForTableContainer(DatabaseMetaData dbmd, String catalogName, String schemaName)
throws SQLObjectException, SQLException {
ResultSet rs = null;
try {
rs = dbmd.getTables(catalogName,
schemaName,
"%",
new String[] {"TABLE", "VIEW"});
List<SQLTable> tables = new ArrayList<SQLTable>();
while (rs.next()) {
tables.add(new SQLTable(null,
rs.getString(3),
rs.getString(5),
rs.getString(4),
false));
}
return tables;
} finally {
if (rs != null) rs.close();
}
}
/**
* Returns an unmodifiable list of all the indices of this table, in the
* same order they appear in the indices folder. If this table has no indices,
* the returned list will be empty (never null).
*
* @throws SQLObjectException If there is a problem populating the indices folder
*/
@NonProperty
public List<SQLIndex> getIndices() throws SQLObjectException {
populateColumns();
populateIndices();
List<SQLIndex> allIndices = new ArrayList<SQLIndex>();
allIndices.add(primaryKeyIndex);
allIndices.addAll(indices);
return Collections.unmodifiableList(allIndices);
}
public String toQualifiedName() {
return SQLObjectUtils.toQualifiedName(this, SQLDatabase.class);
}
public String toQualifiedName(String quote) {
return SQLObjectUtils.toQualifiedName(this, SQLDatabase.class, quote);
}
/**
* Refreshing tables only works if it is done for the whole
* "table container" at once, since all the columns have to be refreshed
* before any of the FK relationships. As such, this method just throws
* an exception. The generic refresh in SQLObject knows about this requirement,
* and does the right thing when it encounters a table container.
*/
@Override
void refresh() throws SQLObjectException {
// XXX think about automatically forwarding this request to parent.refresh(),
// since parent ought to be the table container we're looking for...
throw new UnsupportedOperationException("Individual tables can't be refreshed.");
}
public boolean removeIndex(SQLIndex sqlIndex) {
if (isMagicEnabled() && sqlIndex.getParent() != this) {
throw new IllegalStateException("Cannot remove child " + sqlIndex.getName() +
" of type " + sqlIndex.getClass() + " as its parent is not " + getName());
}
int index = indices.indexOf(sqlIndex);
if (index != -1) {
try {
begin("Removing index " + sqlIndex.getName());
indices.remove(index);
//Primary key is the first index in the first position.
fireChildRemoved(SQLIndex.class, sqlIndex, index + 1);
sqlIndex.setParent(null);
commit();
return true;
} catch (Throwable t) {
rollback("Failed to remove the index.");
throw new RuntimeException(t);
}
}
return false;
}
@Override
public <T extends SPObject> List<T> getChildren(Class<T> type) {
if (!isMagicEnabled()) {
return getChildrenWithoutPopulating(type);
}
try {
if (type == SQLImportedKey.class) {
//doing nothing because we can now populate imported keys without
//needing columns.
} else if (type == SQLColumn.class) {
populateColumns();
} else if (type == SQLIndex.class) {
populateColumns();
populateIndices();
} else {
populate();
}
return getChildrenWithoutPopulating(type);
} catch (SQLObjectException e) {
throw new RuntimeException("Could not populate " + getName(), e);
}
}
public List<? extends SQLObject> getChildrenWithoutPopulating() {
List<SQLObject> children = new ArrayList<SQLObject>();
children.addAll(columns);
children.addAll(exportedKeys);
children.addAll(importedKeys);
children.add(primaryKeyIndex);
children.addAll(indices);
return Collections.unmodifiableList(children);
}
@Override
protected boolean removeChildImpl(SPObject child) {
if (child instanceof SQLColumn) {
if (isMagicEnabled()) {
SQLColumn col = (SQLColumn) child;
// a column is only locked if it is an IMPORTed key--not if it is EXPORTed.
for (SQLImportedKey k : importedKeys) {
k.getRelationship().checkColumnLocked(col);
}
}
return removeColumn((SQLColumn) child);
} else if (child instanceof SQLRelationship) {
return removeExportedKey((SQLRelationship) child);
} else if (child instanceof SQLImportedKey) {
return removeImportedKey((SQLImportedKey) child);
} else if (child instanceof SQLIndex) {
return removeIndex((SQLIndex) child);
}
return false;
}
public List<? extends SPObject> getDependencies() {
return Collections.emptyList();
}
public void removeDependency(SPObject dependency) {
for (SQLObject child : getChildrenWithoutPopulating()) {
child.removeDependency(dependency);
}
}
/**
* Overridden so that when you set the populated flag, the populated flag
* for columns, imported keys, exported keys and indices are updated
* accordingly as well.
*/
@Override
@Mutator
public void setPopulated(boolean v) {
boolean oldPop = populated;
populated = v;
columnsPopulated = v;
importedKeysPopulated = v;
exportedKeysPopulated = v;
indicesPopulated = v;
firePropertyChange("populated", oldPop, v);
}
void refreshExportedKeys() throws SQLObjectException {
if (!exportedKeysPopulated) {
logger.debug("Not refreshing unpopulated exported keys of " + this);
return;
}
final List<SQLRelationship> newRels = SQLRelationship.fetchExportedKeys(this, null);
if (logger.isDebugEnabled()) {
logger.debug("New imported keys of " + getName() + ": " + newRels);
}
runInForeground(new Runnable() {
public void run() {
try {
SQLObjectUtils.refreshChildren(SQLTable.this, newRels, SQLRelationship.class);
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
}
}
});
}
void refreshIndexes() throws SQLObjectException {
if (!isIndicesPopulated()) return;
Connection con = null;
try {
con = getParentDatabase().getConnection();
DatabaseMetaData dbmd = con.getMetaData();
// indexes (incl. PK)
final List<SQLIndex> newIndexes = SQLIndex.fetchIndicesForTableAndUpdatePK(dbmd, this);
newIndexes.add(0, getPrimaryKeyIndex());
runInForeground(new Runnable() {
public void run() {
try {
SQLObjectUtils.refreshChildren(SQLTable.this, newIndexes, SQLIndex.class);
} catch (SQLObjectException e) {
throw new SQLObjectRuntimeException(e);
}
}
});
} catch (SQLException e) {
throw new SQLObjectException("Refresh failed", e);
} finally {
if (con != null) {
try {
con.close();
} catch (SQLException e) {
logger.error("Failed to close connection. Squishing this exception: ", e);
}
}
}
}
public List<Class<? extends SPObject>> getAllowedChildTypes() {
return allowedChildTypes;
}
/**
* Called when a table is being removed from its parent. This will be called
* before the table is actually removed from its parent. Package private as
* classes outside of the SQLObjects do not need to call this method.
*/
void removeNotify() {
for (int i = exportedKeys.size() - 1; i >= 0; i--) {
exportedKeys.get(i).tableDisconnected();
}
for (int i = importedKeys.size() - 1; i >= 0; i--) {
importedKeys.get(i).getRelationship().tableDisconnected();
}
}
/**
* This method will move the given column to the last position just after
* the primary key. This means that the column will be just below the
* primary key line and not in the primary key.
*
* @param col
* The column to move.
*/
public void moveAfterPK(SQLColumn col) throws SQLObjectException {
int targetIndex;
if (col.isPrimaryKey()) {
targetIndex = getPkSize() - 1;
} else {
targetIndex = getPkSize();
}
int currentIndex = columns.indexOf(col);
changeColumnIndex(currentIndex, targetIndex, false);
}
/**
* If the given column is not part of the primary key the column will be
* moved to the last position in the primary key and added to the primary
* key. If the column is already part of the primary key this method will do
* nothing.
*/
public void addToPK(SQLColumn col) throws SQLObjectException {
if (!col.isPrimaryKey()) {
moveAfterPK(col);
primaryKeyIndex.addIndexColumn(col);
}
}
/**
* Updates the column mappings of relationships for changes to the primary
* key index. This must be done on each column added to the primary key
* index.
* <p>
* Package private because only the SQLIndex need to be concerned about this.
*/
void updateRelationshipsForNewIndexColumn(SQLColumn col) {
if (isMagicEnabled()) {
for (SQLRelationship r : exportedKeys) {
r.fixMappingNewChildInParent(col);
}
}
}
/**
* Updates the column mappings of relationships for each column that gets
* removed from the primary key index. Must be called once per column
* removal on the primary key index.
* <p>
* Package private because only the SQLIndex need to be concerned about this.
*/
void updateRelationshipsForRemovedIndexColumns(SQLColumn col) {
if (isMagicEnabled()) {
for (SQLRelationship r : exportedKeys) {
r.fixMappingChildRemoved(col);
}
}
}
/**
* Helper method for finding if an index is the primary key without causing
* the table to populate.
* <p>
* Package private for use in SQLIndex.
*/
boolean isPrimaryKey(SQLIndex index) {
return index == primaryKeyIndex;
}
/**
* Helper method for finding if a column is in the primary key index without
* causing the table to populate.
* <p>
* Package private for use in SQLColumn.
*
* @param col
* @return
*/
boolean isInPrimaryKey(SQLColumn col) {
return primaryKeyIndex.containsColumn(col);
}
SQLIndex getPrimaryKeyIndexWithoutPopulating() {
return primaryKeyIndex;
}
}