/*
* 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.Collections;
import java.util.List;
import org.apache.log4j.Logger;
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.Transient;
import ca.sqlpower.util.SQLPowerUtils;
/**
* A SQLSchema is a container for SQLTables. If it is in the
* containment hierarchy for a given RDBMS, it will be directly above
* SQLTables. Its parent could be either a SQLDatabase or a SQLCatalog.
*/
public class SQLSchema extends SQLObject {
/**
* Defines an absolute ordering of the child types of this class.
*/
public static final List<Class<? extends SPObject>> allowedChildTypes =
Collections.<Class<? extends SPObject>>singletonList(SQLTable.class);
private static final Logger logger = Logger.getLogger(SQLSchema.class);
private final List<SQLTable> tables = new ArrayList<SQLTable>();
/**
* Creates a list of unpopulated Schema objects corresponding to the list of
* schemas in the given database metadata.
*
* @param dbmd
* The database metadata to get the schema names from.
* @param catalogName
* The catalog under which to look for the schemas. If the
* underlying database does not support catalogs, or you just
* want the schema list for the current catalog, set this
* argument to null.
* @return A list of unpopulated, unparented SQLSchema objects whose names
* and order matches that given by the database metadata.
* @throws SQLObjectException If database access fails.
*/
public static List<SQLSchema> fetchSchemas(DatabaseMetaData dbmd, String catalogName)
throws SQLObjectException {
ResultSet rs = null;
String oldCatalog = null;
try {
if (catalogName != null) {
oldCatalog = dbmd.getConnection().getCatalog();
// This can fail in SS2K because of privilege problems. There is
// apparently no way to check if it will fail; you just have to try.
try {
dbmd.getConnection().setCatalog(catalogName);
} catch (SQLException ex) {
// XXX it would be preferable to store this exception in a popuateFailReason on the containing catalog
logger.info("populate: Could not setCatalog("+catalogName+"). Assuming it's a permission problem. Stack trace:", ex);
return Collections.emptyList();
}
}
List<SQLSchema> schemas = new ArrayList<SQLSchema>();
rs = dbmd.getSchemas();
while (rs.next()) {
String schemaName = rs.getString(1);
if (schemaName != null) {
SQLSchema schema = new SQLSchema(null, schemaName, false);
schema.setNativeTerm(dbmd.getSchemaTerm());
logger.debug("Set schema term to "+schema.getNativeTerm());
schemas.add(schema);
}
}
return schemas;
} catch (SQLException ex) {
throw new SQLObjectException("Failed to get schema names from source database", ex);
} finally {
try {
if (rs != null) rs.close();
} catch (SQLException e) {
logger.warn("Couldn't close result set. Squishing this exception:", e);
}
try {
if (oldCatalog != null) {
dbmd.getConnection().setCatalog(oldCatalog);
}
} catch (SQLException e) {
logger.warn("Couldn't set catalog back to '"+oldCatalog+"'. Squishing this exception:", e);
}
}
}
protected String nativeTerm;
public SQLSchema(boolean populated) {
this(null, null, populated);
}
@Constructor
public SQLSchema(@ConstructorParameter(propertyName = "parent") SQLObject parent,
@ConstructorParameter(propertyName = "name") String name,
@ConstructorParameter(propertyName = "populated") boolean populated) {
if (parent != null && !(parent instanceof SQLCatalog || parent instanceof SQLDatabase)) {
throw new IllegalArgumentException("Parent to SQLSchema must be SQLCatalog or SQLDatabase");
}
setParent(parent);
setName(name);
this.nativeTerm = "schema";
this.populated = populated;
}
@Override
public void updateToMatch(SQLObject source) throws SQLObjectException {
SQLSchema s = (SQLSchema) source;
setName(s.getName());
setNativeTerm(s.getNativeTerm());
setPhysicalName(s.getPhysicalName());
}
public SQLTable findTableByName(String tableName) throws SQLObjectException {
populate();
for (SQLTable child : tables) {
logger.debug("getTableByName: is child '"+child.getName()+"' equal to '"+tableName+"'?");
if (child.getName().equalsIgnoreCase(tableName)) {
return child;
}
}
return null;
}
public String toString() {
return getShortDisplayName();
}
@Transient @Accessor
public boolean isParentTypeDatabase() {
return (getParent() instanceof SQLDatabase);
}
// ---------------------- SQLObject support ------------------------
@Transient @Accessor
public String getShortDisplayName() {
return getName();
}
/**
* Populates this schema from the source database, if there
* is one. Schemas that have no parent should not need to be
* autopopulated, because this makes no sense.
*
* @throws NullPointerException if this schema has no parent database.
*/
protected void populateImpl() throws SQLObjectException {
if (populated) return;
logger.debug("SQLSchema: populate starting");
SQLDatabase parentDatabase = SQLPowerUtils.getAncestor(this, SQLDatabase.class);
if (parentDatabase == null) throw new IllegalStateException("Schema does not have a SQLDatabase ancestor. Can't populate.");
Connection con = null;
ResultSet rs = null;
final List<SQLTable> fetchedTables;
try {
synchronized (parentDatabase) {
con = parentDatabase.getConnection();
DatabaseMetaData dbmd = con.getMetaData();
if ( getParent() instanceof SQLDatabase ) {
fetchedTables = SQLTable.fetchTablesForTableContainer(dbmd, null, getName());
} else if ( getParent() instanceof SQLCatalog ) {
fetchedTables = SQLTable.fetchTablesForTableContainer(dbmd, getParent().getName(), getName());
} else {
throw new RuntimeException("Invalid parent " + getParent());
}
}
} catch (SQLException e) {
throw new SQLObjectException("schema.populate.fail", e);
} finally {
try {
if ( rs != null ) rs.close();
} catch (SQLException e2) {
logger.error("Could not close result set", e2);
}
try {
if ( con != null ) con.close();
} catch (SQLException e2) {
logger.error("Could not close connection", e2);
}
}
runInForeground(new Runnable() {
public void run() {
if (populated) return;
populateSchemaWithList(SQLSchema.this, fetchedTables);
}
});
logger.debug("SQLSchema: populate finished");
}
/**
* Populates the SQLSchema with a given list of children. This must be
* done on the foreground thread.
* <p>
* Package private for use in the {@link SQLObjectUtils}.
*
* @param schema
* The schema 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 populateSchemaWithList(SQLSchema schema, List<SQLTable> children) {
try {
for (SQLTable table : children) {
schema.tables.add(table);
table.setParent(schema);
}
schema.populated = true;
schema.begin("Populating schema");
for (SQLTable table : children) {
schema.fireChildAdded(SQLTable.class, table, schema.tables.indexOf(table));
}
schema.firePropertyChange("populated", false, true);
schema.commit();
} catch (Exception e) {
schema.rollback(e.getMessage());
for (SQLTable table : children) {
schema.tables.remove(table);
}
schema.populated = false;
throw new RuntimeException(e);
}
}
// ----------------- accessors and mutators -------------------
/**
* Gets the value of nativeTerm
*
* @return the value of nativeTerm
*/
@Accessor(isInteresting=true)
public String getNativeTerm() {
return this.nativeTerm;
}
@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);
}
/**
* Sets the value of nativeTerm to a lowercase version of argNativeTerm.
*
* @param argNativeTerm Value to assign to this.nativeTerm
*/
@Mutator
public void setNativeTerm(String argNativeTerm) {
String oldValue = nativeTerm;
if (argNativeTerm != null) argNativeTerm = argNativeTerm.toLowerCase();
this.nativeTerm = argNativeTerm;
firePropertyChange("nativeTerm", oldValue, argNativeTerm);
}
@Override
public List<SQLTable> getChildrenWithoutPopulating() {
return Collections.unmodifiableList(new ArrayList<SQLTable>(tables));
}
@Override
protected boolean removeChildImpl(SPObject child) {
if (child instanceof SQLTable) {
return removeTable((SQLTable) child);
} else {
throw new IllegalArgumentException("Cannot remove children of type "
+ child.getClass() + " from " + getName());
}
}
public boolean removeTable(SQLTable table) {
if (isMagicEnabled() && table.getParent() != this) {
throw new IllegalStateException("Cannot remove child " + table.getName() +
" of type " + table.getClass() + " as its parent is not " + getName());
}
table.removeNotify();
int index = tables.indexOf(table);
if (index != -1) {
tables.remove(index);
fireChildRemoved(SQLTable.class, table, index);
table.setParent(null);
return true;
}
return false;
}
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 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 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);
}
public List<Class<? extends SPObject>> getAllowedChildTypes() {
return allowedChildTypes;
}
}